web-dev-qa-db-fra.com

JWT (JSON Web Token) prolongation automatique de l'expiration

J'aimerais implémenter l'authentification basée sur JWT dans notre nouvelle API REST. Mais puisque l'expiration est définie dans le jeton, est-il possible de la prolonger automatiquement? Je ne veux pas que les utilisateurs aient besoin de se connecter toutes les X minutes s'ils utilisaient activement l'application au cours de cette période. Ce serait un énorme échec UX. 

Mais prolonger l'expiration crée un nouveau jeton (et l'ancien est toujours valable jusqu'à son expiration). Et générer un nouveau jeton après chaque requête me semble ridicule. Cela ressemble à un problème de sécurité lorsque plusieurs jetons sont valides en même temps. Bien sûr, je pourrais invalider l'ancien utilisé à l'aide d'une liste noire, mais il me faudrait stocker les jetons. Et l'un des avantages de JWT est l'absence de stockage.

J'ai trouvé comment Auth0 l'a résolu. Ils utilisent non seulement le jeton JWT, mais également un jeton d'actualisation: https://docs.auth0.com/refresh-token

Mais encore une fois, pour implémenter ceci (sans Auth0), il me faudrait stocker des jetons d'actualisation et conserver leur expiration. Quel est le véritable avantage alors? Pourquoi ne pas avoir un seul jeton (pas JWT) et garder l'expiration sur le serveur?

Y a-t-il d'autres options? L'utilisation de JWT ne convient-elle pas à ce scénario?

410
maryo

Je travaille à Auth0 et j'ai participé à la conception de la fonctionnalité de jeton d'actualisation.

Tout dépend du type d'application et voici notre approche recommandée.

Des applications Web

Un bon modèle consiste à actualiser le jeton avant son expiration.

Définissez l'expiration du jeton sur une semaine et actualisez-le à chaque fois que l'utilisateur ouvre l'application Web et toutes les heures. Si un utilisateur n'ouvre pas l'application pendant plus d'une semaine, il devra se reconnecter et c'est une application Web UX acceptable.

Pour actualiser le jeton, votre API a besoin d'un nouveau point de terminaison qui reçoit un JWT valide et non expiré et renvoie le même JWT signé avec le nouveau champ d'expiration. Ensuite, l'application Web stockera le jeton quelque part.

Applications mobiles/natives

La plupart des applications natives ne se connectent qu'une seule fois. 

L'idée est que le jeton d'actualisation n'expire jamais et qu'il peut toujours être échangé contre un JWT valide.

Le problème avec un jeton qui n'expire jamais, c'est que jamais signifie jamais. Que faites-vous si vous perdez votre téléphone? Donc, il doit être identifiable par l'utilisateur d'une manière ou d'une autre et l'application doit fournir un moyen de révoquer l'accès. Nous avons décidé d'utiliser le nom de l'appareil, par exemple. "iPad de maryo". Ensuite, l'utilisateur peut accéder à l'application et révoquer l'accès à "l'iPad de Maryo".

Une autre approche consiste à révoquer le jeton d'actualisation sur des événements spécifiques. Un événement intéressant est en train de changer le mot de passe.

Nous pensons que JWT n'est pas utile pour ces cas d'utilisation, nous utilisons donc une chaîne générée de manière aléatoire et nous la stockons de notre côté.

478

Si vous gérez vous-même l’autorisation (c’est-à-dire que vous n’utilisez pas de fournisseur comme Auth0), il est possible que les solutions suivantes fonctionnent:

  1. Emettez un jeton JWT avec une échéance relativement courte, disons 15min.
  2. L'application vérifie la date d'expiration du jeton avant toute transaction nécessitant un jeton (le jeton contient la date d'expiration). Si le jeton a expiré, il demande d'abord à l'API de «rafraîchir» le jeton (cette opération est effectuée de manière transparente pour l'UX).
  3. L'API obtient la demande d'actualisation du jeton, mais vérifie d'abord la base de données utilisateur pour voir si un indicateur 'reauth' a été défini par rapport à ce profil utilisateur (le jeton peut contenir l'identifiant de l'utilisateur). Si l'indicateur est présent, l'actualisation du jeton est refusée, sinon un nouveau jeton est émis.
  4. Répéter.

L'indicateur 'reauth' dans le backend de la base de données serait défini lorsque, par exemple, l'utilisateur a réinitialisé son mot de passe. L'indicateur est supprimé lors de la prochaine connexion de l'utilisateur.

En outre, supposons que vous ayez une politique selon laquelle un utilisateur doit se connecter au moins une fois toutes les 72 heures. Dans ce cas, votre logique d'actualisation de jeton d'API vérifiera également la dernière date de connexion de l'utilisateur à partir de la base de données utilisateur et refusera/autorisera l'actualisation de jeton sur cette base.

58
IanB

