web-dev-qa-db-fra.com

Invalidation des jetons Web JSON

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

Le projet est un jeu qui utilise socket.io. Il serait utile d’avoir une session à base de jetons dans un tel scénario où il y aura plusieurs canaux de communication au cours d’une même session (Web et socket.io).

Comment pourrait-on fournir une invalidation de jeton/session du serveur en utilisant l'approche jwt?

Je voulais aussi comprendre quels pièges/attaques communs (ou rares) je devrais rechercher avec ce type 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/les cookies.

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

Connexion au magasin de session:

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 des 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 une invalidation) pour l'approche Session Store 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 puisque le jeton lui-même contiendrait les informations qui existeraient normalement dans le magasin de valeurs-clés.

328
funseiki

Moi aussi, je suis à la recherche de cette question et, bien qu'aucune des idées ci-dessous ne soit une solution complète, elle pourrait aider les autres à écarter les idées ou à en proposer d'autres.

1) Il suffit de retirer le jeton du client

Évidemment, cela ne fait rien pour la sécurité côté serveur, mais cela empêche un attaquant de supprimer le jeton (c’est-à-dire qu’il aurait dû lui 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 toutefois nier la raison de l’utilisation de jetons entièrement basés sur la base de données, car vous auriez besoin de toucher la base de données pour chaque demande. Cependant, la taille de stockage sera probablement plus faible, car vous n’auriez besoin que de stocker les jetons situés entre le moment de la déconnexion et celui de l’expiration (c’est un sentiment instinctif, qui dépend définitivement du contexte).

3) Gardez simplement les temps d'expiration des jetons et tournez-les souvent

Si vous conservez les heures d'expiration des jetons à des intervalles suffisamment courts et que le client en cours d'exécution garde une trace et demande des mises à jour si nécessaire, le numéro 1 fonctionnerait comme un système de déconnexion complet. Le problème avec cette méthode est qu’il est impossible de garder l’utilisateur connecté entre les fermetures du code client (en fonction de la longueur de l’intervalle d’expiration).

Plans d'urgence

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

Je souhaitais également noter qu'il est judicieux d'inclure la dernière date de connexion avec le jeton, afin de pouvoir appliquer une nouvelle connexion après un certain laps de temps.

En termes de similitudes/différences en ce qui concerne les attaques utilisant des jetons, ce message adresse la question suivante: http://blog.auth0.com/2014/01/07/angularjs-authentication-with-cookies-vs-token /

298
Matt Way

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. Changer le secret invalidera tous les jetons existants et obligera tous les utilisateurs à obtenir 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).

