Authentication

JWT

Un JSON Web Token est une chaîne signée, compatible URL, qui transporte des revendications (claims) sur un utilisateur. Compact et autonome, il s'adapte parfaitement à la montée en charge, mais il est facile de commettre des erreurs lors de sa mise en œuvre.

intermediate15 min readUpdated 16 sept. 2026
verify.ts
ts
// verify.ts
import { jwtVerify, type JWTPayload } from "jose";

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

export async function verify(token: string): Promise<JWTPayload> {
  const { payload } = await jwtVerify(token, secret, {
    issuer: "https://auth.example.com",
    audience: "api.example.com",
    algorithms: ["HS256"],
  });
  return payload;
}
Première spec
2015
Encodage
Base64url
Algorithme par défaut
HS256
Option asymétrique
RS256 / ES256
Variante chiffrée
JWE

Pourquoi c'est important

Ce que vous apporte un JWT

Identifiants autonomes

Chaque claim dont un service a besoin voyage à l'intérieur du token ; la validation est donc une opération locale sans aller-retour vers la base de données sur le chemin critique.

Détection d'altération native

La signature couvre le header et le payload ; modifier un seul caractère invalide le token et provoque l'échec immédiat de la vérification.

Idéal pour la rotation

L'association de tokens d'accès à courte durée de vie et de refresh tokens rotatifs limite les dégâts en cas de fuite et permet de mettre en œuvre la révocation.

Le tableau complet

Les trois couches d'un token

Un header nomme l'algorithme, un payload transporte les claims, et une signature scelle les deux pour empêcher toute altération.

Header

Déclarer

Un petit objet JSON nommant l'algorithme de signature et l'ID de la clé, permettant aux vérificateurs de trouver la bonne clé lors d'une rotation.

Payload

Transporter

Un objet JSON contenant des claims enregistrés et personnalisés tels que sub, exp, iss, aud, scope et role.

Signature

Sceller

Un MAC cryptographique ou une signature sur le header et le payload encodés, rendant le token infalsifiable.

HTML5 en un coup d'oeil

L'intérieur du token

Header

alg, typ et kid décrivent comment le token a été signé.

Claims

Des noms enregistrés comme iss, sub, aud et exp, plus vos propres champs.

Signature

Le résultat HMAC ou RSA/ECDSA que la vérification recalcule et compare.

Expiration

Le claim exp limite la fenêtre durant laquelle un token volé est utile.

Rotation

Les refresh tokens sont à usage unique et remplacés à chaque échange.

Vérification

Fixez l'algorithme et vérifiez iss, aud et exp avant de faire confiance au contenu.

Flux

Comment un JWT est vérifié

Chaque requête protégée suit le même chemin. Tout échec de vérification se solde par une erreur 401.

  1. 1

    Extraire le token

    Lire le header Authorization et exiger le schéma Bearer. Rejeter la requête si aucun token n'est présent.

  2. 2

    Découper le token

    Séparer la chaîne aux points pour obtenir le header, le payload et la signature. Un token sans exactement trois parties est malformé.

  3. 3

    Vérifier la signature

    Recalculer la signature sur les deux premières parties avec la clé et l'algorithme fixé. Une différence signifie que le token a été altéré.

  4. 4

    Valider les claims

    Vérifier exp et nbf pour le timing, iss pour l'émetteur attendu, et aud pour l'audience prévue. Un token de staging ne doit pas être accepté en production.

  5. 5

    Attacher le principal

    En cas de succès, placer le sujet (subject) et le scope sur la requête pour que les handlers suivants puissent autoriser sans reparser le token.

  6. 6

    Rejeter avec une 401

    Si une étape échoue, retourner une 401 avec une erreur générique. N'expliquez pas quelle vérification a échoué à un appelant non fiable.

Un bref aperçu