Une autre solution pour invalider les fichiers JWT, sans stockage sécurisé supplémentaire sur le backend, consiste à implémenter une nouvelle colonne d'entier jwt_version dans la table users. Si l'utilisateur souhaite se déconnecter ou expirer des jetons existants, il incrémente simplement le champ jwt_version.

Lors de la génération d'un nouveau JWT, encodez le jwt_version dans la charge JWT, en incrémentant éventuellement la valeur au préalable si le nouveau JWT doit remplacer tous les autres.

Lors de la validation du fichier JWT, le champ jwt_version est comparé avec le user_id et l'autorisation n'est accordée que si elle correspond.

11
Ollie Bennett

Je bricolais lorsque nous passions nos applications au format HTML5 avec des apis RESTful dans le backend. La solution que j'ai trouvée était:

  1. Le client reçoit un jeton avec une durée de session de 30 minutes (ou quel que soit le délai de session habituel côté serveur) en cas de connexion réussie.
  2. Un temporisateur côté client est créé pour appeler un service afin de renouveler le jeton avant son heure d'expiration. Le nouveau jeton remplacera l'existant lors des prochains appels.

Comme vous pouvez le constater, cela réduit les demandes fréquentes de jetons d'actualisation. Si l'utilisateur ferme le navigateur/l'application avant le déclenchement de l'appel de jeton de renouvellement, le jeton précédent expirera avec le temps et l'utilisateur devra se reconnecter.

Une stratégie plus complexe peut être mise en œuvre pour remédier à l'inactivité de l'utilisateur (par exemple, négliger un onglet de navigateur ouvert). Dans ce cas, l'appel de jeton de renouvellement doit inclure le délai d'expiration prévu, qui ne doit pas dépasser le temps de session défini. L'application devra garder une trace de la dernière interaction de l'utilisateur en conséquence.

Je n'aime pas l'idée de définir une expiration longue. Cette approche risque donc de ne pas bien fonctionner avec les applications natives nécessitant une authentification moins fréquente.

11
coolersport

Bonne question - et la question elle-même regorge d'informations.

L'article Actualiser les jetons: quand les utiliser et comment ils interagissent avec les JWT donne une bonne idée de ce scénario. Quelques points sont: -

  • Les jetons d'actualisation contiennent les informations nécessaires pour obtenir un nouveau jeton d'accès
  • Les jetons d'actualisation peuvent également expirer, mais ont une durée de vie assez longue. 
  • Les jetons d'actualisation sont généralement soumis à des exigences de stockage strictes pour s'assurer que rien ne leur échappe. 
  • Ils peuvent également être mis sur liste noire par le serveur d'autorisation.

Regardez aussi auth0/angular-jwt angularjs

Pour les API Web. read Activer les jetons d'actualisation OAuth dans l'application AngularJS à l'aide de ASP .NET Web API 2 et Owin

9
Lijo

jwt-autorefresh

Si vous utilisez un noeud (React/Redux/Universal JS), vous pouvez installer npm i -S jwt-autorefresh.

Cette bibliothèque planifie l'actualisation des jetons JWT selon le nombre de secondes calculé par l'utilisateur avant l'expiration du jeton d'accès (en fonction de la revendication exp codée dans le jeton). Il dispose d'une suite de tests étendue et vérifie plusieurs conditions pour s'assurer que toute activité étrange est accompagnée d'un message descriptif concernant les erreurs de configuration de votre environnement.

Exemple complet d'implémentation

import autorefresh from 'jwt-autorefresh'

/** Events in your app that are triggered when your user becomes authorized or deauthorized. */
import { onAuthorize, onDeauthorize } from './events'

/** Your refresh token mechanism, returning a promise that resolves to the new access tokenFunction (library does not care about your method of persisting tokens) */
const refresh = () => {
  const init =  { method: 'POST'
                , headers: { 'Content-Type': `application/x-www-form-urlencoded` }
                , body: `refresh_token=${localStorage.refresh_token}&grant_type=refresh_token`
                }
  return fetch('/oauth/token', init)
    .then(res => res.json())
    .then(({ token_type, access_token, expires_in, refresh_token }) => {
      localStorage.access_token = access_token
      localStorage.refresh_token = refresh_token
      return access_token
    })
}

/** You supply a leadSeconds number or function that generates a number of seconds that the refresh should occur prior to the access token expiring */
const leadSeconds = () => {
  /** Generate random additional seconds (up to 30 in this case) to append to the lead time to ensure multiple clients dont schedule simultaneous refresh */
  const jitter = Math.floor(Math.random() * 30)

  /** Schedule autorefresh to occur 60 to 90 seconds prior to token expiration */
  return 60 + jitter
}

let start = autorefresh({ refresh, leadSeconds })
let cancel = () => {}
onAuthorize(access_token => {
  cancel()
  cancel = start(access_token)
})

onDeauthorize(() => cancel())

disclaimer: je suis le mainteneur

6
cchamberlain

