Invalidation des jetons Web JSON

421

Pour un nouveau projet node.js sur lequel je travaille, je pense à passer d'une approche de session basée sur les cookies (par cela, je veux dire, stocker un identifiant dans un magasin de valeurs-clés contenant des sessions utilisateur dans le navigateur d'un utilisateur) à une approche de session basée sur des jetons (pas de magasin de valeurs-clés) à l'aide de jetons Web JSON (jwt).

Le projet est un jeu qui utilise socket.io - avoir une session basée sur des jetons serait utile dans un tel scénario où il y aura plusieurs canaux de communication dans une seule session (web et socket.io)

Comment fournir une invalidation de jeton / session à partir du serveur en utilisant l'approche jwt?

Je voulais aussi comprendre quels pièges / attaques courants (ou peu communs) je devais rechercher avec ce genre de paradigme. Par exemple, si ce paradigme est vulnérable aux mêmes / différents types d'attaques que l'approche basée sur le magasin de session / cookie.

Donc, disons que j'ai ce qui suit (adapté de ceci et de cela ):

Connexion au magasin de sessions:

app.get('/login', function(request, response) {
    var user = {username: request.body.username, password: request.body.password };
    // Validate somehow
    validate(user, function(isValid, profile) {
        // Create session token
        var token= createSessionToken();

        // Add to a key-value database
        KeyValueStore.add({token: {userid: profile.id, expiresInMinutes: 60}});

        // The client should save this session token in a cookie
        response.json({sessionToken: token});
    });
}

Connexion basée sur les jetons:

var jwt = require('jsonwebtoken');
app.get('/login', function(request, response) {
    var user = {username: request.body.username, password: request.body.password };
    // Validate somehow
    validate(user, function(isValid, profile) {
        var token = jwt.sign(profile, 'My Super Secret', {expiresInMinutes: 60});
        response.json({token: token});
    });
}

-

Une déconnexion (ou invalidation) pour l'approche du magasin de sessions nécessiterait une mise à jour de la base de données KeyValueStore avec le jeton spécifié.

Il semble qu'un tel mécanisme n'existerait pas dans l'approche basée sur les jetons, car le jeton lui-même contiendrait les informations qui existeraient normalement dans le magasin de valeurs-clés.

funseiki
la source
1
Si vous utilisez le package 'express-jwt', vous pouvez jeter un œil à l' isRevokedoption ou tenter de répliquer la même fonctionnalité. github.com/auth0/express-jwt#revoked-tokens
Signus
1
Envisagez d'utiliser un court délai d'expiration sur le jeton d'accès et d'utiliser un jeton d'actualisation, avec une expiration de longue durée, pour permettre de vérifier l'état d'accès de l'utilisateur dans une base de données (liste noire). auth0.com/blog/…
Rohmer
une autre option serait d'attacher l'adresse IP dans la charge utile tout en générant un jeton jwt et en vérifiant l'IP stockée par rapport à la demande entrante pour la même adresse IP. ex: req.connection.remoteAddress dans nodeJs. Il y a des fournisseurs de FAI qui n'émettent pas d'IP statique par client, je pense que ce ne sera pas un problème à moins qu'un client ne se reconnecte à Internet.
Gihan Sandaru

Réponses:

392

Moi aussi, j'ai fait des recherches sur cette question, et bien qu'aucune des idées ci-dessous ne soit une solution complète, elles pourraient aider d'autres personnes à exclure des idées ou à en fournir d'autres.

1) Retirez simplement le jeton du client

Évidemment, cela ne fait rien pour la sécurité côté serveur, mais cela arrête un attaquant en supprimant le jeton d'existence (c'est-à-dire qu'ils devraient avoir volé le jeton avant de se déconnecter).

2) Créez une liste noire de jetons

Vous pouvez stocker les jetons non valides jusqu'à leur date d'expiration initiale et les comparer aux demandes entrantes. Cela semble cependant nier la raison pour laquelle il est entièrement basé sur des jetons, car vous devrez toucher la base de données pour chaque demande. La taille du stockage serait probablement inférieure, car vous n'auriez besoin que de stocker des jetons qui se situaient entre la déconnexion et l'heure d'expiration (c'est un sentiment instinctif et dépend certainement du contexte).

3) Gardez les durées d'expiration des jetons courtes et faites-les tourner souvent

Si vous conservez les durées d'expiration des jetons à des intervalles suffisamment courts et que le client en cours d'exécution effectue le suivi et demande des mises à jour si nécessaire, le numéro 1 fonctionnerait efficacement comme un système de déconnexion complet. Le problème avec cette méthode est qu'elle rend impossible la connexion de l'utilisateur entre les fermetures du code client (selon la durée de l'intervalle d'expiration).

Des plans d'urgence

En cas d'urgence ou si un jeton d'utilisateur a été compromis, vous pouvez autoriser l'utilisateur à modifier un ID de recherche d'utilisateur sous-jacent avec ses informations de connexion. Cela rendrait tous les jetons associés invalides, car l'utilisateur associé ne pourrait plus être trouvé.

Je voulais également noter que c'est une bonne idée d'inclure la dernière date de connexion avec le jeton, afin que vous puissiez appliquer une reconnexion après une période de temps éloignée.

En termes de similitudes / différences en ce qui concerne les attaques utilisant des jetons, ce message répond à la question: https://github.com/dentarg/blog/blob/master/_posts/2014-01-07-angularjs-authentication-with-cookies -vs-token.markdown

