Use Cases (Mis à jour: 02/06/2026)

Authentification JWT avec Claude Code : claims, cookies, rotation et clés

Implémentez JWT avec Claude Code : claims, cookies, rotation des refresh tokens, révocation, clés et prompts sûrs.

Authentification JWT avec Claude Code : claims, cookies, rotation et clés

L’authentification JWT paraît simple dans une démo. On signe un payload, on renvoie un token, puis on le vérifie dans un middleware. En production, ce raccourci ne suffit pas. Une audience non vérifiée, un refresh token trop long, un token stocké dans localStorage ou une rotation de clés improvisée peuvent devenir un vrai problème de sécurité.

Ce guide reprend les bases de JWT pour les débutants et les transforme en brief exploitable par Claude Code. Nous couvrons la conception des claims, la différence entre signature et chiffrement, le placement Cookie/session, la rotation des refresh tokens, la révocation, la rotation de clés, les erreurs fréquentes et les prompts sûrs. Pour le flux de login complet, lisez aussi l’implémentation de l’authentification, la gestion des cookies et le RBAC.

Gardez les sources primaires sous la main : RFC 7519 pour JWT, RFC 8725 pour les bonnes pratiques, RFC 9700 pour les refresh tokens et la détection de rejeu, OWASP JWT Cheat Sheet, MDN Set-Cookie, jose et Claude Code settings.

Bases de JWT pour débutants

Un JWT contient trois parties séparées par des points : header.payload.signature. Le header indique le type et l’algorithme. Le payload contient des claims, c’est-à-dire des affirmations sur le token. La signature sert à détecter une modification. Les claims courants sont sub pour l’utilisateur, iss pour l’émetteur, aud pour l’audience, exp pour l’expiration et jti pour l’identifiant du token.

Le point le plus important est qu’un JWT courant est signé, pas chiffré. La signature ne cache pas le payload. Toute personne qui obtient le token peut le décoder. N’y mettez donc pas de clé API, adresse, données de paiement, note interne ou information personnelle inutile. Si une donnée doit rester secrète, gardez-la côté serveur et ne mettez qu’un identifiant minimal dans le token.

Donnez d’abord ce cadre à Claude Code.

Implémente l'authentification JWT.
Règles:
- Le payload JWT est signé, pas chiffré. Ne pas y mettre de secret.
- Access token: 15 minutes maximum.
- Refresh token: 7 jours maximum.
- Valider iss, aud, sub, exp, iat et jti.
- Refuser alg none et tout algorithme inattendu.
- Implémenter refresh-token rotation et détection de réutilisation.
- Vérifier Cookie, CSRF, XSS, révocation et rotation de clés.

Concevoir des claims minimaux

Un JWT ne doit pas devenir un cache du profil utilisateur. L’access token doit contenir seulement les informations nécessaires à l’entrée de l’API : identifiant stable, session id, tenant id, rôle large et token id. Les données qui changent souvent, comme le plan, la suspension, les permissions fines ou les quotas, doivent être relues côté serveur.

ClaimUtilitéAttention
subIdentifiant utilisateurPréférer un ID interne à un email
issÉmetteurLe fixer au serveur d’authentification
audAudienceRefuser les tokens d’une autre API
expExpirationGarder l’access token court
jtiID du tokenRévocation et audit
sidID de sessionLogout par appareil et famille de tokens
roleRôle largeRevalider les droits précis côté serveur

L’erreur classique consiste à mettre plan: "pro" oudisabled: false dans le token. Si le plan change ou si le compte est suspendu, le token peut conserver l’ancien état jusqu’à expiration. L’authentification répond à “qui est-ce ?”. L’autorisation répond à “peut-il faire cette action maintenant ?”. Séparez ces deux décisions.

Exemple TypeScript exécutable avec jose

Le demo suivant signe et vérifie des access tokens, stocke uniquement des hashes de refresh tokens, effectue la rotation et détecte la réutilisation. En production, remplacez le Map par Redis ou une base de données, ajoutez rate limit, logs d’audit, HTTPS et protection CSRF.

