Se connecter avec Google, Microsoft, Keycloak ou Auth0 signifie que votre application ne voit jamais le mot de passe. Elle envoie l’utilisateur chez le fournisseur d’identité, qui le renvoie avec un code à usage unique ; l’application échange ce code contre des tokens. OAuth 2.1 — la version nettoyée d’OAuth 2.0 — garde un seul flow pour les applications dans le navigateur : l’authorization code avec PKCE.
PKCE (Proof Key for Code Exchange, prononcé « pixy ») résout un problème précis. Une application single-page est un client public : elle n’a aucun secret à cacher, car tout ce qu’elle livre peut être lu. Il lui faut donc un autre moyen de prouver que celui qui échange le code est celui qui l’a demandé. Les démos de cette page utilisent une vraie cryptographie (Web Crypto), pas une imitation.
response_type=token, tokens dans le fragment de l’URL) a disparu d’OAuth 2.1 : les tokens dans les URL finissent dans l’historique du navigateur, dans les logs et dans les en-têtes Referer. Passer au code flow avec PKCE est en général un changement de configuration dans votre bibliothèque et chez le fournisseur.Le flow, en sept étapes
L’application prépare quelques valeurs aléatoires, envoie le navigateur chez le fournisseur, reçoit un code en retour et l’échange — avec une preuve — contre des tokens. Seule la dernière étape concerne votre API. Parcourez-le dans la démo ; cochez « attaquant » pour voir ce qu’arrête la vérification de state.
app (browser) fournisseur d’identité API
│ 1. verifier, challenge = S256(verifier), state, nonce │
│ 2. GET /authorize?…&code_challenge&state&nonce ─▶│ │
│ 3. connexion, MFA, consentement │
│◀─ 4. 302 /callback?code=…&state=… (vérifier state !) │
│ 5. POST /token code + code_verifier ────────────▶│ S256(verifier) == challenge?
│◀─ 6. access_token, id_token, refresh_token ───────│ │
│ 7. GET /orders Authorization: Bearer <access_token> ─────────────────────▶│PKCE : prouver que c’est vous qui avez demandé
Avant la redirection, l’application génère un code_verifier aléatoire et n’envoie à /authorize que son hash, le code_challenge. Le fournisseur le garde à côté du code. Quand l’application échange le code sur /token, elle envoie le verifier lui-même ; le fournisseur le hache et compare. Celui qui intercepte le code — une application malveillante enregistrée sur la même redirection, un log, une extension de navigateur — n’a pas le verifier, et le code ne lui sert à rien.
/** base64url sans padding (RFC 4648 §5), comme l’exige PKCE */
function base64url(bytes: Uint8Array): string {
let binary = '';
for (const b of bytes) binary += String.fromCharCode(b);
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
/** 32 octets aléatoires → 43 caractères (le minimum autorisé est 43, le maximum 128) */
export function createVerifier(): string {
return base64url(crypto.getRandomValues(new Uint8Array(32)));
}
/** code_challenge = BASE64URL(SHA-256(code_verifier)), méthode S256 */
export async function challengeFor(verifier: string): Promise<string> {
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
return base64url(new Uint8Array(digest));
}
// RFC 7636, annexe B :
// challengeFor('dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk')
// → 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM'- 1L’application génère un verifier
0 caractères · entre 43 et 128 sont autorisés
code_challenge… - 2Seul le challenge part vers /authorize
Le fournisseur n’a encore rien reçu.
- 3Échanger le code sur /token
Utilisez S256, jamais plain (avec plain, le challenge est le verifier, donc qui voit la redirection a tout). Le verifier reste dans sessionStorage — cet onglet seulement — et est supprimé dès que le code est échangé.
state, nonce et redirect_uri
state— une valeur aléatoire enregistrée avant la redirection et vérifiée dans/callback. Sans elle, un attaquant peut envoyer la victime sur votre callback avec le code de l’attaquant, et la victime se retrouve connectée au compte de l’attaquant (login CSRF). PKCE bloque aussi cela, mais les fournisseurs exigent quand mêmestate, et vous en avez de toute façon besoin pour transporter votre propre contexte.nonce— une valeur aléatoire envoyée à/authorizequi doit revenir dans l’ID token. Elle prouve que l’ID token a été émis pour cette connexion et non copié d’une précédente.redirect_uri— enregistrée chez le fournisseur exactement, sans wildcards. Et la destination après la connexion (returnTo) est enregistrée par votre application avant la redirection ; ne la prenez jamais d’un?returnUrl=dans la barre d’adresse, sinon votre page de connexion devient un open redirect.
import { challengeFor, createVerifier } from './pkce';
const AUTHORIZE = 'https://id.example.com/authorize';
export async function login(returnTo = '/'): Promise<void> {
const verifier = createVerifier();
const state = createVerifier(); // aléatoire, à usage unique
const nonce = createVerifier();
// sessionStorage : cet onglet seulement, effacé quand l’onglet se ferme
sessionStorage.setItem('oauth', JSON.stringify({ verifier, state, nonce, returnTo }));
const url = new URL(AUTHORIZE);
url.search = new URLSearchParams({
response_type: 'code',
client_id: 'atlas-spa',
redirect_uri: `${location.origin}/callback`, // enregistrée EXACTEMENT, sans wildcards
scope: 'openid profile email',
code_challenge: await challengeFor(verifier),
code_challenge_method: 'S256',
state,
nonce,
}).toString();
location.assign(url);
}Trois tokens, trois rôles
La réponse de /token contient jusqu’à trois tokens, et les confondre est l’erreur la plus fréquente. L’ID token dit à l’application qui s’est connecté. L’access token dit à l’API ce que l’appelant peut faire. Le refresh token obtient de nouveaux access tokens sans nouvelle connexion. Décoder un JWT dans le navigateur est utile pour l’affichage, mais ce n’est pas une validation : la signature se vérifie avec les clés publiques du fournisseur (JWKS), et c’est le travail d’une bibliothèque.
| ID token | Access token | Refresh token | |
|---|---|---|---|
| Lu par | votre application | votre API | le fournisseur |
| Envoyé à | nulle part — il reste dans l’application | seulement l’origine de votre API | seulement le /token du fournisseur |
| Durée de vie | quelques minutes | 5 à 60 minutes | des heures à des jours, renouvelé à chaque usage |
Plus sûr : un BFF garde les tokens
Après l’étape 6, les tokens vivent dans le navigateur — exactement là où une XSS peut les atteindre. Le document de l’IETF sur OAuth pour les applications navigateur décrit une option plus solide : un Backend for Frontend. Un petit serveur sur votre propre site fait l’échange du code, garde les tokens côté serveur et ne donne au navigateur qu’un cookie de session HttpOnly, que JavaScript ne peut pas lire.
browser (SPA) BFF (votre serveur, même site) fournisseur d’identité
│ GET /auth/login ──────────▶│ PKCE + state + nonce, côté serveur ────────▶│ /authorize
│◀─ 302 vers le fournisseur ─│ │
│ …connexion chez le fournisseur, retour au BFF… │
│ │ code + verifier ──────────────────────────▶│ /token
│ │◀── access, id, refresh (restent ICI) ───────│
│◀─ Set-Cookie: session=…; HttpOnly; Secure; SameSite=Strict │
│ GET /api/orders (cookie) ─▶│ ajoute Authorization: Bearer … ──▶ API │La SPA appelle /api sur sa propre origine ; le BFF ajoute l’access token et gère le refresh. Une XSS peut encore faire des requêtes tant que la page est ouverte, mais elle ne peut pas voler de tokens pour s’en servir ailleurs. Le prix : un composant de plus à faire tourner, et une protection CSRF pour le cookie (SameSite, un en-tête anti-CSRF). Pour les tokens gardés dans le navigateur, l’intercepteur avec file d’attente et refresh est sur la page tokens et refresh.
Dans Angular : une bibliothèque, un guard et un intercepteur
N’implémentez pas le protocole vous-même. Des bibliothèques certifiées comme angular-auth-oidc-client ou angular-oauth2-oidc gèrent la découverte du fournisseur, PKCE, la validation de l’ID token signature comprise, le refresh et la déconnexion. Le code de cette page montre ce qu’elles font, pour que vous puissiez les configurer et les déboguer en connaissance de cause. Votre part :
- La configuration : authority, client id,
redirect_uri, scopes et le code flow avec PKCE — en général par défaut. - Un guard sur les routes privées, qui lance la connexion et retient où revenir. Voir guards & resolvers.
- Un intercepteur qui n’ajoute l’access token que pour l’origine de votre API. Un token envoyé à toutes les URL finit chez des tiers ; voir la sécurité pour le reste du tableau.
Quand OUI, quand NON
- Une connexion via un fournisseur d’identité externe : SSO d’entreprise, Google, Microsoft, Apple.
- Plusieurs applications qui partagent la même connexion et les mêmes utilisateurs.
- MFA, passkeys et récupération de compte, que vous préférez laisser à des spécialistes.
- Une seule application avec ses propres utilisateurs et son backend : un cookie de session classique peut suffire. Les tokens et le refresh ont leur propre page.
- Les appels de serveur à serveur : c’est le flow client credentials, sur le serveur, jamais dans le navigateur.
- Écrire le protocole vous-même en production : utilisez une bibliothèque certifiée.
Pièges fréquents
- L’implicit flow, ou des tokens dans l’URL. Ils finissent dans l’historique, les logs et l’en-tête
Referer. - Ne pas vérifier
state. Le callback accepte un code que votre application n’a jamais demandé. - L’ID token envoyé à l’API. Il est pour l’application ; l’API prend des access tokens, avec la bonne audience.
- Décoder au lieu de valider. Sans vérification de la signature, n’importe qui peut écrire un JWT avec n’importe quels claims.
- Le verifier dans
localStorage, gardé après usage. Sa place est danssessionStorage, et il meurt avec l’échange. - Un
?returnUrl=pris dans la barre d’adresse. Votre page de connexion devient un open redirect utilisé pour le phishing.
Aide-mémoire
SPA (OAuth 2.1) authorization code + PKCE, rien d’autre (pas d’implicit flow)
code_verifier 43 à 128 caractères aléatoires, dans sessionStorage, supprimé après usage
code_challenge BASE64URL(SHA-256(verifier)), méthode S256 (jamais "plain")
state aléatoire, vérifié sur /callback : bloque le login CSRF
nonce aléatoire, doit figurer dans l’ID token : bloque les tokens rejoués
redirect_uri enregistrée exactement, sans wildcards ; returnTo enregistré AVANT la redirection
ID token qui s’est connecté (pour l’app) ; valider iss, aud, exp, nonce, signature
access token ce que vous pouvez appeler (pour l’API) ; seulement vers l’origine de votre API
refresh token renouvelé à chaque usage, ou pas du tout dans le navigateur
le plus sûr un BFF : tokens sur le serveur, un cookie HttpOnly dans le navigateur
déconnexion session locale + end_session_endpoint du fournisseur
ne pas l’écrire soi-même bibliothèques certifiées : angular-auth-oidc-client, angular-oauth2-oidc