Matt Way
la source
3
Excellente approche. Mon instinct serait de faire une combinaison des 3 et / ou de demander un nouveau jeton après chaque "n" demande (par opposition à une minuterie). Nous utilisons redis pour le stockage d'objets en mémoire, et nous pourrions facilement l'utiliser pour le cas n ° 2, puis la latence diminuerait considérablement.
Aaron Wagner
2
Ce message d'horreur de codage offre quelques conseils: gardez les cookies (ou jetons) portant une session courts mais rendez-les invisibles pour l'utilisateur - ce qui semble être conforme à # 3. Mon propre instinct (peut-être parce qu'il est plus traditionnel) consiste simplement à faire en sorte que le jeton (ou un hachage) agisse comme une clé dans la base de données de sessions répertoriée en liste blanche (similaire au n ° 2)
funseiki
8
L'article est bien écrit et est une version élaborée de ce qui 2)précède. Bien que cela fonctionne bien, personnellement, je ne vois pas beaucoup de différence avec les magasins de sessions traditionnels. Je suppose que l'exigence de stockage serait inférieure, mais vous avez toujours besoin d'une base de données. Le plus grand attrait de JWT pour moi était de ne pas utiliser du tout de base de données pour les sessions.
Matt Way
211
Une approche courante pour invalider les jetons lorsqu'un utilisateur modifie son mot de passe consiste à signer le jeton avec un hachage de son mot de passe. Ainsi, si le mot de passe change, tous les jetons précédents ne vérifient pas automatiquement. Vous pouvez l'étendre à la déconnexion en incluant une heure de dernière déconnexion dans l'enregistrement de l'utilisateur et en utilisant une combinaison de la dernière heure de déconnexion et du hachage de mot de passe pour signer le jeton. Cela nécessite une recherche de base de données chaque fois que vous devez vérifier la signature du jeton, mais vous recherchez probablement l'utilisateur de toute façon.
Travis Terry
4
Une liste noire peut être rendue efficace en la gardant en mémoire, de sorte que la base de données n'a besoin que d'être frappée pour enregistrer les invalidations et supprimer les invalidations expirées et uniquement lues au lancement du serveur. Dans une architecture d'équilibrage de charge, la liste noire en mémoire peut interroger la base de données à de courts intervalles, comme 10 secondes, ce qui limite l'exposition des jetons invalidés. Ces approches permettent au serveur de continuer à authentifier les demandes sans accès à la base de données par demande.
Joe Lapp
86

Les idées affichées ci-dessus sont bonnes, mais un moyen très simple et facile d'invalider tous les JWT existants consiste simplement à changer le secret.

Si votre serveur crée le JWT, le signe avec un secret (JWS) puis l'envoie au client, le simple changement du secret invalidera tous les jetons existants et exigera que tous les utilisateurs obtiennent un nouveau jeton pour s'authentifier car leur ancien jeton devient soudainement invalide selon au serveur.

Il ne nécessite aucune modification du contenu réel du jeton (ou de l'ID de recherche).

De toute évidence, cela ne fonctionne que pour un cas d'urgence lorsque vous souhaitez que tous les jetons existants expirent, pour l'expiration par jeton, l'une des solutions ci-dessus est requise (comme un court délai d'expiration du jeton ou invalider une clé stockée à l'intérieur du jeton).

Andy
la source
9
Je pense que cette approche n'est pas idéale. Bien que cela fonctionne et soit certainement simple, imaginez un cas où vous utilisez une clé publique - vous ne voudriez pas recréer cette clé chaque fois que vous souhaitez invalider un seul jeton.
Signus
1
@KijanaWoodard, une paire de clés publique / privée peut être utilisée pour valider la signature aussi efficacement que le secret de l'algorithme RS256. Dans l'exemple montré ici, il mentionne la modification du secret pour invalider un JWT. Cela peut être fait en a) introduisant une fausse clé de pub qui ne correspond pas à la signature ou b) générant une nouvelle clé de pub. Dans cette situation, c'est loin d'être idéal.
Signus
1
@Signus - gotcha. n'utilisant pas la clé publique comme secret, mais d'autres peuvent s'appuyer sur la clé publique pour vérifier la signature.
Kijana Woodard
8
C'est une très mauvaise solution. La principale raison d'utiliser JWT est son état et ses échelles. L'utilisation d'un secret dynamique introduit un état. Si le service est mis en cluster sur plusieurs nœuds, vous devrez synchroniser le secret chaque fois qu'un nouveau jeton est émis. Vous devrez stocker des secrets dans une base de données ou un autre service externe, ce qui reviendrait à réinventer l'authentification basée sur les cookies
Tuomas Toivonen
5
@TuomasToivonen, mais vous devez signer un JWT avec un secret et pouvoir vérifier le JWT avec ce même secret. Vous devez donc stocker le secret sur les ressources protégées. Si le secret est compromis, vous devez le modifier et distribuer ce changement à chacun de vos nœuds. Les fournisseurs d'hébergement avec clustering / scaling vous permettent généralement de stocker des secrets dans leur service pour rendre la distribution de ces secrets facile et fiable.
Rohmer
67

Il s'agit principalement d'un long commentaire soutenant et s'appuyant sur la réponse de @mattway

Donné:

Certaines des autres solutions proposées sur cette page préconisent de frapper le magasin de données à chaque demande. Si vous frappez la banque de données principale pour valider chaque demande d'authentification, je vois moins de raisons d'utiliser JWT au lieu d'autres mécanismes d'authentification de jeton établis. Vous avez essentiellement rendu JWT avec état, au lieu d'être sans état si vous allez à chaque fois dans le magasin de données.

(Si votre site reçoit un volume élevé de demandes non autorisées, JWT les refuserait sans toucher au magasin de données, ce qui est utile. Il existe probablement d'autres cas d'utilisation comme celui-ci.)

