Authorization Code Flow avec PKCE, étape par étape
Comprendre Authorization Code Flow avec PKCE : code_verifier, code_challenge, échange du code et protection contre le vol du code d’autorisation.
- Identity Security
- OIDC
- SSO
J’ai déjà décrit le login SSO avec OpenID Connect et le rôle de l’ID token, de l’access token et du refresh token. Ici, je zoome sur l’aller-retour central : comment l’application récupère ces tokens sans les faire transiter directement dans la redirection du navigateur.
C’est le rôle de l’Authorization Code Flow. PKCE ajoute une propriété essentielle : même si quelqu’un récupère le code d’autorisation, ce code ne doit pas suffire à obtenir les tokens.
Le problème
Après l’authentification, l’Identity Provider redirige le navigateur vers l’application avec un authorization code.
Ce code est court, temporaire et à usage unique, mais il reste sensible. Un attaquant qui l’intercepte peut tenter de l’échanger avant le client légitime.
Un client confidentiel peut aussi s’authentifier au token endpoint avec un client secret. Mais ce secret répond surtout à :
Quel client appelle le token endpoint ?
PKCE ajoute une autre question :
Le client qui présente ce code possède-t-il
la preuve éphémère créée au début de cette transaction ?
PKCE signifie Proof Key for Code Exchange. Il est défini par la RFC 7636. Les recommandations OAuth actuelles, dans la RFC 9700, l’imposent aux clients publics et le recommandent aussi aux clients confidentiels.
Le modèle mental
PKCE repose sur deux valeurs :
code_verifier
↓ SHA-256 + base64url
code_challenge
Le code_verifier est un secret aléatoire généré pour une seule demande d’autorisation. Avec S256, le client en dérive :
code_challenge =
BASE64URL(SHA256(code_verifier))
Le client envoie le challenge au début du flux, mais conserve le verifier.
Authorization request
→ code_challenge
Callback
← authorization code
Token request
→ authorization code
→ code_verifier
Le serveur recalcule ensuite le challenge à partir du verifier reçu et le compare à celui associé au code.
Le principe à retenir :
Voler le code ne suffit pas.
Il faut aussi posséder le verifier correspondant.
PKCE ne chiffre rien et ne remplace pas TLS. Il lie un authorization code à la transaction qui l’a créé.
Comment ça fonctionne
1. Génération du verifier
Avant la redirection, le client génère un code_verifier aléatoire à forte entropie. La RFC 7636 définit une longueur de 43 à 128 caractères.
Cette valeur reste côté client.
2. Calcul du challenge
Avec S256 :
challenge = BASE64URL(SHA256(code_verifier))
Le mode plain, où challenge et verifier sont identiques, existe encore pour compatibilité. Pour une implémentation moderne, S256 est le choix attendu.
3. Départ vers l’authorization endpoint
Dans un login OIDC, la requête ressemble conceptuellement à ceci :
response_type=code
client_id=identity-lab-web
redirect_uri=https://app.example.com/callback
scope=openid profile email
state=...
code_challenge=...
code_challenge_method=S256
Le scope=openid transforme la requête OAuth en demande OpenID Connect.
state et PKCE n’ont pas le même rôle. state aide à rattacher le callback au flux initié par le client ; PKCE protège l’échange du code.
4. Authentification et retour du code
L’Identity Provider authentifie l’utilisateur puis redirige le navigateur vers la redirect_uri enregistrée :
https://app.example.com/callback?code=abc123&state=...
Dans l’Authorization Code Flow, les tokens ne sont pas placés dans cette URL. OpenID Connect Core définit le flux ainsi : le code revient depuis l’authorization endpoint, puis les tokens sont obtenus auprès du token endpoint.
5. Échange au token endpoint
Le client présente ensuite :
grant_type=authorization_code
code=abc123
redirect_uri=https://app.example.com/callback
code_verifier=<valeur originale>
S’il s’agit d’un client confidentiel, il peut en plus s’authentifier avec son client secret.
Les deux contrôles sont complémentaires :
client secret → authentifie le client
code_verifier → lie le code à la transaction
Le serveur recalcule BASE64URL(SHA256(code_verifier)). Si le résultat ne correspond pas au challenge enregistré, l’échange doit échouer. Sinon, et si les autres contrôles passent, il peut émettre les tokens.
Exemple concret
Alice démarre un login.
Le client crée :
verifier = V
challenge = SHA256(V)
Il envoie le challenge, puis Keycloak renvoie plus tard :
code = C
Un attaquant réussit à récupérer C.
Sans PKCE, il chercherait à échanger ce code avant l’application. Avec PKCE, le token endpoint attend aussi V.
Le serveur connaît :
C → challenge attendu
L’attaquant connaît C, mais pas le verifier permettant de produire ce challenge. Son échange échoue.
PKCE ne rend donc pas le code impossible à voler. Il cherche à rendre un code volé inutilisable sans la preuve correspondante.
Dans mon lab
Dans mon Identity Security Lab, le frontend Next.js utilise Auth.js avec Keycloak.
Le client OIDC identity-lab-web est configuré comme client confidentiel :
Client authentication ON
Standard flow ON
Implicit flow OFF
Redirect URI http://localhost:3000/api/auth/callback/keycloak
Le Standard Flow de Keycloak correspond ici à l’Authorization Code Flow.
Côté application, je n’ai pas réimplémenté PKCE à la main. La configuration est essentiellement :
Keycloak({
clientId: process.env.KEYCLOAK_CLIENT_ID,
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
issuer: process.env.KEYCLOAK_ISSUER,
})
Auth.js prend en charge le flux OAuth/OIDC et utilise PKCE par défaut. Le code du lab n’a donc pas à générer lui-même le verifier, à le conserver pendant la redirection puis à le présenter au token endpoint.
La documentation du repo décrit le flux réel : challenge envoyé au départ, login sur Keycloak, retour du code, échange avec le verifier, récupération des tokens puis création de la session. Le mot de passe reste chez Keycloak et l’échange du code se fait côté serveur.
J’ai également configuré une redirect URI précise, sans wildcard. Le lab documente un test où :
http://attaquant.example.com/vol
est utilisé comme redirect URI et Keycloak répond 400. Ce contrôle et PKCE se complètent : l’un limite la destination du code, l’autre protège son échange.
Une limite réelle de ma configuration
L’export Keycloak actuel de identity-lab-web n’impose pas explicitement PKCE method = S256.
Le flux utilise bien PKCE parce qu’Auth.js l’envoie. Keycloak vérifie alors le code_verifier. Mais la documentation Keycloak précise qu’un champ PKCE laissé vide autorise PKCE sans le rendre obligatoire pour le client.
Pour durcir le lab, j’ajouterais donc côté Keycloak :
PKCE method = S256
Ainsi, une régression côté client qui supprimerait PKCE ferait échouer le flux au lieu de dégrader silencieusement la protection.
Deux autres limites sont volontaires. Keycloak tourne en local avec sslRequired: none, donc sans TLS : acceptable pour ce lab isolé, pas pour une exposition réelle. Et Direct access grants reste activé uniquement pour certains tests curl du RBAC ; ce n’est pas le flux utilisé par le login web et je le désactiverais en production.
Ce qui peut mal tourner
Le verifier ne correspond pas
Symptôme : l’utilisateur revient bien sur le callback, mais l’échange du code échoue.
À ce stade, inutile de repartir sur le mot de passe. Il faut vérifier le couple PKCE : challenge envoyé au départ, verifier conservé par le client, puis verifier présenté au token endpoint.
plain est utilisé à la place de S256
Avec plain, le challenge est le verifier. La valeur censée servir de preuve secrète apparaît donc directement dans la requête initiale.
Les recommandations actuelles privilégient S256 précisément pour éviter cela.
Le client secret est considéré comme suffisant
Un client secret authentifie un client capable de le protéger. Il ne remplace pas le lien entre un code et la transaction qui l’a créé. C’est pourquoi la RFC 9700 recommande également PKCE pour les clients confidentiels.
PKCE est pris pour une protection universelle
PKCE ne remplace pas :
HTTPS
redirect URIs strictes
validation de l’issuer
contrôles OIDC
validation des tokens
C’est une protection ciblée contre le détournement ou le mauvais usage d’un authorization code.
À retenir
- L’Authorization Code Flow renvoie d’abord un code, puis récupère les tokens au token endpoint.
- PKCE crée un
code_verifieréphémère propre à la transaction. - Le client envoie d’abord un
code_challenge, puis révèle le verifier lors de l’échange du code. - Avec
S256, le challenge dérive du verifier par SHA-256 et base64url. - Un code intercepté ne suffit donc pas sans le verifier correspondant.
- PKCE est obligatoire pour les clients publics dans les recommandations OAuth actuelles et recommandé aussi pour les clients confidentiels.
- Il complète le client secret,
state, les redirect URIs strictes et TLS ; il ne les remplace pas. - Dans mon lab, Auth.js gère PKCE, mais imposer
S256côté Keycloak renforcerait encore le contrôle serveur.
Le flux est maintenant complet : login, retour du code, preuve PKCE, échange et émission des tokens.
La prochaine couche technique est côté API : comment vérifier qu’un access token JWT reçu peut réellement être considéré comme valide.