Il est clair que cela ne fonctionne que dans les cas d'urgence où vous voulez que tous les jetons existants expirent, car l'une des solutions ci-dessus est requise (par exemple, l'expiration d'un jeton court ou l'invalidation d'une clé stockée à l'intérieur du jeton).

74
Andy

Ceci est principalement un long commentaire soutenant et développant 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 appuyez sur le magasin de données principal pour valider chaque demande d'authentification, je vois moins de raisons d'utiliser JWT au lieu des autres mécanismes d'authentification de jeton établis. Vous avez essentiellement rendu JWT avec état, au lieu d'être sans état, si vous accédez au magasin de données à chaque fois.

(Si votre site reçoit un grand nombre de requêtes non autorisées, JWT les refusera sans toucher au magasin de données, ce qui est utile. Il existe probablement d'autres cas d'utilisation similaires.)

Donné:

Une authentification JWT véritablement sans état ne peut pas être obtenue pour une application Web typique du monde réel car JWT sans état ne dispose pas d'un moyen de fournir une prise en charge 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 est changé.

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

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

Toute autre donnée critique d'application contenue dans le jeton JWT est modifiée 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. De plus, 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 contenant ces jetons jusqu'à ce que leur date d'expiration soit atteinte. La liste des jetons sera relativement petite par rapport au nombre total d'utilisateurs, car il ne reste plus qu'à conserver les jetons de la liste noire jusqu'à leur expiration. Je mettrais en œuvre en plaçant des jetons invalidés dans redis, memcached ou un autre magasin de données en mémoire prenant en charge la définition d'un délai d'expiration pour une clé.

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

54
Ed J

Je voudrais garder une trace du numéro de version de JWT sur le modèle de l'utilisateur. Les nouveaux jetons jwt définiraient leur version en conséquence.

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

Chaque fois que vous souhaitez invalider d'anciens jwts, il suffit de remplacer le numéro de version de l'utilisateur par jwt.

37
DaftMonk

Je n’ai pas encore essayé, et c’est beaucoup d’informations qui reposent sur d’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 requête dans un magasin de session utilisateur. Cela convient dans certains scénarios, mais cela a été créé dans le but d'éviter de tels appels et de faire en sorte que l'état requis côté serveur soit très petit. Vous finirez par recréer une session côté serveur, même la plus petite possible, pour fournir toutes les fonctionnalités d'invalidation de force. Mais si vous voulez le faire, voici le Gist:

Objectifs:

  • Atténuer 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 la ressaisie du mot de passe après un certain temps.
  • Capacité à travailler avec plusieurs clients.
  • Possibilité de forcer une nouvelle connexion lorsqu'un utilisateur clique sur la déconnexion d'un client particulier. (Pour empêcher quelqu'un de "supprimer la suppression" d'un jeton client après le départ de l'utilisateur - voir les commentaires pour plus d'informations)

La solution:

  • Utilisez des jetons d'accès de courte durée (<5 m) associés à un client plus long (quelques heures) stocké (rafraîchissement de jeton .
  • Chaque demande vérifie la validité de la date d'expiration de l'authentification ou du jeton d'actualisation.
  • Lorsque le jeton d'accès expire, le client utilise le jeton d'actualisation pour actualiser le jeton d'accès.
  • Au cours de la vérification du jeton d'actualisation, le serveur vérifie une petite liste noire d'identifiants d'utilisateurs. Le cas échéant, rejetez la demande d'actualisation.
  • Lorsqu'un client ne dispose pas d'un 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 le magasin de données de l'utilisateur pour obtenir une interdiction.
  • À la déconnexion - ajoutez cet utilisateur à la liste noire de la session pour qu'il se reconnecte. Vous devez stocker des informations supplémentaires pour ne pas les déconnecter de tous les périphériques dans un environnement à plusieurs périphériques, mais vous pouvez le faire en ajoutant un champ liste noire des utilisateurs.
  • Pour forcer la ré-entrée après x temps, conservez la dernière date de connexion dans le jeton d'authentification et vérifiez-la à la demande.
  • Pour forcer la déconnexion de tous les utilisateurs - réinitialisez la clé de hachage du jeton.

Cela nécessite que vous mainteniez une liste noire (état) sur le serveur, en supposant que la table user contienne des informations sur l'utilisateur banni. La liste noire des sessions non valides est une liste d'identifiants d'utilisateurs. Cette liste noire est uniquement vérifiée lors d'une demande d'actualisation de jeton. 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.

Inconvénients:

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

Avantages:

  • Fournit la fonctionnalité souhaitée.
  • L'action d'actualisation du jeton est masquée pour l'utilisateur en mode de fonctionnement normal.
  • Requis uniquement pour effectuer une recherche de 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 en une très petite liste noire.

Avec cette solution, un magasin de données en mémoire tel que reddis n'est pas nécessaire, du moins pas pour les informations utilisateur car vous êtes en train de passer un appel de base de données toutes les 15 minutes environ. Si vous utilisez reddis, le stockage d’une liste de sessions valides/non valides constituerait une solution très simple et rapide. Pas besoin d'un 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 s'ils n'étaient pas valides.

28
Ashtonian

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

L'inconvénient majeur que je vois ici est qu'il les déconnecterait de toutes leurs sessions s'ils utilisaient plusieurs navigateurs ou s'ils disposaient également d'un client mobile.

Cela pourrait aussi être un mécanisme de Nice pour invalider tous les JWT d’un système. Une partie de la vérification peut être effectuée par rapport à un horodatage global de la dernière heure valide iat.

13
Brack Mo

Je suis un peu en retard, 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 l'a rejeté, même s'il n'a pas encore expiré.

8
Matas Kairaitis

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ère une nouvelle chaîne aléatoire, la stocke dans le champ last_key_used et l'ajoute au contenu 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, remplacez simplement ce champ "last_key_used" par une autre valeur aléatoire. Toute vérification ultérieure échouera, obligeant ainsi l'utilisateur à se connecter avec l'utilisateur et à le transmettre à nouveau.

5
NickVarcha

Tard à la fête, MY deux cents sont donnés ci-dessous après quelques recherches. Lors de la déconnexion, assurez-vous que les événements suivants se produisent ...

Effacer le stockage client/session

Mettez à 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 a lieu. Donc, la date de connexion doit toujours être supérieure à la déconnexion (Ou conservez la date de déconnexion nulle si le statut actuel est connexion et pas encore déconnecté)

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 périphériques nécessite une table supplémentaire pour conserver les dates de déconnexion et de déconnexion avec quelques détails supplémentaires, tels que les détails du système d'exploitation ou du client.

2
Shamseer
  1. Donnez un jour d'expiration pour les jetons
  2. Maintenir une liste noire quotidienne.
  3. Mettez les jetons invalidés/logout 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 longues sessions, il devrait exister un mécanisme permettant de prolonger le délai d'expiration des jetons.

2
Ebru Yener

Pourquoi ne pas simplement utiliser la revendication jti (nonce) et la stocker dans une liste en tant que champ d’enregistrement d’utilisateur (dépend de la base de données, mais au moins une liste séparée par des virgules est acceptable)? Pas besoin de rechercher séparément, comme d'autres l'ont déjà fait remarquer, on souhaite de toute façon obtenir l'enregistrement de l'utilisateur. Ainsi, vous pouvez avoir plusieurs jetons valides pour différentes instances de client ("logout partout" peut réinitialiser la liste à vide).

2
davidkomer

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épend de la durée de conservation des jetons et de la fréquence à laquelle les utilisateurs révoquent leurs jetons. Utilisez db uniquement lorsque la table change. Chargez la table en mémoire au démarrage de votre application.

2
Eduardo

Si l'option "Déconnexion de tous les périphériques" est acceptable (dans la plupart des cas, c'est le cas):

  • Ajoutez le champ de version du jeton à la fiche de l'utilisateur.
  • Ajoutez la valeur dans ce champ aux revendications stockées dans le fichier JWT.
  • Incrémenter la version chaque fois que l'utilisateur se déconnecte.
  • Lors de la validation du jeton, comparez sa revendication de version avec la version stockée dans l'enregistrement utilisateur et rejetez-la si ce n'est pas la même chose.