Comment le JWT est devenu le standard

  1. 2011

    Apparition du draft JWT

    Le groupe de travail OAuth propose un format de token compact pour transporter des claims entre services.

    11
  2. 2015

    La RFC 7519 standardise le JWT

    Le JWT est publié aux côtés de JWS, JWE, JWK et JWA, donnant au format une spécification stable.

    15
  3. 2015

    Adoption par OpenID Connect

    Les ID tokens sont définis comme des JWT, faisant de ce format le standard pour les fournisseurs d'identité.

    15
  4. 2015

    Les attaques classiques surgissent

    Des chercheurs documentent "alg: none" et la confusion HS/RS, forçant les vérificateurs à fixer les algorithmes.

    15
  5. 2020

    La rotation devient la norme

    Les tokens d'accès courts et les refresh tokens rotatifs remplacent les tokens à longue durée de vie dans les pratiques courantes.

    20

Le guide complet

JWT: Tout ce que vous devez savoir

Ce qu’est réellement un JWT

Un JSON Web Token est une chaîne compacte, compatible avec les URL, qui encode un ensemble de revendications (claims) accompagnées d’une signature. Il est défini par la RFC 7519 et s’appuie sur deux spécifications complémentaires : JSON Web Signature (JWS) pour la signature et JSON Web Encryption (JWE) pour la confidentialité. La quasi-totalité des JWT que vous rencontrerez en pratique sont des JWS.

Le jeton est composé de trois parties encodées en Base64url et séparées par des points : le header, le payload et la signature. Il est autonome (self-contained), ce qui signifie que toutes les informations dont un serveur a besoin pour prendre une décision sont transmises avec la requête. Sa validation ne nécessite aucune requête en base de données, aucun store de session partagé, ni aucun appel à l’émetteur.

Anatomy of a JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzQyIiwic2NvcGUiOiJyZWFkOnBvc3RzIiwiZXhwIjoxNzYwMDAwMDAwfQ.3vJ1o0Q8m4nQ7bKc9xP2sT6wYqL0dRfH8uZgWmA1eJk
headerthe algorithm and token type, Base64url
payloadthe claims, also Base64url and readable by anyone
signatureHMAC over the first two parts, proving they were not changed

Il est important d’être précis sur ce que cela prouve. Un JWT démontre que l’entité qui l’a émis a signé exactement ce payload. Cela ne prouve pas que le jeton vous était destiné, à moins que vous ne vérifiiez l’audience. Cela ne prouve pas que l’utilisateur est toujours autorisé à agir, à moins que vous ne vérifiiez les scopes et les rôles. Enfin, cela ne cache rien, à moins d’utiliser JWE. Chaque propriété de sécurité importante doit être vérifiée explicitement.

Les trois parties décodées

Le header est un petit objet JSON indiquant l’algorithme de signature et le type de token. Le champ alg est le plus critique. typ est presque toujours JWT, et kid identifie la clé utilisée afin que les vérificateurs puissent trouver la bonne lors d’une rotation de clés.

{ "alg": "HS256", "typ": "JWT", "kid": "2026-09" }

Le payload est un objet JSON composé de claims. Les claims enregistrés possèdent des noms standards définis par la spécification : iss pour l’émetteur (issuer), sub pour le sujet (subject), aud pour l’audience, exp pour l’expiration, nbf pour la date de début de validité (not before), iat pour la date d’émission (issued at), et jti pour un identifiant de token unique. Tout le reste correspond à des claims personnalisés que vous définissez vous-même.

{
  "iss": "https://auth.example.com",
  "sub": "user_42",
  "aud": "api.example.com",
  "exp": 1760000000,
  "iat": 1759999100,
  "scope": "read:posts write:posts",
  "role": "editor"
}

La signature est calculée à partir du Base64url du header et du payload. Si vous modifiez un seul caractère dans l’un ou l’autre, la signature recalculée ne correspondra plus. C’est cette propriété qui rend un JWT sûr à confier à un client non fiable, et c’est le seul rempart entre un claim légitime et une falsification.

Signé, mais pas chiffré

L’erreur la plus courante concernant les JWT est de penser qu’ils sont secrets. Ce n’est pas le cas. Le payload est en Base64url, ce qui est un encodage et non un chiffrement. Toute personne détenant le token peut décoder chaque claim avec une seule ligne de code.