En fait, j'ai implémenté cela dans PHP en utilisant le client Guzzle pour créer une bibliothèque cliente pour l'API, mais le concept devrait fonctionner pour d'autres plateformes.

En gros, j'émets deux jetons, un court (5 minutes) et un long qui expire après une semaine. La bibliothèque cliente utilise un middleware pour tenter une actualisation du jeton court si elle reçoit une réponse 401 à une requête. Il réessayera ensuite la demande d'origine et, s'il a été en mesure d'actualiser, obtient la réponse correcte, de manière transparente pour l'utilisateur. En cas d'échec, il enverra simplement la 401 à l'utilisateur.

Si le jeton court a expiré, mais qu'il est toujours authentique et que le jeton long est valide et authentique, il sera actualisé en utilisant un noeud final spécial sur le service que le jeton long authentifie (c'est la seule chose pour laquelle il peut être utilisé). Il utilisera ensuite le jeton court pour obtenir un nouveau jeton long, le prolongeant ainsi d'une semaine à chaque fois qu'il actualise le jeton court.

Cette approche nous permet également de révoquer l'accès dans un délai maximal de 5 minutes, ce qui est acceptable pour notre utilisation sans qu'il soit nécessaire de stocker une liste noire de jetons.

Edition tardive: relisant ce texte quelques mois plus tard, je vous ferai remarquer que vous pouvez révoquer l’accès lors de l’actualisation du jeton court car cela donne la possibilité d’appels plus coûteux (par exemple, appelez la base de données pour voir si l’utilisateur a été banni) sans payer pour chaque appel vers votre service.

6
BytePorter

Que diriez-vous de cette approche:

  • Pour chaque demande du client, le serveur compare le délai d'expiration du jeton avec (currentTime - lastAccessTime).
  • Si expirationTime <(currentTime - lastAccessedTime) , le dernier lastAccessedTime est remplacé par currentTime.
  • En cas d'inactivité du navigateur pendant une durée supérieure à expirationTime ou si la fenêtre du navigateur a été fermée et expirationTime> (currentTime - lastAccessedTime) , le serveur peut alors expirer le jeton et demander à l'utilisateur de se reconnecter .

Nous n'avons pas besoin de point final supplémentaire pour actualiser le jeton dans ce cas . Nous apprécierions tout feedack.

2
sjaiswal

J'ai résolu ce problème en ajoutant une variable dans les données de jeton:

softexp - I set this to 5 mins (300 seconds)

Je règle l'option expiresIn sur l'heure souhaitée avant que l'utilisateur ne soit obligé de se connecter à nouveau. Le mien est réglé à 30 minutes. Cela doit être supérieur à la valeur de softexp.

Lorsque mon application côté client envoie une demande à l'API du serveur (où un jeton est requis, par exemple une page de liste de clients), le serveur vérifie si le jeton soumis est toujours valide ou non en fonction de sa valeur d'expiration (expiresIn) d'origine. Si ce n'est pas valide, le serveur répondra avec un statut particulier pour cette erreur, par exemple. INVALID_TOKEN.

Si le jeton est toujours valide sur la base de la valeur expiredIn, mais qu'il a déjà dépassé la valeur softexp, le serveur répondra avec un statut distinct pour cette erreur, par exemple. EXPIRED_TOKEN:

(Math.floor(Date.now() / 1000) > decoded.softexp)

Côté client, s'il a reçu la réponse EXPIRED_TOKEN, il doit renouveler le jeton automatiquement en envoyant une demande de renouvellement au serveur. Ceci est transparent pour l'utilisateur et automatiquement pris en charge de l'application cliente.

La méthode de renouvellement sur le serveur doit vérifier si le jeton est toujours valide:

jwt.verify(token, secret, (err, decoded) => {})

Le serveur refusera de renouveler les jetons s'il a échoué avec la méthode ci-dessus.

2
James A

Vous trouverez ci-dessous les étapes à suivre pour révoquer votre jeton d'accès JWT: 

1) Lorsque vous vous connectez, envoyez 2 jetons (jeton d'accès, jeton d'actualisation) en réponse au client.
2) Le jeton d'accès aura un délai d'expiration moins long et le délai d'expiration de Refresh.
3) Le client (frontal) stockera le jeton d'actualisation dans son stockage local et le jeton d'accès dans les cookies.
4) Le client utilisera un jeton d'accès pour appeler des API. Mais quand il expire, choisissez le jeton d'actualisation à partir du stockage local et appelez l'API du serveur d'authentification pour obtenir le nouveau jeton.
5) Votre serveur d’authentification disposera d’une API qui acceptera le jeton d’actualisation, en vérifiera la validité et renverra un nouveau jeton d’accès.
6) Une fois le jeton d'actualisation expiré, l'utilisateur sera déconnecté. 

S'il vous plaît, faites-moi savoir si vous avez besoin de plus de détails, je peux également partager le code (Java + Spring Boot).

0
Bhupinder Singh