Donné:

Une authentification JWT véritablement sans état ne peut pas être obtenue pour une application Web typique, car le JWT sans état n'a aucun moyen de fournir une sécurité immédiate et sécurisée pour les cas d'utilisation importants suivants:

Le compte de l'utilisateur est supprimé / bloqué / suspendu.

Le mot de passe de l'utilisateur a été modifié.

Les rôles ou autorisations de l'utilisateur sont modifiés.

L'utilisateur est déconnecté par l'administrateur.

Toutes les autres données critiques de l'application dans le jeton JWT sont modifiées par l'administrateur du site.

Vous ne pouvez pas attendre l'expiration du jeton dans ces cas. L'invalidation du jeton doit avoir lieu immédiatement. En outre, vous ne pouvez pas faire confiance au client pour ne pas conserver et utiliser une copie de l'ancien jeton, que ce soit avec une intention malveillante ou non.

Par conséquent: je pense que la réponse de @ matt-way, # 2 TokenBlackList, serait le moyen le plus efficace d'ajouter l'état requis à l'authentification basée sur JWT.

Vous avez une liste noire qui contient ces jetons jusqu'à ce que leur date d'expiration soit atteinte. La liste des jetons sera assez petite par rapport au nombre total d'utilisateurs, car elle n'a qu'à conserver les jetons sur liste noire jusqu'à leur expiration. Je mettrais en œuvre en plaçant des jetons invalides dans redis, memcached ou une autre banque de données en mémoire qui prend en charge la définition d'un délai d'expiration sur une clé.

Vous devez toujours appeler votre base de données en mémoire pour chaque demande d'authentification qui passe l'authentification JWT initiale, mais vous n'avez pas à y stocker les clés de l'ensemble de vos utilisateurs. (Ce qui peut ou non être un gros problème pour un site donné.)

Ed J
la source
15
Je ne suis pas d'accord avec votre réponse. Frapper une base de données ne fait rien d'état; le stockage de l'état sur votre backend le fait. JWT n'a pas été créé afin que vous n'ayez pas à accéder à la base de données à chaque demande. Chaque application majeure qui utilise JWT est soutenue par une base de données. JWT résout un problème complètement différent. en.wikipedia.org/wiki/Stateless_protocol
Julian
6
@Julian pouvez-vous nous en dire un peu plus? Quel problème JWT résout-il vraiment alors?
zero01alpha
8
@ zero01alpha Authentication: il s'agit du scénario le plus courant pour utiliser JWT. Une fois l'utilisateur connecté, chaque demande ultérieure inclura le JWT, permettant à l'utilisateur d'accéder aux routes, services et ressources autorisés avec ce jeton. Échange d'informations: les jetons Web JSON sont un bon moyen de transmettre des informations en toute sécurité entre les parties. Parce que les JWT peuvent être signés, vous pouvez être sûr que les expéditeurs sont bien ceux qu'ils disent être. Voir jwt.io/introduction
Julian
7
@Julian Je ne suis pas d'accord avec votre désaccord :) JWT résout le problème (pour les services) d'avoir la nécessité d'accéder à une entité centralisée fournissant des informations d'autorisation pour un client donné. Ainsi, au lieu du service A et du service B doivent accéder à une ressource pour savoir si le client X a ou n'a pas les autorisations pour faire quelque chose, les services A et B reçoivent un jeton de X qui prouve ses autorisations (le plus souvent émis par un tiers fête). Quoi qu'il en soit, JWT est un outil qui permet d'éviter un état partagé entre les services d'un système, en particulier lorsqu'ils sont contrôlés par plusieurs fournisseurs de services.
LIvanov
1
Aussi sur jwt.io/introduction If the JWT contains the necessary data, the need to query the database for certain operations may be reduced, though this may not always be the case.
giantas
43

Je garderais un enregistrement du numéro de version jwt sur le modèle utilisateur. Les nouveaux jetons jwt définiraient leur version sur ceci.

Lorsque vous validez le jwt, vérifiez simplement qu'il a un numéro de version égal à la version jwt actuelle de l'utilisateur.

Chaque fois que vous souhaitez invalider d'anciens jwts, il vous suffit de déplacer le numéro de version des utilisateurs jwt.

DaftMonk
la source
15
C'est une idée intéressante, la seule chose est de savoir où stocker la version, dans le but des jetons, c'est d'être apatride et de ne pas avoir besoin d'utiliser la base de données. Une version codée en dur rendrait la tâche difficile, et un numéro de version dans une base de données annulerait certains des avantages de l'utilisation de jetons.
Stephen Smith
13
Vraisemblablement, vous stockez déjà un ID utilisateur dans votre jeton, puis interrogez la base de données pour vérifier que l'utilisateur existe / est autorisé à accéder au point de terminaison api. Donc, vous ne faites aucune requête DB supplémentaire en comparant le numéro de version du jeton jwt avec celui de l'utilisateur.
DaftMonk
5
Je ne devrais pas dire probablement, car il y a beaucoup de situations où vous pouvez utiliser des jetons avec des validations qui ne touchent pas du tout à la base de données. Mais je pense que dans ce cas, c'est difficile à éviter.
DaftMonk
11
Que se passe-t-il si l'utilisateur se connecte à partir de plusieurs appareils? Un jeton doit-il être utilisé sur tous ou la connexion doit-elle invalider tous les précédents?
meeDamian
10
Je suis d'accord avec @SergioCorrea Cela rendrait JWT presque aussi dynamique que tout autre mécanisme d'authentification par jeton.
Ed J
40