mkdir jwt-lab
cd jwt-lab
npm init -y
npm install jose
npm install -D tsx typescript @types/node
// auth-demo.ts
import { createHash, createSecretKey, randomUUID } from "node:crypto";
import { SignJWT, jwtVerify } from "jose";

const ISSUER = "https://auth.example.com";
const AUDIENCE = "claudecodelab-api";
const ACCESS_TTL = "15m";
const REFRESH_TTL_SECONDS = 60 * 60 * 24 * 7;

const accessKey = createSecretKey(
  Buffer.from(
    process.env.JWT_ACCESS_SECRET ??
      "dev-only-secret-change-me-32-bytes-minimum"
  )
);

const refreshKey = createSecretKey(
  Buffer.from(
    process.env.JWT_REFRESH_SECRET ??
      "dev-only-refresh-secret-change-me-32-bytes"
  )
);

type Role = "admin" | "user" | "viewer";
type User = { id: string; role: Role; tenantId: string };
type VerifiedAccess = {
  userId: string;
  role: Role;
  tenantId: string;
  sessionId: string;
  tokenId: string;
};

type RefreshRecord = {
  userId: string;
  sessionId: string;
  tokenHash: string;
  expiresAt: number;
  revokedAt?: number;
};

const refreshStore = new Map<string, RefreshRecord>();
const revokedAccessTokenIds = new Set<string>();

function sha256(value: string) {
  return createHash("sha256").update(value).digest("hex");
}

function assertRole(value: unknown): asserts value is Role {
  if (!["admin", "user", "viewer"].includes(String(value))) {
    throw new Error("invalid role claim");
  }
}

async function signAccessToken(user: User, sessionId: string) {
  const tokenId = randomUUID();

  return new SignJWT({ role: user.role, tid: user.tenantId, sid: sessionId })
    .setProtectedHeader({ alg: "HS256", typ: "JWT" })
    .setIssuer(ISSUER)
    .setAudience(AUDIENCE)
    .setSubject(user.id)
    .setIssuedAt()
    .setExpirationTime(ACCESS_TTL)
    .setJti(tokenId)
    .sign(accessKey);
}

async function verifyAccessToken(token: string): Promise<VerifiedAccess> {
  const { payload } = await jwtVerify(token, accessKey, {
    issuer: ISSUER,
    audience: AUDIENCE,
    algorithms: ["HS256"],
  });

  assertRole(payload.role);

  if (
    typeof payload.sub !== "string" ||
    typeof payload.tid !== "string" ||
    typeof payload.sid !== "string" ||
    typeof payload.jti !== "string"
  ) {
    throw new Error("missing required claim");
  }

  if (revokedAccessTokenIds.has(payload.jti)) {
    throw new Error("access token revoked");
  }

  return {
    userId: payload.sub,
    role: payload.role,
    tenantId: payload.tid,
    sessionId: payload.sid,
    tokenId: payload.jti,
  };
}

async function signRefreshToken(user: User, sessionId: string) {
  const tokenId = randomUUID();
  const token = await new SignJWT({ sid: sessionId, kind: "refresh" })
    .setProtectedHeader({ alg: "HS256", typ: "JWT" })
    .setIssuer(ISSUER)
    .setAudience("claudecodelab-refresh")
    .setSubject(user.id)
    .setIssuedAt()
    .setExpirationTime("7d")
    .setJti(tokenId)
    .sign(refreshKey);

  refreshStore.set(tokenId, {
    userId: user.id,
    sessionId,
    tokenHash: sha256(token),
    expiresAt: Date.now() + REFRESH_TTL_SECONDS * 1000,
  });

  return token;
}