const [, payload] = token.split(".");
console.log(JSON.parse(atob(payload)));
// { sub: "user_42", role: "editor", scope: "read:posts" }

Cela a deux conséquences. Premièrement, ne placez jamais de secrets, de mots de passe ou de données personnelles dans un JWT que vous ne seriez pas à l’aise de montrer au client. Deuxièmement, considérez le token lui-même comme un identifiant : sa possession suffit pour agir en tant que sujet, c’est pourquoi le stockage et le transport sont si importants plus loin dans ce guide.

Si vous avez réellement besoin de masquer le payload pour le client, utilisez JSON Web Encryption et une bibliothèque qui le supporte. Pour la vaste majorité des systèmes, un JWS signé via TLS est la solution appropriée, et l’ajout du chiffrement ne ferait qu’ajouter une complexité inutile.

Algorithmes de signature : HS256 vs RS256

Deux familles d’algorithmes dominent les déploiements réels.

HS256 est un HMAC avec SHA-256. Le même secret sert à signer et à vérifier. C’est rapide, simple et idéal lorsqu’un seul service émet et vérifie ses propres tokens.

import { SignJWT, jwtVerify } from "jose";

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

const token = await new SignJWT({ role: "editor" })
  .setProtectedHeader({ alg: "HS256" })
  .setSubject("user_42")
  .setExpirationTime("15m")
  .sign(secret);

await jwtVerify(token, secret, { algorithms: ["HS256"] });

RS256 est un RSA avec SHA-256, et ES256 est un ECDSA sur une courbe première. L’émetteur détient une clé privée pour signer ; tous les autres détiennent la clé publique pour vérifier. Cette asymétrie est la raison pour laquelle les grands systèmes le préfèrent : un serveur de ressources peut vérifier les tokens sans être capable de les générer. ES256 offre la même garantie avec des clés et des signatures beaucoup plus petites.

import { importPKCS8, importSPKI, SignJWT, jwtVerify } from "jose";

const privateKey = await importPKCS8(process.env.JWT_PRIVATE_KEY!, "RS256");
const publicKey = await importSPKI(process.env.JWT_PUBLIC_KEY!, "RS256");

const token = await new SignJWT({ role: "editor" })
  .setProtectedHeader({ alg: "RS256", kid: "2026-09" })
  .setSubject("user_42")
  .setExpirationTime("15m")
  .sign(privateKey);

await jwtVerify(token, publicKey, {
  algorithms: ["RS256"],
  issuer: "https://auth.example.com",
  audience: "api.example.com",
});

La règle la plus importante, au-delà du choix de l’algorithme : le vérificateur doit fixer l’algorithme attendu. Ne laissez jamais le header du token décider de la manière dont il est vérifié.

Claims : enregistrées et personnalisées

Les claims enregistrées sont réservées par la spécification et sont reconnues par toutes les bibliothèques sérieuses.

  • iss — l’émetteur du token. Vérifiez-le pour rejeter les tokens provenant d’un autre environnement.
  • sub — le sujet du token, généralement l’identifiant de l’utilisateur.
  • aud — le destinataire du token. Un token généré pour votre API publique ne devrait pas être accepté par votre API d’administration.
  • exp — la date d’expiration du token, sous forme de timestamp Unix. Définissez-la systématiquement.
  • nbf — “not before” (pas avant). Rarement nécessaire, mais utile pour les déploiements progressifs.
  • iat — la date d’émission. Pratique pour les vérifications d’âge maximum et le débogage.
  • jti — un identifiant unique pour ce token, utilisé pour la détection de rejeu et les listes de blocage.

Les claims personnalisées transportent des données applicatives : scope, role, tenant_id, email. Gardez-les légères et non sensibles. Un token voyage à chaque requête, donc un payload trop volumineux représente une taxe permanente sur la bande passante et la latence.