Je n'ai pas encore essayé cela, et il utilise beaucoup d'informations basées sur certaines des autres réponses. La complexité ici est d'éviter un appel de magasin de données côté serveur par demande d'informations utilisateur. La plupart des autres solutions nécessitent une recherche de base de données par demande vers un magasin de sessions utilisateur. C'est très bien dans certains scénarios, mais cela a été créé dans le but d'éviter de tels appels et de rendre très petit l'état côté serveur requis. Vous finirez par recréer une session côté serveur, même petite pour fournir toutes les fonctionnalités d'invalidation forcée. Mais si vous voulez le faire, voici l'essentiel:

Buts:

  • Limitez l'utilisation d'un magasin de données (sans état).
  • Possibilité de forcer la déconnexion de tous les utilisateurs.
  • Possibilité de forcer la déconnexion de tout individu à tout moment.
  • Possibilité d'exiger une nouvelle saisie du mot de passe après un certain temps.
  • Capacité à travailler avec plusieurs clients.
  • Possibilité de forcer une reconnexion lorsqu'un utilisateur clique sur la déconnexion d'un client particulier. (Pour empêcher quelqu'un de «supprimer» un jeton client après que l'utilisateur s'est éloigné - voir les commentaires pour plus d'informations)

La solution:

  • Utiliser des jetons d'accès de courte durée (<5 m) associés à un jeton de rafraîchissement stocké par le client (plus de quelques heures) .
  • Chaque demande vérifie la date d'expiration de l'authentification ou de l'actualisation du jeton pour la validité.
  • Lorsque le jeton d'accès expire, le client utilise le jeton d'actualisation pour actualiser le jeton d'accès.
  • Lors de la vérification du jeton d'actualisation, le serveur vérifie une petite liste noire d'ID utilisateur - s'il est trouvé, rejetez la demande d'actualisation.
  • Lorsqu'un client n'a pas de jeton d'actualisation ou d'authentification valide (non expiré), l'utilisateur doit se reconnecter, car toutes les autres demandes seront rejetées.
  • Sur demande de connexion, vérifiez l'interdiction du magasin de données utilisateur.
  • Lors de la déconnexion - ajoutez cet utilisateur à la liste noire de la session afin qu'il doive se reconnecter. Vous devrez stocker des informations supplémentaires pour ne pas les déconnecter de tous les appareils dans un environnement à plusieurs appareils, mais cela pourrait être fait en ajoutant un champ d'appareil à la liste noire des utilisateurs.
  • Pour forcer la rentrée après x durée - conservez la dernière date de connexion dans le jeton d'authentification et vérifiez-la par demande.
  • Pour forcer la déconnexion de tous les utilisateurs - réinitialisez la clé de hachage de jeton.

Cela vous oblige à maintenir une liste noire (état) sur le serveur, en supposant que la table utilisateur contient des informations utilisateur interdites. La liste noire des sessions invalides - est une liste d'ID utilisateur. Cette liste noire n'est vérifiée que lors d'une demande de jeton d'actualisation. Les entrées sont nécessaires pour y vivre aussi longtemps que le jeton d'actualisation TTL. Une fois le jeton d'actualisation expiré, l'utilisateur devra se reconnecter.

Les inconvénients:

  • Toujours nécessaire pour effectuer une recherche de magasin de données sur la demande de jeton d'actualisation.
  • Les jetons non valides peuvent continuer à fonctionner pour le TTL du jeton d'accès.

Avantages:

  • Fournit la fonctionnalité souhaitée.
  • L'action d'actualisation du jeton est cachée à l'utilisateur en fonctionnement normal.
  • Uniquement requis pour effectuer une recherche dans le magasin de données sur les demandes d'actualisation au lieu de chaque demande. soit 1 toutes les 15 min au lieu de 1 par seconde.
  • Réduit l'état côté serveur à une très petite liste noire.

Avec cette solution, un magasin de données en mémoire comme reddis n'est pas nécessaire, du moins pas pour les informations utilisateur, car le serveur ne fait un appel db que toutes les 15 minutes environ. Si vous utilisez reddis, le stockage d'une liste de sessions valides / invalides serait une solution très rapide et plus simple. Pas besoin de jeton d'actualisation. Chaque jeton d'authentification aurait un identifiant de session et un identifiant de périphérique, ils pourraient être stockés dans une table reddis lors de la création et invalidés le cas échéant. Ensuite, ils seraient vérifiés à chaque demande et rejetés lorsqu'ils n'étaient pas valides.

Ashtonien
la source
Qu'en est-il du scénario où une personne se lève d'un ordinateur pour laisser une autre personne utiliser le même ordinateur? La 1ère personne se déconnectera et s'attendra à ce que la déconnexion bloque instantanément la 2ème personne. Si la 2e personne est un utilisateur moyen, le client pourrait facilement bloquer l'utilisateur en supprimant le jeton. Mais si le 2ème utilisateur possède des compétences de piratage, l'utilisateur a le temps de récupérer le jeton encore valide pour s'authentifier en tant que 1er utilisateur. Il semble qu'il n'y ait aucun moyen d'éviter la nécessité d'invalider immédiatement les jetons, sans délai.
Joe Lapp
5
Ou vous pouvez supprimer votre JWT de sesion / stockage local ou cookie.
Kamil Kiełczewski
1
Merci @Ashtonian. Après avoir fait des recherches approfondies, j'ai abandonné les JWT. Sauf si vous faites des efforts extraordinaires pour sécuriser la clé secrète ou si vous ne déléguez pas à une implémentation OAuth sécurisée, les JWT sont beaucoup plus vulnérables que les sessions ordinaires. Voir mon rapport complet: by.jtl.xyz/2016/06/the-unspoken-vulnerability-of-jwts.html
Joe Lapp
2
L'utilisation d'un jeton d'actualisation est la clé pour autoriser la mise sur liste noire. Grande explication: auth0.com/blog/…
Rohmer
1
Cela me semble la meilleure réponse car il combine un jeton d'accès de courte durée avec un jeton de rafraîchissement de longue durée qui peut être mis sur liste noire. Lors de la déconnexion, le client doit supprimer le jeton d'accès afin qu'un deuxième utilisateur ne puisse pas y accéder (même si le jeton d'accès restera valide pendant quelques minutes après la déconnexion). @Joe Lapp dit qu'un pirate (2e utilisateur) obtient le jeton d'accès même après sa suppression. Comment?
M3RS
14