async function rotateRefreshToken(refreshToken: string, user: User) {
  const { payload } = await jwtVerify(refreshToken, refreshKey, {
    issuer: ISSUER,
    audience: "claudecodelab-refresh",
    algorithms: ["HS256"],
  });

  if (
    typeof payload.jti !== "string" ||
    typeof payload.sid !== "string" ||
    typeof payload.sub !== "string"
  ) {
    throw new Error("invalid refresh token claims");
  }

  const record = refreshStore.get(payload.jti);
  const presentedHash = sha256(refreshToken);

  if (!record || record.revokedAt || record.tokenHash !== presentedHash) {
    for (const item of refreshStore.values()) {
      if (item.sessionId === payload.sid) item.revokedAt = Date.now();
    }
    throw new Error("refresh token reuse detected");
  }

  if (record.expiresAt < Date.now()) {
    throw new Error("refresh token expired");
  }

  record.revokedAt = Date.now();

  return {
    accessToken: await signAccessToken(user, payload.sid),
    refreshToken: await signRefreshToken(user, payload.sid),
  };
}

async function main() {
  const user: User = {
    id: "user_123",
    role: "admin",
    tenantId: "tenant_a",
  };
  const sessionId = randomUUID();
  const accessToken = await signAccessToken(user, sessionId);
  const refreshToken = await signRefreshToken(user, sessionId);
  const verified = await verifyAccessToken(accessToken);
  const rotated = await rotateRefreshToken(refreshToken, user);

  console.log({ verified, rotatedRefreshLength: rotated.refreshToken.length });
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});
npx tsx auth-demo.ts

Le détail central est que le refresh token brut n’est pas stocké. Seul son hash l’est. Quand il est utilisé, l’ancien enregistrement est révoqué et une nouvelle paire est émise. Si l’ancien token revient, toute la famille liée au même sid est révoquée.

Cookies, session et stockage

Pour une application web, placez le refresh token dans une Cookie HttpOnly, Secure et SameSite. HttpOnly empêche JavaScript de lire directement la valeur, ce qui réduit le vol du token en cas de XSS. Mais la Cookie est envoyée automatiquement, donc les routes de refresh, logout et modification d’état ont besoin de protections CSRF.

const refreshCookieOptions = {
  httpOnly: true,
  secure: true,
  sameSite: "lax" as const,
  path: "/api/auth/refresh",
  maxAge: 60 * 60 * 24 * 7,
};

const clearRefreshCookieOptions = {
  ...refreshCookieOptions,
  maxAge: 0,
};

L’access token peut rester en mémoire dans une SPA, avec la contrainte qu’un rechargement impose un refresh. Dans une architecture BFF ou avec des Route Handlers Next.js, le serveur peut appeler l’API sans exposer l’access token au navigateur. localStorage est pratique, mais un XSS peut lire les bearer tokens. Évitez-y les tokens longs.

Révocation et rotation de clés

Un JWT reste valable jusqu’à exp si le serveur n’ajoute aucun contrôle. Combinez donc access tokens courts, liste de jti révoqués pour événements critiques, invalidation par sid et refresh-token rotation. Logout révoque le refresh record. Changement de mot de passe, suspension et compromission suspectée révoquent toutes les sessions du compte.

La rotation de clés doit prévoir un chevauchement. HS256 est simple, mais chaque vérificateur possède le secret partagé. Quand les services se multiplient, RS256 ou ES256 avec JWKS est plus sain : les vérificateurs n’ont besoin que des clés publiques.

import { createRemoteJWKSet, jwtVerify } from "jose";

const JWKS = createRemoteJWKSet(
  new URL("https://auth.example.com/.well-known/jwks.json")
);

export async function verifyWithRotatingKeys(token: string) {
  return jwtVerify(token, JWKS, {
    issuer: "https://auth.example.com",
    audience: "claudecodelab-api",
    algorithms: ["RS256", "ES256"],
  });
}
{
  "rotationPlan": {
    "step1": "Créer une nouvelle clé et la publier dans JWKS",
    "step2": "Signer les nouveaux tokens avec le nouveau kid",
    "step3": "Garder l'ancienne clé publique jusqu'à expiration",
    "step4": "Relire les logs puis supprimer l'ancienne clé"
  }
}

Cas d’usage et prompts sûrs