Un voyage de base de données pour obtenir l'enregistrement de l'utilisateur dans la plupart des cas est de toute façon nécessaire afin d'éviter des coûts supplémentaires pour le 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 séparé, nettoyez les anciens enregistrements, etc.

1
user2555515

Si vous souhaitez pouvoir révoquer les 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 (existants) sur une table de type session. L'inconvénient est que vous frapperez 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 minimisant 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 en fonction d'une association déterministe (par exemple, 10 groupes par le premier chiffre de l'ID utilisateur).

Chaque jeton JWT contiendra l'identifiant du 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 indiquera la date du dernier événement de déconnexion d'un utilisateur appartenant à ce groupe. par exemple, { "group1": 1551861473714, "group2": 1551861487293, ... }

Les demandes avec un jeton JWT ayant un horodatage de groupe plus ancien, seront vérifiées pour leur validité (hit de base de données) et si elles sont 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 de base de données).

Alors -

  1. Nous ne validons un jeton JWT à l'aide de la base de données que si le jeton a un horodatage de groupe ancien, tandis que les futures demandes ne seront pas validées tant qu'un membre du groupe de l'utilisateur ne se déconnectera pas.
  2. Nous utilisons des groupes pour limiter le nombre de modifications d'horodatage (disons qu'un utilisateur se connecte et se déconnecte comme s'il n'y avait pas de lendemain - cela 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. Invalider 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.
1
Arik

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

  1. Générez un unique hash, puis enregistrez-le dans redis et votre JWT. Ceci peut être appelé un session
    • Nous allons également stocker le nombre de demandes le JWT a fait - Chaque fois qu'un jwt est envoyé au serveur, nous incrémentons le demandes entier. (c'est optionnel)

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

Lorsqu'un utilisateur essaie de visiter un point d'extrémité 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 étendre à partir de cela et rendre notre JWT encore plus sécurisé, voici comment:

Chaque X demande un (_ jwt _), nous générons une nouvelle session unique, nous la stockons dans notre JWT, et puis liste noire le précédent.

Cela signifie que le JWT change constamment et qu'il cesse d'être obsolète JWT est piraté, volé ou autre chose.

1
James111

Unique par chaîne utilisateur et chaîne globale hachée ensemble

1
Mark Essel