Une approche que j'ai envisagée est de toujours avoir une valeur iat(émise à) dans le JWT. Ensuite, lorsqu'un utilisateur se déconnecte, stockez cet horodatage sur l'enregistrement utilisateur. Lors de la validation du JWT, il suffit de comparer iatle dernier horodatage déconnecté. Si laiat ancien est plus ancien, il n'est pas valide. Oui, vous devez aller à la base de données, mais je retirerai toujours l'enregistrement utilisateur de toute façon si le JWT est par ailleurs valide.

L'inconvénient majeur que je vois est que cela les déconnecterait de toutes leurs sessions s'ils sont dans plusieurs navigateurs ou s'ils ont également un client mobile.

Cela pourrait également être un bon mécanisme pour invalider tous les JWT d'un système. Une partie de la vérification peut concerner un horodatage global de la dernière iatheure valide .

Brack Mo
la source
1
Bonne pensée! Pour résoudre le problème «d'un seul appareil», il faut en faire une fonction d'urgence plutôt qu'une déconnexion. Stockez la date a dans l'enregistrement utilisateur qui invalide tous les jetons émis avant. Quelque chose comme token_valid_after, ou quelque chose. Impressionnant!
OneHoopyFrood
1
Hé @OneHoopyFrood, vous avez un exemple de code pour m'aider à mieux comprendre l'idée? J'apprécie vraiment votre aide!
alexventuraio
2
Comme toutes les autres solutions proposées, celle-ci nécessite une recherche de base de données, raison pour laquelle cette question existe car éviter cette recherche est la chose la plus importante ici! (Performance, évolutivité). Dans des circonstances normales, vous n'avez pas besoin d'une recherche de base de données pour avoir les données utilisateur, vous les avez déjà obtenues du client.
Rob Evans
9

Je suis un peu en retard ici, mais je pense avoir une solution décente.

J'ai une colonne "last_password_change" dans ma base de données qui stocke la date et l'heure de la dernière modification du mot de passe. Je stocke également la date / heure d'émission dans le JWT. Lors de la validation d'un jeton, je vérifie si le mot de passe a été modifié après l'émission du jeton et s'il s'agissait du jeton, même s'il n'est pas encore expiré.

Matas Kairaitis
la source
1
Comment rejetez-vous le jeton? Pouvez-vous montrer un bref exemple de code?
alexventuraio
1
if (jwt.issue_date < user.last_pw_change) { /* not valid, redirect to login */}
Vanuan
15
Nécessite une recherche db!
Rob Evans
5

Vous pouvez avoir un champ "last_key_used" sur votre base de données sur le document / enregistrement de votre utilisateur.

Lorsque l'utilisateur se connecte avec l'utilisateur et passe, générez une nouvelle chaîne aléatoire, stockez-la dans le champ last_key_used et ajoutez-la à la charge utile lors de la signature du jeton.

Lorsque l'utilisateur se connecte à l'aide du jeton, vérifiez que last_key_used dans la base de données correspond à celui du jeton.

Ensuite, lorsque l'utilisateur se déconnecte par exemple, ou si vous souhaitez invalider le jeton, changez simplement ce champ "last_key_used" en une autre valeur aléatoire et toutes les vérifications ultérieures échoueront, obligeant ainsi l'utilisateur à se connecter avec l'utilisateur et à repasser.

NickVarcha
la source
C'est la solution que j'ai envisagée, mais elle présente ces inconvénients: (1) vous effectuez une recherche de base de données sur chaque demande pour vérifier l'aléatoire (annulant la raison d'utiliser des jetons au lieu de sessions) ou vous êtes vérification uniquement par intermittence après l'expiration d'un jeton d'actualisation (empêchant les utilisateurs de se déconnecter immédiatement ou de mettre fin immédiatement aux sessions); et (2) la déconnexion déconnecte l'utilisateur de tous les navigateurs et de tous les appareils (ce qui n'est pas le comportement habituel).
Joe Lapp
Vous n'avez pas besoin de changer la clé lorsque l'utilisateur se déconnecte, uniquement lorsqu'il change de mot de passe ou - si vous le fournissez - lorsqu'il choisit de se déconnecter de tous les appareils
NickVarcha
3

Gardez une liste en mémoire comme celle-ci

user_id   revoke_tokens_issued_before
-------------------------------------
123       2018-07-02T15:55:33
567       2018-07-01T12:34:21

Si vos jetons expirent dans une semaine, nettoyez ou ignorez les enregistrements plus anciens. Conservez également uniquement l'enregistrement le plus récent de chaque utilisateur. La taille de la liste dépendra de la durée de conservation de vos jetons et de la fréquence à laquelle les utilisateurs révoqueront leurs jetons. Utilisez db uniquement lorsque la table change. Chargez le tableau en mémoire au démarrage de votre application.

Eduardo
la source
2
La plupart des sites de production fonctionnent sur plusieurs serveurs, cette solution ne fonctionnera donc pas. L'ajout de Redis ou d'un cache d'interprocessus similaire complique considérablement le système et apporte souvent plus de problèmes que de solutions.
user2555515
@ user2555515 tous les serveurs peuvent être synchronisés avec la base de données. C'est votre choix de frapper la base de données à chaque fois ou non. Vous pourriez dire quels problèmes cela entraîne.
Eduardo
3

------------------------ Un peu en retard pour cette réponse mais peut-être que cela aidera à quelqu'un ------------- -----------

Du côté client , le moyen le plus simple consiste à supprimer le jeton du stockage du navigateur.

Mais que faire si vous voulez détruire le jeton sur le serveur Node -

Le problème avec le package JWT est qu'il ne fournit aucune méthode ni aucun moyen de détruire le jeton. Vous pouvez utiliser différentes méthodes concernant JWT qui sont mentionnées ci-dessus. Mais ici je vais avec le jwt-redis.

Donc, afin de détruire le jeton sur le côté serveur, vous pouvez utiliser le package jwt-redis au lieu de JWT

Cette bibliothèque (jwt-redis) répète complètement toutes les fonctionnalités de la bibliothèque jsonwebtoken, avec un ajout important. Jwt-redis vous permet de stocker l'étiquette de jeton dans redis pour vérifier la validité. L'absence d'une étiquette de jeton dans redis rend le jeton non valide. Pour détruire le jeton dans jwt-redis, il existe une méthode destroy

cela fonctionne de cette façon:

1) Installez jwt-redis à partir de npm

