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


509

Je voudrais 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 après toutes les X minutes s'ils utilisaient activement l'application pendant cette période. Ce serait un énorme échec UX.

Mais prolonger l'expiration crée un nouveau jeton (et l'ancien est toujours valide jusqu'à son expiration). Et générer un nouveau jeton après chaque demande me semble idiot. Cela ressemble à un problème de sécurité lorsque plusieurs jetons sont valides en même temps. Bien sûr, je pouvais invalider l'ancien utilisé en utilisant une liste noire, mais je devrais 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 un jeton JWT mais également un jeton d'actualisation: https://docs.auth0.com/refresh-token

Mais encore une fois, pour implémenter cela (sans Auth0), je devrais stocker des jetons de rafraîchissement et maintenir leur expiration. Quel est donc le véritable avantage? Pourquoi ne pas avoir un seul jeton (pas JWT) et conserver l'expiration sur le serveur?

Y a-t-il d'autres options? L'utilisation de JWT n'est-elle pas adaptée à ce scénario?


1
En fait, il n'y a probablement pas de problème de sécurité avec de nombreux jetons valides à la fois ... Il y a en fait un nombre infini de jetons valides ... Alors, pourquoi avoir un jeton d'actualisation alors? Je les régénérerai après chaque demande, cela ne devrait en fait pas être un problème.
maryo

1
Pour SPA, consultez mon article de blog: blog.wong2.me/2017/02/20/refresh-auth0-token-in-spa
wong2

2
@maryo Je pense qu'avoir (potentiellement) des centaines ou des milliers de JWT valides inutilisés à un moment donné augmente votre empreinte d'attaque et constitue un risque pour la sécurité. Dans mon esprit, les JWT devraient être émis avec soin car ce sont des jetons d'accès avec les clés du château d'une manière.
java-addict301

Réponses:


590

Je travaille chez Auth0 et j'ai été impliqué dans la conception de la fonctionnalité d'actualisation du jeton.

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

des applications Web

Un bon schéma consiste à actualiser le jeton avant son expiration.

Définissez l'expiration du jeton sur une semaine et actualisez le jeton à 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 ceci est une UX d'application web 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 est que cela ne signifie jamais. Que faites-vous si vous perdez votre téléphone? Il doit donc ê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 change 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é.


42
Pour l'approche recommandée par les applications Web, si le jeton est valide pendant une semaine, ne nous préoccupons-nous pas de quelqu'un qui intercepte le jeton et puisse ensuite l'utiliser pendant si longtemps? Avertissement: je ne sais pas de quoi je parle.
user12121234

30
@wbeange yes l'interception est un problème, même avec les cookies. Vous devez utiliser https.
José F. Romaniello

15
@ JoséF.Romaniello Dans votre exemple d'application Web, tout a du sens pour moi sauf d'avoir à stocker le jeton. Je pensais que la beauté de JWT était l'authentification sans état - ce qui signifie que l'application Web n'a PAS à stocker le jeton lors de sa signature. Je pense que le serveur pourrait simplement vérifier la validité du jeton, s'assurer qu'il est dans la période d'expiration, puis émettre un jeton JWT renouvelé. Peux tu développer ta pensée à ce propos? Peut-être que je ne comprends pas encore assez les JWT.
Lo-Tan

7
Deux questions / préoccupations: 1- Cas d'application Web: pourquoi le jeton expiré ne peut-il pas être autorisé à être actualisé? Supposons que nous définissions une expiration courte (1 heure) et que nous renouvelions les appels au serveur principal lorsqu'un expéditeur expire, comme vous l'avez dit. 2- Existe-t-il un problème de sécurité lié au stockage du mot de passe haché (avec sel aléatoire) dans le jeton? L'idée est que s'il est là, le serveur principal peut vérifier le mot de passe stocké dans la base de données lorsqu'on lui demande un renouvellement, et refuser la demande si les mots de passe ne correspondent pas. Cela couvrirait le changement de mot de passe de l'application Mobile / Native, permettant à la solution d'être étendue au cas d'utilisation Mobile.
psamaan

8
-1 L'exposition d'une API publique qui re-signe aveuglément n'importe quel jeton pour étendre sa période de validation est mauvaise. Désormais, tous vos jetons ont une expiration infinie effective. L'acte de signature d'un jeton doit inclure les vérifications d'authentification appropriées pour chaque réclamation faite dans ce jeton au moment de la signature.
Phil

