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.
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.
| Claim | Utilité | Attention |
|---|---|---|
sub | Identifiant utilisateur | Préférer un ID interne à un email |
iss | Émetteur | Le fixer au serveur d’authentification |
aud | Audience | Refuser les tokens d’une autre API |
exp | Expiration | Garder l’access token court |
jti | ID du token | Révocation et audit |
sid | ID de session | Logout par appareil et famille de tokens |
role | Rôle large | Revalider 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.
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.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.
Articles liés
Workflow Obsidian vers CLAUDE.md avec Claude Code
Transformer des notes Obsidian en notes CLAUDE.md concises pour reprendre les sessions sans réexpliquer.
Claude Code Revenue CTA Routing : relier articles, PDF, Gumroad et consultation
Un workflow Claude Code pour orienter les lecteurs vers PDF gratuit, Gumroad ou consultation selon l'intention.
Règles de handoff Claude Code en équipe: preuves, permissions, rollback et revenus
Un format concret pour transmettre un travail Claude Code avec preuves, permissions, rollback, PDF gratuit, Gumroad et consultation.