2) Pour créer -

var redis = require('redis');
var JWTR =  require('jwt-redis').default;
var redisClient = redis.createClient();
var jwtr = new JWTR(redisClient);

jwtr.sign(payload, secret)
    .then((token)=>{
            // your code
    })
    .catch((error)=>{
            // error handling
    });

3) Pour vérifier -

jwtr.verify(token, secret);

4) Détruire -

jwtr.destroy(token)

Remarque : vous pouvez fournir expiresIn lors de la connexion du jeton de la même manière que celle fournie dans JWT.

Peut-être que cela aidera quelqu'un

Aman Kumar Gupta
la source
2

Pourquoi ne pas simplement utiliser la revendication jti (nonce) et la stocker dans une liste en tant que champ d'enregistrement utilisateur (dépendant de la base de données, mais au moins une liste séparée par des virgules convient)? Pas besoin de recherche distincte, comme d'autres l'ont fait remarquer, vous voulez probablement obtenir l'enregistrement utilisateur de toute façon, et de cette façon, vous pouvez avoir plusieurs jetons valides pour différentes instances clientes ("se déconnecter partout" peut réinitialiser la liste à vide)

davidkomer
la source
Oui ca. Peut-être établissez une relation un-à-plusieurs entre la table utilisateur et une nouvelle table (session), afin que vous puissiez stocker des métadonnées avec les revendications jti.
Peter Lada
2
  1. Donner 1 jour d'expiration pour les jetons
  2. Maintenez une liste noire quotidienne.
  3. Mettre les jetons invalides / déconnexion dans la liste noire

Pour la validation du jeton, vérifiez d'abord l'heure d'expiration du jeton, puis la liste noire si le jeton n'a pas expiré.

Pour les longs besoins de session, il devrait y avoir un mécanisme pour prolonger le délai d'expiration du jeton.

Ebru Yener
la source
4
mettre des jetons dans la liste noire et
voilà
2

Tard dans la soirée, MES deux cents sont donnés ci-dessous après quelques recherches. Pendant la déconnexion, assurez-vous que les choses suivantes se produisent ...

Effacer le stockage / la session client

Mettre à jour la date et l'heure de la dernière connexion de la table utilisateur et la date et l'heure de déconnexion chaque fois que la connexion ou la déconnexion se produit respectivement. Ainsi, l'heure de la date de connexion doit toujours être supérieure à la déconnexion (ou garder la date de déconnexion nulle si l'état actuel est la connexion et pas encore la déconnexion)

C'est bien plus simple que de garder un tableau supplémentaire de liste noire et de purger régulièrement. La prise en charge de plusieurs appareils nécessite un tableau supplémentaire pour conserver les dates de connexion, de déconnexion avec des détails supplémentaires tels que les détails du système d'exploitation ou du client.

Shamseer
la source
2

Chaîne unique par utilisateur et chaîne globale hachée ensemble

pour servir de partie secrète JWT, permettre l'invalidation des jetons individuels et globaux. Flexibilité maximale au prix d'une recherche / lecture de base de données pendant l'authentification de la demande. Ils sont également faciles à mettre en cache, car ils changent rarement.

Voici un exemple:

HEADER:ALGORITHM & TOKEN TYPE

{
  "alg": "HS256",
  "typ": "JWT"
}
PAYLOAD:DATA

{
  "sub": "1234567890",
  "some": "data",
  "iat": 1516239022
}
VERIFY SIGNATURE

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload), 
  HMACSHA256('perUserString'+'globalString')
)

where HMACSHA256 is your local crypto sha256
  nodejs 
    import sha256 from 'crypto-js/sha256';
    sha256(message);

par exemple, utilisation https://jwt.io (pas sûr qu'ils gèrent les secrets dynamiques de 256 bits)

Mark Essel
la source
1
Un peu plus de détails suffiraient
giantas
2
@giantas, je pense que ce que Mark veut dire est la partie signature. Donc, au lieu d'utiliser une seule clé pour signer le JWT, combinez-le avec une clé unique pour chaque client. Par conséquent, si vous souhaitez invalider toutes les sessions d'un utilisateur, changez simplement la clé de cet utilisateur et si vous invalidez toutes les sessions de votre système, changez simplement cette clé unique globale.
Tommy Aria Pradana
1