69

Dans le cas où vous gérez vous-même l'authentification (c'est-à-dire n'utilisez pas de fournisseur comme Auth0), les éléments suivants peuvent fonctionner:

  1. Émettez un jeton JWT avec une expiration relativement courte, disons 15 minutes.
  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 (cela se fait de manière transparente pour l'UX).
  3. L'API reçoit une demande d'actualisation du jeton, mais vérifie d'abord la base de données des utilisateurs pour voir si un indicateur de «réautorisation» a été défini par rapport à ce profil d'utilisateur (le jeton peut contenir l'ID 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 est défini lorsque, par exemple, l'utilisateur a réinitialisé son mot de passe. L'indicateur est supprimé lorsque l'utilisateur se connecte la prochaine fois.

De plus, 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 API vérifierait également la dernière date de connexion de l'utilisateur à partir de la base de données utilisateur et refuser / autoriser l'actualisation du jeton sur cette base.


7
Je ne pense pas que ce serait sûr. Si j'étais un attaquant et que je volais votre jeton et l'envoyais au serveur, le serveur vérifierait et verrait que le drapeau est défini sur true, ce qui est génial car il bloquerait une actualisation. Le problème, je pense, serait que si la victime changeait son mot de passe, le drapeau serait mis à faux et maintenant l'attaquant peut utiliser ce jeton d'origine pour se rafraîchir.
user2924127

6
@ user2924127 aucune solution d'authentification n'est parfaite et il y aura toujours des compromis. Si un attaquant est en mesure de `` voler votre jeton '', vous pourriez avoir de plus gros problèmes à vous inquiéter. La définition d'une durée de vie maximale du jeton serait un ajustement utile à ce qui précède.
IanB

27
au lieu d'avoir un autre champ dans la base de données, l'indicateur reauth, vous pouvez inclure le hachage (bcrypt_password_hash) dans le jeton. Ensuite, lors de l'actualisation du jeton, vous confirmez simplement si le hachage (bcrypt_password_hash) est égal à une valeur du jeton. Afin de refuser l'actualisation du jeton, il suffit de mettre à jour le hachage du mot de passe.
bas

4
@bas, en pensant aux optimisations et aux performances, je pense que la validation du hachage de mot de passe serait redondante et aurait plus d'implications sur le serveur. Augmentez la taille du jeton afin que la signature / validation de la signature prenne plus de temps. calculs de hachage supplémentaires pour le serveur pour le mot de passe. avec l'approche de champ supplémentaire que vous venez de valider dans le recalcul avec un simple booléen. Les mises à jour de base de données sont moins fréquentes pour le champ supplémentaire, mais sont plus fréquemment mises à jour des jetons. Et vous bénéficiez du service optionnel de force de reconnexion individuelle pour toute session existante (mobile, web, etc.).
le0diaz

6
Je pense que le premier commentaire de user2924127 est en fait faux. Lorsque le mot de passe est modifié, le compte est marqué comme nécessitant une ré-authentification, donc tous les jetons expirés existants seront invalides.
Ralph

15

Je bricolais lors du déplacement de nos applications vers HTML5 avec des API RESTful dans le backend. La solution que j'ai trouvée était:

  1. Le client reçoit un jeton avec un temps de session de 30 minutes (ou quel que soit le temps de session côté serveur habituel) une fois la 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 dans les appels futurs.

Comme vous pouvez le voir, 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 renouvellement de jeton, le jeton précédent expirera à temps et l'utilisateur devra se reconnecter.

Une stratégie plus compliquée peut être mise en œuvre pour répondre à l'inactivité des utilisateurs (par exemple, négligé un onglet de navigateur ouvert). Dans ce cas, l'appel de renouvellement de jeton doit inclure le temps d'expiration attendu qui ne doit pas dépasser le temps de session défini. L'application devra suivre la dernière interaction de l'utilisateur en conséquence.

Je n'aime pas l'idée de définir une longue expiration, donc cette approche peut ne pas fonctionner correctement avec des applications natives nécessitant une authentification moins fréquente.


1
Et si l'ordinateur était suspendu / en veille. Le chronomètre comptera toujours jusqu'à l'expiration, mais le jeton était en fait déjà expiré. La minuterie ne fonctionne pas dans ces situations
Alex Parij

@AlexParij Vous compareriez avec un temps fixe, quelque chose comme ceci: stackoverflow.com/a/35182296/1038456
Aparajita

2
Permettre au client de demander un nouveau jeton avec une date d'expiration préférée me sent comme un risque pour la sécurité.
java-addict301

14

Une solution alternative pour invalider les JWT, sans stockage sécurisé supplémentaire sur le backend, consiste à implémenter une nouvelle jwt_versioncolonne entière sur la table des utilisateurs. Si l'utilisateur souhaite se déconnecter ou expirer les jetons existants, il incrémente simplement lejwt_version champ.

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

Lors de la validation du JWT, le jwt_versionchamp est comparé à côté de user_idet l'autorisation n'est accordée que si elle correspond.


1
Cela a des problèmes avec plusieurs appareils. Essentiellement, si vous vous déconnectez sur un appareil, il se déconnecte partout. Droite?
Sam Washburn

4
Hé, cela peut ne pas être un "problème" selon vos besoins, mais vous avez raison; cela ne prend pas en charge la gestion de session par appareil.
Ollie Bennett

Cela ne signifie-t-il pas que jwt_version doit être stocké côté serveur de telle sorte que le schéma d'authentification devienne "semblable à une session" et vienne à l'encontre de l'objectif fondamental des JWT?
ChetPrickles

8

Bonne question - et il y a une mine d'informations dans la question elle-même.

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 plutôt longue.
  • Les jetons d'actualisation sont généralement soumis à des exigences de stockage strictes pour garantir qu'ils ne fuient pas.
  • Ils peuvent également être mis sur liste noire par le serveur d'autorisation.

Jetez également un œil à auth0 / angular-jwt angularjs

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


Peut-être que je l'ai mal lu ... Mais l'article dont le titre commence par "Refresh Tokens ..." ne contient rien sur les jetons d'actualisation, sauf ce que vous avez mentionné ici.
Ievgen Martynov,

8

J'ai en fait implémenté cela en PHP en utilisant le client Guzzle pour créer une bibliothèque cliente pour l'API, mais le concept devrait fonctionner pour d'autres plates-formes.

Fondamentalement, 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 alors la demande d'origine et s'il a pu actualiser obtient la réponse correcte, de manière transparente pour l'utilisateur. S'il échoue, il enverra simplement le 401 à l'utilisateur.

Si le jeton court a expiré, mais reste authentique et que le jeton long est valide et authentique, il actualisera le jeton court en utilisant un point de terminaison 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 une semaine de plus chaque fois qu'il actualise le jeton court.

Cette approche nous permet également de révoquer l'accès dans un délai maximum de 5 minutes, ce qui est acceptable pour notre utilisation sans avoir à stocker une liste noire de jetons.

Édition tardive: relisant ce mois après qu'il était frais dans ma tête, je dois souligner 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é interdit) sans le payer à chaque appel vers votre service.


8

Voici 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 inférieur et le rafraîchissement aura un délai d'expiration long.
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 les API. Mais lorsqu'il expire, choisissez le jeton d'actualisation dans le stockage local et appelez l'API du serveur d'authentification pour obtenir le nouveau jeton.
5) Votre serveur d'authentification aura une API exposée qui acceptera le jeton de rafraîchissement et vérifiera sa validité et retournera un nouveau jeton d'accès.
6) Une fois le jeton d'actualisation expiré, l'utilisateur sera déconnecté.