flowchart LR
  Login["Connexion"] --> Access["Access token court"]
  Login --> Refresh["Refresh cookie HttpOnly"]
  Access --> API["API valide iss/aud/exp/jti"]
  Refresh --> Rotate["Rotation au refresh"]
  Rotate --> Store["DB/Redis stocke hash et sid"]
  Store --> Reuse["Réutilisation: révoquer la famille"]

Cas 1 : panneau SaaS. Vous pouvez mettre tenantId dans le claim pour le contexte, mais chaque requête base de données doit aussi filtrer par tenant. Les droits admin, l’état de facturation et la suspension se relisent côté serveur avant les actions destructrices.

Cas 2 : contenu payant ou formation. Gardez les access tokens courts et rafraîchissez silencieusement pour ne pas interrompre le lecteur. Si le site contient publicité, Analytics et CTA d’achat, reliez ce travail aux headers de sécurité web et au consentement Cookie.

Cas 3 : application mobile ou desktop. Utilisez le stockage sécurisé du système plutôt que les Cookies du navigateur. Gardez sid pour révoquer un appareil perdu et journalisez la réutilisation de refresh token comme événement de sécurité.

Cas 4 : microservices. Ne distribuez pas un secret symétrique partout. Préférez vérification par clé publique, gateway ou token exchange. Chaque service doit valider aud.

Conçois et implémente l'authentification JWT dans ce dépôt.
Avant modification, produis un tableau avec:
- framework, modèle utilisateur, code session/cookie, middleware auth
- contrôles d'autorisation, CSRF, CSP et rate limit existants
- stockage actuel du token et risque XSS/CSRF

Règles:
- Utiliser jose. Ne pas revenir à jsonwebtoken.
- Access token 15 minutes. Refresh token 7 jours.
- Valider iss, aud, sub, exp, iat, jti et sid.
- Stocker seulement le hash du refresh token en DB/Redis.
- Faire la rotation et révoquer la famille sid en cas de reuse.
- Ne pas afficher secrets, .env ou tokens de production.
- Terminer avec une preuve de test ou curl.

Échecs, vérification et CTA

Les échecs fréquents sont connus : algorithme non fixé, données sensibles dans le payload, absence de aud ouiss, refresh token réutilisable, logout limité au navigateur, suppression trop rapide de l’ancienne clé, secret réel collé dans Claude Code. Les protections correspondantes sont allowlist d’algorithmes, claims minimaux, rotation, révocation par sid, fenêtre JWKS et prompts sans secrets.

curl -i -X POST https://example.com/api/auth/login \
  -H "content-type: application/json" \
  -d '{"email":"demo@example.com","password":"correct horse"}'

npm test -- --runInBand auth

Testez tokens expirés, signatures modifiées, mauvaise audience, jti révoqué, refresh token réutilisé, logout, changement de mot de passe et suspension. Vérifiez aussi que la Cookie refresh possède HttpOnly, Secure, un SameSite adapté et un path aussi étroit que possible.

Pour standardiser ce travail, commencez par la cheatsheet Claude Code gratuite afin de fixer les habitudes de vérification. Pour des prompts et modèles réutilisables, consultez les produits ClaudeCodeLab. Pour une équipe qui doit aligner JWT, RBAC, cookies, audit logs et CI gates, utilisez Claude Code training and consultation.

En testant l’exemple de cet article, l’étape la plus utile a été d’écrire le tableau des claims avant le code de signature. Cela a révélé tôt un aud manquant, un exemple avec top-level await fragile dans le chemin tsx par défaut et le danger de stocker des refresh tokens en clair. Une authentification JWT fiable ne consiste pas seulement à signer un token : elle relie claims, validation, stockage, rotation, révocation et clés dans un flux vérifiable.

#Claude Code #JWT #authentification #sécurité #Node.js
Gratuit

PDF gratuit: cheatsheet Claude Code

Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.

Nous protégeons vos données et n'envoyons pas de spam.

Masa

À propos de l'auteur

Masa

Ingénieur spécialisé dans les workflows pratiques avec Claude Code.