Je l'ai fait de la manière suivante:

  1. Générez un unique hash, puis stockez-le dans redis et votre JWT . Cela peut être appelé une session
    • Nous enregistrerons également le nombre de requêtes que le JWT a effectuées - Chaque fois qu'un jwt est envoyé au serveur, nous incrémentons l' entier des requêtes . (c'est facultatif)

Ainsi, lorsqu'un utilisateur se connecte, un hachage unique est créé, stocké dans redis et injecté dans votre JWT .

Lorsqu'un utilisateur essaie de visiter un point de terminaison protégé, vous récupérez le hachage de session unique de votre JWT , interrogez redis et voyez s'il s'agit d'une correspondance!

Nous pouvons prolonger cela et rendre notre JWT encore plus sécurisé, voici comment:

Chaque X demande un JWT particulier , nous générons une nouvelle session unique, la stockons dans notre JWT , puis mettons la précédente sur liste noire.

Cela signifie que le JWT est en constante évolution et empêche les JWT périmés d'être piratés, volés ou autre chose.

James111
la source
1
Vous pouvez hacher le jeton lui-même et stocker cette valeur dans redis, plutôt que d'injecter un nouveau hachage dans le jeton.
Frug
Consultez également audet jtiréclamations dans JWT, vous êtes sur la bonne voie.
Peter Lada
1

Si vous souhaitez pouvoir révoquer des jetons utilisateur, vous pouvez garder une trace de tous les jetons émis sur votre base de données et vérifier s'ils sont valides (existent) sur une table de type session. L'inconvénient est que vous atteindrez la base de données à chaque demande.

Je ne l'ai pas essayé, mais je suggère la méthode suivante pour autoriser la révocation de jetons tout en limitant les hits de base de données -

Pour réduire le taux de vérification de la base de données, divisez tous les jetons JWT émis en groupes X selon une association déterministe (par exemple, 10 groupes par premier chiffre de l'ID utilisateur).

Chaque jeton JWT contiendra l'ID de groupe et un horodatage créé lors de la création du jeton. par exemple,{ "group_id": 1, "timestamp": 1551861473716 }

Le serveur conservera tous les identifiants de groupe en mémoire et chaque groupe aura un horodatage qui indique à quand remonte le dernier événement de déconnexion d'un utilisateur appartenant à ce groupe. par exemple,{ "group1": 1551861473714, "group2": 1551861487293, ... }

Les demandes avec un jeton JWT qui ont un horodatage de groupe plus ancien seront vérifiées pour la validité (hit DB) et si valides, un nouveau jeton JWT avec un nouvel horodatage sera émis pour une utilisation future par le client. Si l'horodatage de groupe du jeton est plus récent, nous faisons confiance au JWT (aucun hit DB).

Donc -

  1. Nous ne validons un jeton JWT à l'aide de la base de données que si le jeton a un ancien horodatage de groupe, tandis que les demandes futures ne seront validées que lorsque quelqu'un du groupe de l'utilisateur se déconnectera.
  2. Nous utilisons des groupes pour limiter le nombre de changements d'horodatage (disons qu'un utilisateur se connecte et se déconnecte comme s'il n'y en a pas demain - n'affectera qu'un nombre limité d'utilisateurs au lieu de tout le monde)
  3. Nous limitons le nombre de groupes pour limiter la quantité d'horodatages conservés en mémoire
  4. L'invalidation d'un jeton est un jeu d'enfant - il suffit de le supprimer de la table de session et de générer un nouvel horodatage pour le groupe de l'utilisateur.
Arik
la source
La même liste peut être conservée en mémoire (application pour c #) et cela éliminerait le besoin de frapper la base de données pour chaque requête. La liste peut être chargée à partir de db au démarrage de l'application
dvdmn
1

Si l'option "déconnexion de tous les appareils" est acceptable (dans la plupart des cas, elle l'est):

  • Ajoutez le champ de version du jeton à l'enregistrement utilisateur.
  • Ajoutez la valeur de ce champ aux revendications stockées dans le JWT.
  • Incrémentez la version à chaque déconnexion de l'utilisateur.
  • Lors de la validation du jeton, comparez sa revendication de version à la version stockée dans l'enregistrement utilisateur et rejetez si elle n'est pas la même.

Un trajet de base de données pour obtenir l'enregistrement utilisateur dans la plupart des cas est de toute façon nécessaire, ce qui n'ajoute pas beaucoup de frais généraux au processus de validation. Contrairement à la gestion d'une liste noire, où la charge de base de données est importante en raison de la nécessité d'utiliser une jointure ou un appel distinct, nettoyer les anciens enregistrements, etc.

user2555515
la source
0

Je vais répondre si nous devons fournir une fonctionnalité de déconnexion de tous les appareils lorsque nous utilisons JWT. Cette approche utilisera des recherches de base de données pour chaque demande. Parce que nous avons besoin d'un état de sécurité persistant même en cas de panne du serveur. Dans la table utilisateur, nous aurons deux colonnes

  1. LastValidTime (par défaut: heure de création)
  2. Connecté (par défaut: vrai)

Chaque fois qu'il y a une demande de déconnexion de l'utilisateur, nous mettrons à jour LastValidTime à l'heure actuelle et Logged-In à false. S'il y a une demande de connexion, nous ne changerons pas LastValidTime mais Logged-In sera défini sur true.

Lorsque nous créons le JWT, nous aurons l'heure de création du JWT dans la charge utile. Lorsque nous autorisons un service, nous vérifions 3 conditions

  1. JWT est-il valide
  2. Le temps de création de la charge utile JWT est-il supérieur à User LastValidTime
  3. L'utilisateur est-il connecté

Voyons un scénario pratique.

L'utilisateur X a deux appareils A, B. Il s'est connecté à notre serveur à 19 heures en utilisant l'appareil A et l'appareil B. (disons que le délai d'expiration JWT est de 12 heures). A et B ont tous les deux JWT avec createdTime: 7pm

À 21 heures, il a perdu son appareil B. Il se déconnecte immédiatement de l'appareil A. Cela signifie que maintenant, notre entrée d'utilisateur de base de données X a LastValidTime comme "ThatDate: 9: 00: xx: xxx" et connecté comme "false".

À 9h30, le Mr.Thief essaie de se connecter en utilisant le périphérique B. Nous vérifierons la base de données même si la connexion est fausse, donc nous ne le permettrons pas.

À 22 h, M.X se connecte à partir de son appareil A. Maintenant, l'appareil A a JWT avec l'heure de création: 22 h. La base de données connectée est désormais définie sur "true"

À 22h30, Mr.Thief essaie de se connecter. Même si la connexion est vraie. Le LastValidTime est 21 h dans la base de données mais le JWT de B a créé l'heure à 19 h. Il ne sera donc pas autorisé à accéder au service. Donc, en utilisant le périphérique B sans avoir le mot de passe, il ne peut pas utiliser le JWT déjà créé après la déconnexion d'un périphérique.

Tharsanan
la source
0

Une solution IAM comme Keycloak (sur laquelle j'ai travaillé) fournit un point de terminaison de révocation de jetons comme

Point de terminaison de révocation de jeton /realms/{realm-name}/protocol/openid-connect/revoke

Si vous souhaitez simplement déconnecter un agent utilisateur (ou utilisateur), vous pouvez également appeler un point de terminaison (cela invaliderait simplement les jetons). Encore une fois, dans le cas de Keycloak, la partie utilisatrice doit simplement appeler le point de terminaison

/realms/{realm-name}/protocol/openid-connect/logout

Lien au cas où vous voulez en savoir plus

Subbu Mahadevan
la source
-1

Cela semble vraiment difficile à résoudre sans une recherche de base de données à chaque vérification de jeton. L'alternative à laquelle je pense est de conserver une liste noire de jetons invalides côté serveur; qui doit être mis à jour sur une base de données chaque fois qu'un changement survient pour conserver les changements à travers les redémarrages, en faisant vérifier par le serveur la base de données au redémarrage pour charger la liste noire actuelle.

Mais si vous le conservez dans la mémoire du serveur (une variable globale de toutes sortes), il ne sera pas évolutif sur plusieurs serveurs si vous en utilisez plusieurs, donc dans ce cas, vous pouvez le conserver sur un cache Redis partagé, qui devrait être mis en place pour conserver les données quelque part (base de données? système de fichiers?) au cas où il devrait être redémarré, et chaque fois qu'un nouveau serveur est lancé, il doit s'abonner au cache Redis.

Alternative à une liste noire, en utilisant la même solution, vous pouvez le faire avec un hachage enregistré en redis par session comme le souligne cette autre réponse (pas sûr que ce serait plus efficace avec de nombreux utilisateurs se connectant cependant).

Cela vous semble-t-il terriblement compliqué? ça me fait!

Avertissement: je n'ai pas utilisé Redis.

Jose PV
la source
-1

Si vous utilisez axios ou une bibliothèque de requêtes http basée sur une promesse similaire, vous pouvez simplement détruire le jeton sur le front-end à l'intérieur de la .then()pièce. Il sera lancé dans la partie réponse .then () une fois que l'utilisateur aura exécuté cette fonction (le code de résultat du point de terminaison du serveur doit être correct, 200). Une fois que l'utilisateur a cliqué sur cette route lors de la recherche de données, si le champ de la base de données user_enabledest faux, il déclenchera la destruction du jeton et l'utilisateur sera immédiatement déconnecté et empêché d'accéder aux routes / pages protégées. Nous n'avons pas à attendre l'expiration du jeton pendant que l'utilisateur est connecté en permanence.

function searchForData() {   // front-end js function, user searches for the data
    // protected route, token that is sent along http request for verification
    var validToken = 'Bearer ' + whereYouStoredToken; // token stored in the browser 

    // route will trigger destroying token when user clicks and executes this func
    axios.post('/my-data', {headers: {'Authorization': validToken}})
     .then((response) => {
   // If Admin set user_enabled in the db as false, we destroy token in the browser localStorage
       if (response.data.user_enabled === false) {  // user_enabled is field in the db
           window.localStorage.clear();  // we destroy token and other credentials
       }  
    });
     .catch((e) => {
       console.log(e);
    });
}
Dan
la source
-3

Je viens d'enregistrer le jeton dans la table des utilisateurs, lors de la connexion de l'utilisateur, je mettrai à jour le nouveau jeton et lorsque l'auth sera égal au jwt actuel de l'utilisateur.

Je pense que ce n'est pas la meilleure solution mais ça marche pour moi.

Vo Manh Kien
la source
2
Bien sûr, ce n'est pas le meilleur! Toute personne ayant accès à la base de données peut facilement se faire passer pour n'importe quel utilisateur.
user2555515
1
@ user2555515 Cette solution fonctionne très bien si le jeton qui est stocké dans la base de données est crypté, comme tout mot de passe stocké dans la base de données devrait l'être. Il y a une différence entre Stateless JWTet Stateful JWT(qui est très similaire aux sessions). Stateful JWTpeut bénéficier du maintien d'une liste blanche de jetons.
TheDarkIn1978