Veuillez me faire savoir si vous avez besoin de plus de détails, je peux également partager le code (Java + Spring boot).


Pourriez-vous s'il vous plaît partager le lien de votre projet si vous l'avez dans GitHub?
Arun Kumar N


6

jwt-autorefresh

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

Cette bibliothèque planifie l'actualisation des jetons JWT à un nombre calculé par l'utilisateur de secondes 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 complète 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 d'implémentation complet

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())

mentions légales: je suis le mainteneur


Question à ce sujet, j'ai vu la fonction de décodage qu'il utilise. Est-ce que cela suppose que le JWT peut être décodé sans utiliser de secret? Cela fonctionne-t-il avec des JWT qui ont été signés avec un secret?
Gian Franco Zabarino

3
Oui, le décodage est un décodage réservé au client et ne doit pas connaître le secret. Le secret est utilisé pour signer le jeton JWT côté serveur pour vérifier que votre signature a été utilisée pour générer le JWT à l'origine et ne doit jamais être utilisée à partir du client. La magie de JWT est que sa charge utile peut être décodée côté client et que les revendications à l'intérieur peuvent être utilisées pour créer votre interface utilisateur sans le secret. La seule chose qui le jwt-autorefreshdécode est d'extraire la exprevendication afin qu'elle puisse déterminer dans quelle mesure planifier la prochaine actualisation.
cchamberlain

