OAuth 2.x permet à une application d'accéder à vos données sans connaître votre mot de passe. C'est le standard utilisé quand une app demande "Se connecter avec Google" ou quand vos microservices communiquent entre eux. OAuth ne gère pas l'identité (c'est le rôle d'OIDC), il gère la délégation d'accès : "cette application peut lire mes fichiers, mais pas les supprimer".
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »Ce guide vous explique OAuth 2.x, le standard dominant pour sécuriser l'accès aux APIs. À la fin, vous saurez :
- Identifier les 4 acteurs OAuth et leur rôle
- Choisir le bon flow selon votre contexte (SPA, mobile, server-to-server)
- Implémenter PKCE correctement (exigé des clients publics par la RFC 9700)
- Situer le rôle de l'access token et du refresh token, et leurs durées de vie
Prérequis : avoir lu Les bases de l'IAM.
Le problème qu'OAuth résout
Section intitulée « Le problème qu'OAuth résout »Prenez le cas d'une application de suivi de commits qui doit lire vos dépôts GitHub. Avant OAuth, la seule façon de lui donner accès était de lui confier vos identifiants, avec trois conséquences :
- Donner votre mot de passe GitHub à l'application
- Faire confiance à cette application pour ne pas l'utiliser malicieusement
- Changer votre mot de passe pour révoquer l'accès (révoquant aussi tous les autres accès)
OAuth permet de déléguer un accès limité sans partager le mot de passe :
- L'application obtient un token avec des permissions spécifiques (scopes)
- Vous pouvez révoquer ce token sans toucher à votre mot de passe
- L'application ne voit jamais vos credentials
Les 4 acteurs OAuth
Section intitulée « Les 4 acteurs OAuth »La confusion la plus fréquente porte sur les deux serveurs. L'Authorization Server délivre les jetons et ne détient aucune donnée métier ; le Resource Server détient les données et ne sait pas authentifier un utilisateur, il se contente de valider le jeton qu'on lui présente. Cette séparation est ce qui permet à un même serveur d'autorisation de protéger des dizaines d'API différentes. Dans les déploiements réels, ces deux rôles sont souvent portés par des équipes distinctes, ce qui explique pourquoi les problèmes de scopes se découvrent tard.
| Acteur | Rôle | Exemple |
|---|---|---|
| Resource Owner | L'entité qui possède les données | Vous (votre compte GitHub) |
| Client | L'application qui veut accéder aux données | Application de suivi de commits |
| Authorization Server | Authentifie le Resource Owner, délivre les tokens | Keycloak, Auth0, GitHub OAuth |
| Resource Server | API qui protège les données, valide les tokens | API GitHub |
Schéma des interactions
Section intitulée « Schéma des interactions »Suivez les flèches dans l'ordre et repérez ce qui ne circule jamais : le mot de passe de l'utilisateur ne quitte pas l'Authorization Server, et le client ne le voit à aucun moment. Le second point à observer est que le client ne parle jamais directement au Resource Owner pour obtenir des droits : tout passe par une redirection du navigateur vers l'Authorization Server, ce qui est précisément la fenêtre que les attaques OAuth cherchent à exploiter.
Scopes : les permissions demandées
Section intitulée « Scopes : les permissions demandées »Les scopes définissent quelles permissions le client demande. L'utilisateur (Resource Owner) peut les accepter ou les refuser.
Exemples de scopes
Section intitulée « Exemples de scopes »Il n'existe aucune norme sur le nommage des scopes : chaque fournisseur invente le sien, et seuls les scopes OIDC (openid, profile, email) sont standardisés. Conséquence pratique, un scope valable chez un fournisseur n'a aucun sens chez un autre, et une erreur de nom se traduit par un jeton délivré avec moins de droits que prévu, sans message d'erreur explicite. Vérifiez toujours les noms exacts dans la documentation du fournisseur : GitHub, par exemple, expose repo, public_repo, repo:status et repo_deployment pour ses applications OAuth classiques, pas de couple lecture/écriture générique.
| Service | Scope | Permission accordée |
|---|---|---|
| GitHub | repo | Accès complet aux dépôts privés et publics |
| GitHub | public_repo | Accès aux seuls dépôts publics |
| GitHub | repo:status | Lecture et écriture des statuts de commit |
profile | Lire le profil public | |
email | Lire l'adresse email | |
| Keycloak | offline_access | Obtenir un refresh token |
Principe du moindre privilège
Section intitulée « Principe du moindre privilège »Demandez uniquement les scopes nécessaires. L'argument n'est pas seulement l'hésitation de l'utilisateur au moment du consentement : un jeton porteur d'un scope large reste dangereux pendant toute sa durée de vie, et un vol de jeton donne alors accès à tout ce que le scope couvre. Un jeton limité à la lecture ne permet ni suppression ni modification, quelle que soit la façon dont il a fuité.
Redirect URI et le flux de redirection
Section intitulée « Redirect URI et le flux de redirection »Le Redirect URI (ou callback URL) est l'URL vers laquelle l'Authorization Server redirige l'utilisateur après l'authentification.
Sécurité critique
Section intitulée « Sécurité critique »Ces trois règles visent toutes le même scénario d'attaque : détourner le code d'autorisation au moment de la redirection. La première est la plus souvent contournée en environnement de développement, où l'on ajoute un caractère joker pour tester plusieurs ports. Ce raccourci finit régulièrement en production, où il suffit alors d'enregistrer un sous-domaine correspondant au motif pour recevoir les codes des utilisateurs. L'exception http://localhost est explicitement prévue par les spécifications, mais elle ne vaut que pour le développement local.
| Règle | Pourquoi |
|---|---|
| Exact match obligatoire | Évite l'interception du code/token par un attaquant |
| Enregistré à l'avance | Le serveur ne redirige que vers les URIs connues |
| HTTPS obligatoire | Sauf http://localhost pour le développement |
Les flows OAuth : lequel choisir ?
Section intitulée « Les flows OAuth : lequel choisir ? »OAuth 2.0 définit plusieurs flows (ou grants). La RFC 9700 (bonnes pratiques de sécurité, publiée en janvier 2025) interdit l'un d'eux et déconseille l'autre, et le brouillon OAuth 2.1 n'en conserve que les sécurisés.
Le flow recommandé pour tout client interactif
Section intitulée « Le flow recommandé pour tout client interactif »Usage : SPA, mobile, applications web avec utilisateur
PKCE (Proof Key for Code Exchange, prononcé « pixy ») résout un problème précis : sur un mobile ou dans un navigateur, le code d'autorisation transite par une redirection que d'autres applications peuvent intercepter. Sans PKCE, quiconque capte ce code peut l'échanger contre un jeton. Avec PKCE, l'échange n'aboutit que si le demandeur présente le code_verifier d'origine, un secret qui n'a jamais circulé sur le réseau. Les six étapes ci-dessous se déroulent en quelques centaines de millisecondes, l'utilisateur ne perçoit que l'écran de connexion.
-
Génération du vérifier
Le client génère un
code_verifieraléatoire (32 octets, base64url). -
Calcul du challenge
Le client calcule le challenge :
code_challenge = BASE64URL(SHA256(code_verifier)), et déclare la méthodecode_challenge_method=S256. La valeurplain, qui transmet le vérifier tel quel, existe encore dans la RFC mais n'offre aucune protection. -
Requête d'autorisation
Le client redirige vers l'Authorization Server avec le
code_challenge. -
Authentification + consentement
L'utilisateur s'authentifie et accepte les scopes.
-
Retour avec le code
L'Authorization Server redirige vers le client avec un
authorization_code. -
Échange du code
Le client envoie le
code+code_verifieret reçoit les tokens.
// Étape 1-2 : Génération PKCEconst codeVerifier = crypto.randomBytes(32).toString('base64url');const codeChallenge = crypto .createHash('sha256') .update(codeVerifier) .digest('base64url');
// Étape 3 : Requête d'autorisationconst authUrl = new URL('https://auth.example.com/authorize');authUrl.searchParams.set('response_type', 'code');authUrl.searchParams.set('client_id', 'my-app');authUrl.searchParams.set('redirect_uri', 'https://myapp.com/callback');authUrl.searchParams.set('scope', 'openid profile email');authUrl.searchParams.set('state', generateRandomState()); // Anti-CSRFauthUrl.searchParams.set('code_challenge', codeChallenge);authUrl.searchParams.set('code_challenge_method', 'S256');
// Rediriger l'utilisateur vers authUrl// Étape 6 : Échange du code côté callbackconst tokenResponse = await fetch('https://auth.example.com/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'authorization_code', code: authorizationCode, redirect_uri: 'https://myapp.com/callback', client_id: 'my-app', code_verifier: codeVerifier, // Prouve qu'on est l'initiateur }),});
const { access_token, refresh_token, id_token } = await tokenResponse.json();Pour le machine-to-machine (pas d'utilisateur)
Section intitulée « Pour le machine-to-machine (pas d'utilisateur) »Usage : microservices, jobs batch, APIs internes
Ce flow se distingue des autres par l'absence de redirection et de consentement : le client s'authentifie lui-même auprès de l'Authorization Server, qui lui remet un jeton portant sa propre identité. La conséquence à retenir est qu'il n'y a pas de refresh token ici, et c'est voulu. Un client capable de se réauthentifier à tout moment avec ses identifiants n'a aucun besoin d'en conserver un, et un refresh token supplémentaire ne ferait qu'agrandir la surface de vol.
# Le client s'authentifie directement avec ses credentials ;# le secret est lu dans un fichier en mode 600, jamais tapé sur la ligne de commandecurl -X POST https://auth.example.com/token \ -d "grant_type=client_credentials" \ -d "client_id=api-backend" \ --data-urlencode "client_secret@secret-api-backend.txt" \ -d "scope=read:orders"La réponse porte l'access token et sa durée en secondes, ici 5 minutes, et aucun refresh token :
{ "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "Bearer", "expires_in": 300}La forme nom@fichier de --data-urlencode garde le secret hors de l'historique du shell et de la liste des processus ; le geste complet, rejoué sur Keycloak, est dans Protéger une API par jetons d'accès.
À ne plus utiliser
Section intitulée « À ne plus utiliser »Ces deux flows sont écartés par la RFC 9700 parce que leur faille est structurelle, pas parce qu'une meilleure alternative est apparue. Implicit livrait le jeton directement dans le fragment d'URL, donc dans l'historique du navigateur et dans les journaux d'accès des serveurs intermédiaires. ROPC obligeait l'application à manipuler le mot de passe de l'utilisateur, ce qui annule tout l'intérêt d'OAuth et empêche l'authentification multifacteur. Si vous en trouvez encore dans votre code, le remplacement passe dans les deux cas par Authorization Code + PKCE.
| Flow | Problème | Alternative |
|---|---|---|
| Implicit | Token exposé dans l'URL (fragment), pas de refresh token | Authorization Code + PKCE |
| Resource Owner Password (ROPC) | L'application voit le mot de passe | Authorization Code + PKCE |
Paramètres de sécurité obligatoires
Section intitulée « Paramètres de sécurité obligatoires »Ces trois paramètres protègent contre trois attaques distinctes et ne se remplacent pas. Le state défend la session du navigateur contre un déclenchement forcé du flow par un tiers, le code_challenge défend le code d'autorisation contre l'interception, le nonce défend l'ID token OIDC contre le rejeu. Une erreur fréquente consiste à croire que PKCE dispense du state : ce n'est pas le cas, les deux couvrent des étapes différentes du parcours et les bonnes pratiques de sécurité OAuth demandent les deux.
| Paramètre | Rôle | Quand l'utiliser |
|---|---|---|
state | Anti-CSRF, lie la requête à la session | Toujours |
code_challenge | PKCE, protège contre l'interception | Toujours pour un client public (RFC 9700), recommandé pour les autres |
nonce | Anti-replay pour OIDC | Avec OIDC |
Validation du state
Section intitulée « Validation du state »Le point à retenir de ce code n'est pas la génération mais la comparaison au retour. Un state généré puis jamais vérifié ne sert à rien, et c'est l'oubli le plus courant en revue de code. Notez également le stockage : sessionStorage est cloisonné par onglet, ce qui suffit ici car le flow se déroule dans un seul onglet ; avec localStorage, deux connexions lancées en parallèle s'écraseraient mutuellement.
// Avant la redirectionconst state = crypto.randomBytes(16).toString('hex');sessionStorage.setItem('oauth_state', state);
// Au retour du callbackconst returnedState = new URLSearchParams(location.search).get('state');const savedState = sessionStorage.getItem('oauth_state');
if (returnedState !== savedState) { throw new Error('State mismatch - possible CSRF attack');}Gestion des tokens
Section intitulée « Gestion des tokens »Un access token vit 5 à 15 minutes (fourchette de ce site, aucune norme ne la fixe) et un refresh token le renouvelle sans nouvelle authentification. La rotation des refresh tokens, ou un jeton lié par DPoP ou mTLS, est exigée par la RFC 9700 pour un client public. La révocation (RFC 7009) et l'introspection (RFC 7662) coupent ou vérifient un jeton avant son expiration, et le stockage côté navigateur (jamais de refresh token en localStorage) conditionne l'effet d'une faille XSS. Tout le cycle de vie est dans la leçon Jetons et sessions, et son observation curl sur Keycloak dans Observer le cycle de vie des jetons dans Keycloak.
Vulnérabilités classiques
Section intitulée « Vulnérabilités classiques »Aucune de ces six failles n'exploite une faiblesse cryptographique : toutes viennent d'une configuration ou d'une implémentation incomplète. C'est la bonne nouvelle, elles se corrigent sans changer de bibliothèque. Les deux premières lignes sont les plus graves parce qu'elles mènent directement à un vol de jeton ; les deux dernières sont les plus répandues parce qu'elles résultent d'un choix de confort, une durée de vie allongée ou un scope élargi pour « éviter les problèmes ».
| Vulnérabilité | Cause | Protection |
|---|---|---|
| Authorization code interception | Pas de PKCE | PKCE obligatoire |
| Open redirect | Redirect URI avec wildcard | Exact match |
| Token leak via referer | Token dans l'URL | Ne pas mettre de token en query string |
| CSRF | Pas de state | Toujours envoyer et valider state |
| Token trop long | Durée de vie excessive | Access token de quelques minutes (5-15 min), refresh token avec rotation |
| Scope trop large | Permissions excessives | Moindre privilège |
OAuth 2.0 vs OAuth 2.1
Section intitulée « OAuth 2.0 vs OAuth 2.1 »OAuth 2.1 est encore un brouillon (Internet-Draft de l'IETF), pas un standard publié. Il ne crée aucun mécanisme nouveau : il consolide en un seul document des bonnes pratiques déjà publiées, et omet les éléments jugés dangereux. Le texte qui fait foi aujourd'hui est la RFC 9700 (BCP 240, janvier 2025), qui impose déjà l'essentiel. Le tableau ci-dessous se lit donc comme une liste de choix qui étaient facultatifs et deviennent des exigences, pas comme une migration technique.
| Point | OAuth 2.0 (RFC 6749) | RFC 9700 (publiée) | Brouillon OAuth 2.1 |
|---|---|---|---|
| PKCE | Extension séparée (RFC 7636) | Obligatoire pour les clients publics, recommandé pour les autres | Exigé dans le flow Authorization Code |
| Implicit flow | Disponible | À éviter (« SHOULD NOT ») | Omis |
| ROPC | Disponible | Interdit (« MUST NOT ») | Omis |
| Redirect URI | Matching flexible possible | Comparaison exacte | Comparaison exacte de chaîne |
| Rotation des refresh tokens | Facultative | Rotation ou jeton lié au client, pour les clients publics | Même principe |
Erreurs fréquentes
Section intitulée « Erreurs fréquentes »Ces cinq erreurs se rencontrent surtout sur les clients publics, c'est-à-dire les applications qui s'exécutent chez l'utilisateur et ne peuvent donc garder aucun secret : une SPA, une application mobile, un outil en ligne de commande. La deuxième ligne est la plus révélatrice d'une incompréhension du modèle : un client_secret embarqué dans du JavaScript livré au navigateur n'est pas un secret, il est lisible par tout visiteur. La réponse n'est pas de l'obfusquer mais de déclarer le client comme public et de s'appuyer sur PKCE.
| Erreur | Conséquence | Solution |
|---|---|---|
| Pas de PKCE sur client public | Vulnérable à l'interception | PKCE obligatoire |
client_secret côté client (SPA) | Secret exposé | Client public sans secret, utiliser PKCE |
| Refresh token stocké en localStorage | XSS = vol | Cookie HttpOnly ou mémoire + refresh silencieux |
Pas de validation du state | CSRF possible | Générer et valider un state aléatoire |
| Access token de 24h | 24h d'accès post-compromission | Tokens courts (5-15 min) + refresh |
À retenir
Section intitulée « À retenir »Contrôle de connaissances
Section intitulée « Contrôle de connaissances »Six questions tirées sur huit : les acteurs d'OAuth 2, le danger d'un redirect URI à joker, ce que vérifie PKCE, le rôle de state, le flow client credentials et ce que dit la RFC 9700 des flows écartés.
Contrôle de connaissances
Validez vos connaissances avec ce quiz interactif
Informations
- Le chronomètre démarre au clic sur Démarrer
- Questions à choix multiples, vrai/faux et réponses courtes
- Vous pouvez naviguer entre les questions
- Les résultats détaillés sont affichés à la fin
Lance le quiz et démarre le chronomètre
Vérification
(0/0)Score par domaine
Un domaine regroupe plusieurs guides et reçoit plusieurs questions : son score est plus fiable qu'un score par thème, qui porte souvent sur une seule question.
Bilan
Points forts
Aucun sujet réussi à 80 % sur au moins deux questions pour l’instant.
Points faibles
Aucun sujet sous le seuil de réussite.
À faire ensuite
Des indices pour retenter votre chance ?
Continuer le cours : OpenID Connect (OIDC) : authentification moderne et SSO
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications