OAuth avec Claude Code : PKCE, state, nonce et stockage sûr des tokens
Guide pratique OAuth 2.1/2.0 Authorization Code + PKCE avec Claude Code, sans fuite de secrets.
OAuth ne se résume pas à afficher un bouton “Se connecter avec Google”. Une erreur peut lier le mauvais compte, réutiliser un code d’autorisation, remplacer le redirect URI ou exposer des tokens au JavaScript du navigateur. Ce guide montre comment utiliser Claude Code pour générer une base OAuth sans lui donner de vrais secrets, puis comment la relire sérieusement.
Le choix pratique est OAuth 2.0 Authorization Code + PKCE, dans l’esprit d’OAuth 2.1. PKCE permet à l’application de prouver, au moment de l’échange de token, qu’elle possède encore le verifier original lié au challenge envoyé au début. Pour le contexte, voir l’authentification avec Claude Code, le développement d’API et le guide de démarrage.
Architecture recommandée
Claude Code peut générer routes, sessions, tests et checklist. Ne collez pas de client secret réel, refresh token, .env de production ou capture de console fournisseur. Donnez des noms de variables, les comportements attendus et les cas d’échec.
| Zone | Recommandation | Pourquoi |
|---|---|---|
| Flux | Authorization Code + PKCE | Évite l’exposition de tokens des anciens flux |
| CSRF | Stocker et vérifier state | Bloque les callbacks forgés |
| Rejeu OIDC | Stocker et vérifier nonce | Détecte les résultats d’identité rejoués |
| Redirect URI | Correspondance exacte | Empêche le remplacement de destination |
| Tokens | Session serveur ou stockage chiffré | Garde les tokens longs hors de localStorage |
| Entrée Claude Code | Valeurs factices et spécification | Évite les secrets dans prompts et logs |
Références primaires : OAuth 2.1, RFC 7636 PKCE, RFC 9700 OAuth 2.0 Security BCP, OpenID Connect Core, OWASP OAuth2 Cheat Sheet, Claude Code Security.
Trois cas d’usage
Premier cas : une console B2B SaaS. L’utilisateur se connecte avec Google Workspace ou Microsoft Entra ID, mais l’application le mappe ensuite vers un utilisateur, une organisation et un rôle internes. OAuth prouve l’identité ; votre autorisation décide des droits.
Deuxième cas : l’accès délégué à des APIs. Calendrier, e-mail, stockage et CRM exigent consentement utilisateur, scopes minimaux, refresh tokens chiffrés et déconnexion explicite.
Troisième cas : outils internes et services de type MCP. Les API keys statiques sont simples au début, mais compliquent révocation et audit. OAuth/OIDC clarifie login, consentement, expiration et identité.
Pour mobile et SPA, PKCE est indispensable car le client secret ne peut pas être caché. Même côté serveur, PKCE facilite la revue.
Démo locale copiable
Cette démo ne demande aucun compte Google, Microsoft ou Auth0. Un seul processus Express joue à la fois le client OAuth et le serveur d’autorisation simulé afin de voir state, nonce, PKCE S256, redirect URI exact, codes à usage unique et tokens côté serveur.
Créez ces fichiers dans un dossier vide, lancez npm install && npm start, puis ouvrez http://localhost:3000.
{
"scripts": { "start": "node server.mjs" },
"dependencies": { "express": "^4.19.2", "express-session": "^1.18.0" },
"engines": { "node": ">=20" }
}
// server.mjs
import crypto from "node:crypto";
import express from "express";
import session from "express-session";
const app = express();
app.use(express.urlencoded({ extended: false }));
app.use(session({
name: "oauth_demo_sid",
secret: "dev-only-change-this-32-byte-secret",
resave: false,
saveUninitialized: false,
cookie: { httpOnly: true, sameSite: "lax", secure: false, maxAge: 10 * 60 * 1000 },
}));
const client = {
clientId: "claude-code-demo",
redirectUri: "http://localhost:3000/callback",
scope: "openid profile email",
};
const authorizationEndpoint = "http://localhost:3000/mock/authorize";
const tokenEndpoint = "http://localhost:3000/mock/token";
const registeredRedirectUris = new Set([client.redirectUri]);
const pendingCodes = new Map();
function randomUrlSafe(bytes = 32) {
return crypto.randomBytes(bytes).toString("base64url");
}
function sha256Base64Url(value) {
return crypto.createHash("sha256").update(value).digest("base64url");
}
function fail(res, status, message) {
return res.status(status).type("text/plain").send(message);
}
app.get("/", (_req, res) => {
res.type("html").send(`<h1>OAuth PKCE local demo</h1><p><a href="/auth/login">Start login</a></p>`);
});
app.get("/auth/login", (req, res) => {
const state = randomUrlSafe();
const nonce = randomUrlSafe();
const codeVerifier = randomUrlSafe(48);
const codeChallenge = sha256Base64Url(codeVerifier);
req.session.oauth = { state, nonce, codeVerifier, createdAt: Date.now() };
const params = new URLSearchParams({
response_type: "code",
client_id: client.clientId,
redirect_uri: client.redirectUri,
scope: client.scope,
state,
nonce,
code_challenge: codeChallenge,
code_challenge_method: "S256",
});
res.redirect(`${authorizationEndpoint}?${params}`);
});
app.get("/mock/authorize", (req, res) => {
const p = req.query;
const redirectUri = String(p.redirect_uri || "");
if (p.response_type !== "code") return fail(res, 400, "response_type must be code");
if (p.client_id !== client.clientId) return fail(res, 400, "unknown client_id");
if (!registeredRedirectUris.has(redirectUri)) return fail(res, 400, "redirect_uri is not registered exactly");
if (p.code_challenge_method !== "S256") return fail(res, 400, "PKCE S256 is required");
if (!p.code_challenge || !p.state || !p.nonce) return fail(res, 400, "missing state, nonce, or PKCE challenge");
const code = randomUrlSafe(24);
pendingCodes.set(code, {
clientId: client.clientId,
redirectUri,
codeChallenge: String(p.code_challenge),
nonce: String(p.nonce),
expiresAt: Date.now() + 60_000,
used: false,
});
const redirect = new URL(redirectUri);
redirect.searchParams.set("code", code);
redirect.searchParams.set("state", String(p.state));
res.redirect(redirect.toString());
});
app.get("/callback", async (req, res) => {
const oauth = req.session.oauth;
const code = String(req.query.code || "");
const returnedState = String(req.query.state || "");
if (!oauth) return fail(res, 400, "missing OAuth session");
if (returnedState !== oauth.state) return fail(res, 403, "state mismatch: possible CSRF or mixed login attempt");
const response = await fetch(tokenEndpoint, {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: client.redirectUri,
client_id: client.clientId,
code_verifier: oauth.codeVerifier,
}),
});
const tokens = await response.json();
if (!response.ok) return fail(res, response.status, JSON.stringify(tokens, null, 2));
if (tokens.nonce !== oauth.nonce) return fail(res, 403, "nonce mismatch: possible replay");
req.session.oauth = undefined;
req.session.tokenSet = {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresAt: Date.now() + tokens.expires_in * 1000,
};
res.redirect("/dashboard");
});
app.post("/mock/token", (req, res) => {
const body = req.body;
const record = pendingCodes.get(body.code);
if (body.grant_type !== "authorization_code") return res.status(400).json({ error: "unsupported_grant_type" });
if (!record || record.used || record.expiresAt < Date.now()) return res.status(400).json({ error: "invalid_grant" });
if (body.client_id !== record.clientId) return res.status(400).json({ error: "invalid_client" });
if (body.redirect_uri !== record.redirectUri) return res.status(400).json({ error: "invalid_redirect_uri" });
if (sha256Base64Url(body.code_verifier || "") !== record.codeChallenge) return res.status(400).json({ error: "invalid_code_verifier" });
record.used = true;
res.json({ token_type: "Bearer", access_token: randomUrlSafe(32), refresh_token: randomUrlSafe(32), expires_in: 300, nonce: record.nonce });
});
app.get("/dashboard", (req, res) => {
const tokenSet = req.session.tokenSet;
if (!tokenSet) return res.redirect("/auth/login");
const secondsLeft = Math.max(0, Math.floor((tokenSet.expiresAt - Date.now()) / 1000));
res.type("html").send(`<h1>Logged in</h1><p>Access token is stored server-side, not in localStorage.</p><p>Expires in ${secondsLeft} seconds.</p>`);
});
app.listen(3000, () => console.log("Open http://localhost:3000"));
Les points clés : /auth/login stocke state, nonce et code_verifier en session serveur ; la requête n’envoie que code_challenge ; /callback vérifie state ; /mock/token recalcule S256 ; /dashboard lit les tokens côté serveur, pas dans localStorage.
Prompt pour Claude Code
Implémente OAuth 2.0 Authorization Code + PKCE en Express + TypeScript.
Contraintes :
- Définir les paramètres fournisseur uniquement comme noms de variables d’environnement, sans secrets réels.
- Stocker state, nonce et code_verifier en session serveur.
- Vérifier redirect_uri par correspondance exacte.
- Autoriser seulement code_challenge_method=S256.
- Ne jamais stocker access token ou refresh token dans localStorage.
- Stocker les refresh tokens uniquement en base chiffrée ou session serveur.
- Ajouter des tests pour succès, state invalide, PKCE invalide, code expiré et réutilisation.
- Finir par une checklist de sécurité.
Pour la revue :
Relis cette implémentation OAuth selon RFC 9700, RFC 7636 et OWASP OAuth2 Cheat Sheet.
Vérifie les secrets dans logs, snapshots, bundles frontend et Git diff.
Classe les constats en High, Medium ou Low et propose des patchs.
Pièges fréquents
Ne validez pas le redirect URI par préfixe. https://app.example.com.evil.test/callback n’est pas votre application. Utilisez une correspondance exacte avec l’URI enregistrée.
Ne générez pas state sans le vérifier. Stockez-le au début, comparez-le au callback et supprimez-le après usage. Pour plusieurs onglets, gardez des transactions courtes indexées par state.
N’autorisez pas PKCE plain sans raison de compatibilité solide. Préférez S256, ne loggez jamais le verifier, expirez vite les codes et rendez-les uniques.
Avec OIDC, nonce ne suffit pas. Validez signature JWT, issuer, audience, expiration et nonce. ID Token prouve l’identité ; Access Token autorise une API.
Évitez localStorage pour les tokens longs. Utilisez sessions serveur, stockage chiffré, cookies courts et révocation. Voir Claude Code cookie management.
Checklist livrable
- Durée de vie et refresh des tokens documentés.
- L’équipe sait expliquer
state,nonceet PKCE simplement. - Redirect URIs, scopes et console fournisseur documentés.
- Logs sans code, verifier, token ni cookie.
- Erreurs sans détails internes.
- Tests sur succès, mismatch, expiration et réutilisation.
- Sortie Claude Code relue avec les sources officielles.
La formation et le conseil ClaudeCodeLab peuvent transformer cela en atelier : prendre un flux OAuth existant, cartographier les risques, ajouter les tests et produire une checklist réutilisable. Voir ClaudeCodeLab training.
Vérification pratique
La démo couvre début de login, autorisation simulée, callback, échange de token et dashboard. Si vous modifiez state, l’application répond state mismatch. Si le verifier est faux, le serveur renvoie invalid_code_verifier. Dans mes prototypes OAuth, les erreurs oubliées venaient surtout des onglets multiples, du bouton retour, des codes expirés et des logs. Ajoutez ces cas dès le premier prompt Claude Code.
Résumé
La qualité OAuth consiste à protéger toute la transaction. Utilisez Authorization Code + PKCE, vérifiez state et nonce, matchez exactement le redirect URI et gardez les tokens côté serveur. Claude Code accélère le travail, mais seulement avec des contraintes explicites et une revue fondée sur les sources primaires.
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.