Il est très tentant d’inclure tout le profil de l’utilisateur dans le token pour éviter une lecture en base de données. Résistez. Les claims deviennent obsolètes dès qu’un rôle change, et vous ne pouvez pas révoquer un token qu’un client détient déjà. N’incluez que ce dont le vérificateur a réellement besoin, et récupérez le reste via une requête.

Création et vérification des tokens

Dans Node, jose est le choix moderne. Il est basé sur les promesses, fonctionne dans tous les runtimes, y compris Cloudflare Workers et Deno, et expose une API concise et rigoureuse. jsonwebtoken est la bibliothèque plus ancienne, basée sur les callbacks, et reste courante dans le code existant.

import { SignJWT } from "jose";

export async function issueAccessToken(userId: string, scope: string) {
  return new SignJWT({ scope })
    .setProtectedHeader({ alg: "HS256", typ: "JWT" })
    .setSubject(userId)
    .setIssuer("https://auth.example.com")
    .setAudience("api.example.com")
    .setIssuedAt()
    .setExpirationTime("15m")
    .setJti(crypto.randomUUID())
    .sign(new TextEncoder().encode(process.env.JWT_SECRET));
}

C’est lors de la vérification que se joue la sécurité. Un vérificateur doit contrôler la signature, fixer l’algorithme et valider exp, iss et aud. Une bibliothèque peut volontiers décoder un token sans le vérifier, et ce payload décodé est alors une entrée contrôlée par l’attaquant.

import { jwtVerify, type JWTPayload } from "jose";

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

export async function verifyAccessToken(token: string): Promise<JWTPayload> {
  const { payload } = await jwtVerify(token, secret, {
    algorithms: ["HS256"],
    issuer: "https://auth.example.com",
    audience: "api.example.com",
  });
  return payload;
}

Remarquez que jwtVerify impose exp et nbf automatiquement, mais ne vérifie iss et aud que si vous les passez en argument. Les omettre constitue une véritable vulnérabilité : un token généré pour un service de staging serait accepté en production, tout comme un token généré pour une application différente.

Jetons d’accès et de rafraîchissement (Access and refresh tokens)

Un jeton unique à longue durée de vie est pratique, mais dangereux. La solution standard consiste à utiliser un couple de jetons avec des durées de vie et des audiences différentes.

  • Le access token est éphémère, généralement de 5 à 15 minutes. Il est envoyé à chaque appel API et est le seul jeton que le serveur de ressources voit.
  • Le refresh token a une durée de vie longue, allant de quelques jours à plusieurs mois. Il est envoyé uniquement au serveur d’autorisation, en échange d’un nouvel access token.

Cette séparation limite les risques. Si un access token fuite, il devient inutile en quelques minutes. Le refresh token, beaucoup plus sensible, n’est jamais transmis aux serveurs de ressources et peut être renouvelé (rotated) ou révoqué, ce qui permet un contrôle réel de la sécurité.

Rotation des refresh tokens

La rotation signifie que chaque demande de rafraîchissement génère un nouveau refresh token et invalide l’ancien. Si un attaquant vole un refresh token et l’utilise, le client légitime présentera plus tard un token déjà utilisé ; le serveur pourra alors détecter cette réutilisation et révoquer l’ensemble de la famille de tokens.

import { randomUUID } from "node:crypto";

export async function rotateRefreshToken(presented: string) {
  const stored = await db.refreshToken.findUnique({ where: { token: presented } });

  if (!stored || stored.revokedAt || stored.expiresAt < new Date()) {
    if (stored) await revokeFamily(stored.familyId);
    throw new Error("invalid_refresh_token");
  }

  await db.refreshToken.update({
    where: { id: stored.id },
    data: { revokedAt: new Date(), replacedBy: randomUUID() },
  });

  return issueTokenPair(stored.userId, stored.familyId);
}

Stockez les refresh tokens hachés, exactement comme vous le feriez pour des mots de passe. Une fuite de base de données ne doit pas fournir d’identifiants exploitables à un attaquant, car un refresh token n’est rien d’autre qu’un mot de passe permettant de contourner le formulaire de connexion.