1
Oh bon à savoir, quelque chose n'avait pas de sens mais maintenant ça l'est. Merci d'avoir répondu.
Gian Franco Zabarino

4

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)

J'ai défini l' expiresInoption à l'heure souhaitée avant que l'utilisateur ne soit obligé de se reconnecter. Le mien est réglé sur 30 minutes. Elle doit être supérieure à la valeur de softexp.

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

Si le jeton est toujours valide en fonction de la expiredInvaleur, mais qu'il a déjà dépassé la softexpvaleur, le serveur répondra avec un état distinct pour cette erreur, par exemple. EXPIRED_TOKEN:

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

Côté client, s'il a reçu EXPIRED_TOKEN réponse, il doit renouveler le token automatiquement en envoyant une demande de renouvellement au serveur. Cela est transparent pour l'utilisateur et est automatiquement pris en charge par 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é la méthode ci-dessus.


Cette stratégie semble bonne. Mais je pense que cela devrait être complété par une sorte de "quantité maximale de renouvellements" parce que (peut-être) une session utilisateur peut être vivante pour toujours.
Juan Ignacio Barisich

1
Vous pouvez définir une variable hardExp dans les données de jeton pour définir une date maximale pour forcer l'expiration du jeton, ou peut-être un compteur qui est décrémenté chaque fois que le jeton est renouvelé, ce qui limite le nombre total de renouvellements de jeton.
James A

1
c'est correct. Je considère cela comme un "must".
Juan Ignacio Barisich

2

Que diriez-vous de cette approche:

  • Pour chaque demande client, le serveur compare l'expirationTime du jeton avec (currentTime - lastAccessTime)
  • Si expirationTime <(currentTime - lastAccessedTime) , il remplace le dernier lastAccessedTime par currentTime.
  • En cas d'inactivité sur le navigateur pendant une durée dépassant expirationTime ou dans le cas où 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 de terminaison supplémentaire pour actualiser le jeton dans ce cas. J'apprécierais n'importe quel feedack.


Est-ce un bon choix de nos jours, il semble assez facile à mettre en œuvre.
b.ben

4
Dans ce cas, où stockez-vous lastAccessedTime? Vous devez le faire sur le backend et par demande, donc cela devient une solution dynamique non souhaitée.
antgar9

2

Aujourd'hui, beaucoup de gens optent pour faire de la gestion de session avec JWTs sans être conscients de ce qu'ils donnent pour l'amour de la perception de simplicité. Ma réponse développe la 2ème partie des questions:

Quel est donc le véritable avantage? Pourquoi ne pas avoir un seul jeton (pas JWT) et conserver l'expiration sur le serveur?

Y a-t-il d'autres options? L'utilisation de JWT n'est-elle pas adaptée à ce scénario?

Les JWT sont capables de prendre en charge la gestion de session de base avec certaines limitations. Étant des jetons auto-descriptifs, ils ne nécessitent aucun état côté serveur. Cela les rend attrayants. Par exemple, si le service n'a pas de couche de persistance, il n'est pas nécessaire d'en apporter une uniquement pour la gestion de session.

Cependant, l'apatridie est également la principale cause de leurs lacunes. Puisqu'ils ne sont émis qu'une seule fois avec un contenu et une expiration fixes, vous ne pouvez pas faire les choses que vous souhaitez avec une configuration de gestion de session typique.

À savoir, vous ne pouvez pas les invalider à la demande. Cela signifie que vous ne pouvez pas implémenter une déconnexion sécurisée car il n'y a aucun moyen d'expiration des jetons déjà émis. Vous ne pouvez pas non plus implémenter le délai d'inactivité pour la même raison. Une solution consiste à conserver une liste noire, mais cela introduit l'état.

J'ai écrit un article expliquant ces inconvénients plus en détail. Pour être clair, vous pouvez contourner ces problèmes en ajoutant plus de complexité (sessions glissantes, jetons d'actualisation, etc.)

En ce qui concerne les autres options, si vos clients interagissent uniquement avec votre service via un navigateur, je recommande fortement d'utiliser une solution de gestion de session basée sur les cookies. J'ai également compilé une liste des méthodes d'authentification actuellement largement utilisées sur le web.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.