Où stocker les tokens dans un navigateur

Il n’existe pas d’endroit parfait, seulement des compromis entre le cross-site scripting (XSS) et le cross-site request forgery (CSRF).

  • localStorage est lisible par n’importe quel script sur la page. Un seul bug XSS et l’attaquant exfiltre tous les tokens. C’est l’erreur que l’on retrouve systématiquement dans les rapports de violation de données.
  • En mémoire, via une variable dans un module, les données sont protégées contre le XSS persistant mais sont perdues lors du rafraîchissement de la page. C’est pourquoi cette méthode est généralement couplée à un refresh token dans un cookie httpOnly.
  • Un cookie httpOnly, Secure et SameSite est illisible par JavaScript, ce qui neutralise le vol de tokens via XSS. Cela réintroduit cependant le risque de CSRF, que SameSite=Lax ou Strict associé à un token CSRF permettent d’atténuer.

Pour une application navigateur, le choix pragmatique par défaut est un access token à courte durée de vie conservé en mémoire et un refresh token rotatif dans un cookie httpOnly limité au point de terminaison (endpoint) de rafraîchissement. Les clients natifs et server-side n’ont pas cette contrainte et peuvent conserver les tokens dans un stockage sécurisé ou simplement en mémoire.

Le compromis de l’absence d’état et la révocation

L’argument principal des JWT est l’absence d’état (statelessness). N’importe quel serveur peut vérifier un jeton sans état partagé, ce qui permet une mise à l’échelle fluide entre les régions et rend le scaling horizontal trivial. Le revers de la médaille est que la révocation est réellement complexe. Un jeton signé reste valide jusqu’à son expiration, que vous ayez supprimé l’utilisateur, modifié son rôle ou qu’il se soit déconnecté entre-temps. Il n’existe aucun registre central à supprimer.

Vous pouvez toutefois reprendre une partie du contrôle :

  • Gardez des jetons d’accès avec une durée de vie courte afin que la fenêtre de révocation soit de quelques minutes plutôt que de plusieurs jours.
  • Maintenez une deny-list de valeurs jti pour les cas rares de déconnexion immédiate, vérifiée à chaque requête. Cela réintroduit un état, veillez donc à ce qu’elle reste légère et que les entrées expirent.
  • Ajoutez un claim token_version par utilisateur et rejetez les jetons dont la version est obsolète. Le changement d’un mot de passe ou d’un rôle incrémente alors cette valeur.

Voici le résumé honnête : les JWT sacrifient la facilité de révocation au profit de la facilité de mise à l’échelle. Si vous avez besoin d’une révocation instantanée partout, les cookies de session appuyés par un store pourraient être plus appropriés, comme détaillé dans Session Auth.

Confusion d’algorithme et alg none

Deux attaques sont suffisamment anciennes pour figurer dans tous les manuels, tout en continuant de trouver des victimes.

La première est alg: none. Un attaquant modifie l’en-tête en {"alg":"none"} et supprime la signature. Un vérificateur naïf qui fait confiance à l’en-tête accepte alors le jeton falsifié. La seconde est la confusion HS/RS. Un service qui attend des jetons RS256 est trompé pour accepter un jeton HS256 signé avec la clé publique RSA, laquelle est, par définition, publique.

Ces deux failles ont la même solution : c’est le vérificateur qui décide de l’algorithme, et non le jeton.

// Good: the verifier decides.
await jwtVerify(token, secret, { algorithms: ["HS256"] });

// Bad: the token decides.
const { header } = decodeProtectedHeader(token);
await jwtVerify(token, secret, { algorithms: [header.alg] });

Rejetez systématiquement alg: none, ne dérivez jamais la clé à partir d’une source non fiable, et traitez l’en-tête comme une donnée, et non comme une instruction.

Scopes et autorisation

L’authentification répond à la question « qui », et les scopes répondent à « quoi ». Un claim scope est une liste de permissions séparées par des espaces, et un middleware la vérifie avant l’exécution d’un handler.

export function requireScope(required: string) {
  return (req, res, next) => {
    const granted = String(req.user.scope ?? "").split(" ");
    if (!granted.includes(required)) {
      return res.status(403).json({ error: "insufficient_scope" });
    }
    next();
  };
}

Gardez vos scopes larges et stables, et imposez-les côté serveur. Un jeton sans le scope approprié doit renvoyer une erreur 403 et non 401 : l’appelant est authentifié, mais n’est tout simplement pas autorisé. Pour des modèles d’accès plus riches basés sur des rôles et des attributs, consultez la section RBAC.

JWT vs tokens opaques

Un JWT est un bearer token qui contient sa propre validation. Un token opaque est une chaîne de caractères aléatoire sans signification intrinsèque ; le serveur doit donc effectuer une recherche pour obtenir des informations à son sujet.

Les tokens opaques l’emportent sur la révocation et la confidentialité. Vous pouvez supprimer la session instantanément, et le token ne révèle rien en cas de fuite. En contrepartie, ils nécessitent un aller-retour vers une base de données ou un cache à chaque requête. Les JWT l’emportent sur la scalabilité et l’indépendance. Les services effectuent la vérification localement et n’ont pas besoin de stockage partagé, au prix d’une fenêtre de délai pour la révocation.

De nombreux systèmes en production utilisent les deux : un access token JWT pour la rapidité et un refresh token opaque pour le contrôle. Cet hybride est ce que la plupart des fournisseurs d’identité proposent aujourd’hui, et c’est un excellent choix par défaut lorsque vous hésitez.

Rotation des clés avec kid

Les clés de signature ne doivent pas être permanentes. La rotation permet de limiter les dégâts en cas de compromission d’une clé, et c’est le champ d’en-tête kid qui rend cette rotation invisible pour les clients : le vérificateur lit kid, sélectionne la clé correspondante et vérifie la signature.

import { SignJWT, jwtVerify, createLocalJWKSet } from "jose";

const jwks = createLocalJWKSet({
  keys: [{ kty: "oct", kid: "2026-09", k: process.env.JWT_SECRET }],
});

// The verifier resolves the key from the header's kid.
await jwtVerify(token, jwks, { algorithms: ["HS256"] });

Lors d’une rotation, publiez la nouvelle clé aux côtés de l’ancienne, signez les nouveaux jetons avec le nouveau kid, et continuez à vérifier la clé précédente jusqu’à ce que tous les jetons signés avec celle-ci aient expiré. Si vous la supprimez trop tôt, vous déconnecterez tous les utilisateurs actifs d’un seul coup. Pour les clés asymétriques, publiez un document JWKS à une URL conventionnelle (well-known) et laissez les serveurs de ressources le mettre en cache.

Durée de vie des tokens en pratique

L’expiration est un curseur entre sécurité et commodité. Il n’y a pas de réponse universelle, mais la structure d’une configuration par défaut raisonnable reste cohérente.

  • Access tokens : 5 à 15 minutes. Assez court pour qu’une fuite devienne rapidement inutile, assez long pour ne pas avoir à rafraîchir à chaque requête.
  • Refresh tokens : 7 à 30 jours, avec rotation et fenêtre glissante. Assez long pour maintenir les utilisateurs connectés, assez court pour qu’un token abandonné finisse par expirer.
  • Limite de session absolue : 30 à 90 jours. Un âge maximum après lequel l’utilisateur doit s’authentifier à nouveau, peu importe la fréquence de ses rafraîchissements.

Si vos utilisateurs se plaignent d’être déconnectés, la solution est un silent refresh plus fluide, et non un access token plus long. Un access token de 24 heures est une faille de révocation qui ne demande qu’à être exploitée.

Déboguer un token sans lui faire confiance

Lorsqu’une requête échoue avec une erreur 401, vous souhaitez examiner les claims du token sans pour autant affaiblir la vérification. Le décodage est sans risque tant que vous traitez le résultat comme une donnée non fiable.

import { decodeJwt, decodeProtectedHeader } from "jose";

const header = decodeProtectedHeader(token);
const claims = decodeJwt(token);

console.log({ alg: header.alg, kid: header.kid });
console.log({
  sub: claims.sub,
  iss: claims.iss,
  aud: claims.aud,
  exp: new Date((claims.exp ?? 0) * 1000).toISOString(),
  expired: (claims.exp ?? 0) * 1000 < Date.now(),
});

Les deux erreurs les plus fréquentes sont un mismatch de aud après le renommage d’un service et un mismatch de iss lors d’un changement d’environnement. Il s’agit dans les deux cas de problèmes de configuration, et ils restent invisibles tant que vous n’affichez pas les claims.

Tester les tokens

Vous devriez être capable de tester une route authentifiée sans avoir à déployer un fournisseur d’identité. Puisqu’un JWT n’est qu’une chaîne signée, un helper de test qui signe un token avec le même secret de test est suffisant.

import { SignJWT } from "jose";
import request from "supertest";
import app from "../app.js";

const secret = new TextEncoder().encode("test-secret");

async function tokenFor(scope = "read:posts") {
  return new SignJWT({ scope })
    .setProtectedHeader({ alg: "HS256" })
    .setSubject("user_1")
    .setIssuer("https://auth.example.com")
    .setAudience("api.example.com")
    .setExpirationTime("5m")
    .sign(secret);
}

test("rejects a request without a token", async () => {
  await request(app).get("/posts").expect(401);
});

test("accepts a request with a valid token", async () => {
  const token = await tokenFor();
  await request(app)
    .get("/posts")
    .set("authorization", `Bearer ${token}`)
    .expect(200);
});

Testez également les cas négatifs : un token expiré, un token avec une audience incorrecte et un token signé avec une clé différente. Ce sont ces vérifications qui vous protègent, elles méritent donc autant de couverture de tests que le cas nominal.

Bonnes pratiques

  • Configurez toujours exp ; limitez la durée de validité des jetons d’accès à 15 minutes ou moins.
  • Fixez l’algorithme sur le vérificateur et rejetez alg: none.
  • Validez iss, aud, exp et nbf ; ne faites jamais confiance à une revendication (claim) que vous n’avez pas vérifiée.
  • Ne stockez jamais les secrets et les clés privées dans le contrôle de version et chargez-les depuis un gestionnaire de secrets.
  • Stockez les jetons de rafraîchissement (refresh tokens) sous forme de hash et effectuez une rotation à chaque utilisation.
  • Gardez les revendications courtes et non sensibles ; le payload est lisible.
  • Dans les navigateurs, privilégiez les cookies httpOnly ou la mémoire au localStorage.
  • Prévoyez la révocation avec des durées de vie courtes, une deny-list jti ou une version de jeton.
  • Utilisez jose pour le nouveau code ; il est basé sur les promesses et portable entre les différents runtimes.

Erreurs courantes

  • Supposer que le payload est chiffré simplement parce qu’il ressemble à du charabia.
  • Lire les claims avec decode et les considérer comme vérifiés.
  • Laisser le header alg du token choisir l’algorithme de vérification.
  • Accepter un token sans vérifier aud, permettant ainsi l’utilisation de tokens de staging en production.
  • Utiliser un secret faible ou partagé, ou le commiter dans le repository.
  • Fixer l’expiration à 30 jours parce que le rafraîchissement est fastidieux.
  • Stocker les tokens dans le localStorage et considérer que le problème est réglé.
  • Inclure un rôle dans le token et ne jamais l’invalider lorsque le rôle change.
  • Traiter une erreur 401 et une erreur 403 comme étant la même erreur.

Et après ?

Les JWT ne sont qu’un outil parmi d’autres dans l’arsenal de la gestion d’identité. Le guide sur OAuth 2.0 explique comment les jetons sont réellement obtenus via l’autorisation déléguée, Session Auth traite de l’alternative basée sur les cookies lorsque vous avez besoin d’une révocation instantanée, et API Keys détaille les identifiants à longue durée de vie pour les clients machines. Si vous souhaitez voir ce code de vérification intégré dans un serveur réel, consultez la section Node.js.

En pratique

Émettre, vérifier, router, inspecter

Les quatre opérations que vous écrirez en premier, en utilisant jose.

tokens.ts
import { SignJWT } from "jose";

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

export async function issueAccessToken(userId: string, scope: string) {
  return new SignJWT({ scope })
    .setProtectedHeader({ alg: "HS256", typ: "JWT" })
    .setSubject(userId)
    .setIssuer("https://auth.example.com")
    .setAudience("api.example.com")
    .setIssuedAt()
    .setExpirationTime("15m")
    .setJti(crypto.randomUUID())
    .sign(secret);
}

HS256 vs RS256

Le symétrique est plus simple quand un seul service émet et vérifie. L'asymétrique vaut la configuration dès que la vérification est répartie sur plusieurs services.

HS256
import { SignJWT, jwtVerify } from "jose";

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

const token = await new SignJWT({ role: "editor" })
  .setProtectedHeader({ alg: "HS256" })
  .setSubject("user_42")
  .setExpirationTime("15m")
  .sign(secret);

await jwtVerify(token, secret, { algorithms: ["HS256"] });
RS256
import { importPKCS8, importSPKI, SignJWT, jwtVerify } from "jose";

const privateKey = await importPKCS8(process.env.JWT_PRIVATE_KEY!, "RS256");
const publicKey = await importSPKI(process.env.JWT_PUBLIC_KEY!, "RS256");

const token = await new SignJWT({ role: "editor" })
  .setProtectedHeader({ alg: "RS256", kid: "2026-09" })
  .setSubject("user_42")
  .setExpirationTime("15m")
  .sign(privateKey);

await jwtVerify(token, publicKey, { algorithms: ["RS256"] });

Cookie httpOnly vs localStorage

JavaScript ne peut pas lire un cookie httpOnly, ce qui supprime le chemin le plus courant entre un bug XSS et une prise de contrôle complète du compte.

Préférer
Set-Cookie: access_token=eyJhbGciOi...;
  HttpOnly;
  Secure;
  SameSite=Lax;
  Path=/;
  Max-Age=900
Éviter
localStorage.setItem("access_token", token);

// Any injected script can now read the token and
// exfiltrate it. Persistent XSS becomes account takeover.

Compromis

L'auth stateless en vaut-elle la peine ?

Les JWT sacrifient la facilité de révocation pour la facilité de mise à l'échelle. Décidez de ce dont votre produit a réellement besoin.

Strengths

  • Pas de store de session partagé

    N'importe quelle instance peut vérifier un token avec une clé qu'elle possède déjà, simplifiant ainsi le scaling horizontal et les déploiements multi-régions.

  • Compact et portable

    Un seul header transporte l'identité, les scopes et l'expiration entre services, langages et runtimes sans couche de traduction.

  • Idéal pour le service-to-service

    Des services indépendants peuvent vérifier les tokens localement, supprimant ainsi une dépendance synchrone envers le serveur d'autorisation.

Trade-offs

  • La révocation est le point difficile

    Un token signé est valide jusqu'à son expiration. Déconnecter un utilisateur ou révoquer un rôle ne peut pas atteindre un token déjà entre les mains d'un client.

  • Le payload est public

    Le Base64url n'est pas du chiffrement. Tout ce que vous mettez dans les claims est lisible par le détenteur et par quiconque l'intercepte.

  • De petites erreurs ont des conséquences graves

    Faire confiance à l'algorithme du header, ignorer la vérification de l'audience ou stocker les tokens dans localStorage transforme chaque commodité en faille de sécurité.

FAQ

Foire aux questions

Keep learning

Related topics from the roadmap.

$ commencer à apprendre

Prêt à apprendre JWT (JSON Web Tokens) ?

Notre tutoriel interactif vous guide à travers JWT (JSON Web Tokens) pas à pas — avec des quiz et du vrai code que vous pouvez exécuter dans le navigateur.