Aller au contenu
English
Compte : non connecté, synchronisation possible Mon apprentissage
Selectionner le thème
English

OAuth 2.x : la délégation d'accès aux APIs

Par mis à jour le

Améliorer cette page

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 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.

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

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.

ActeurRôleExemple
Resource OwnerL'entité qui possède les donnéesVous (votre compte GitHub)
ClientL'application qui veut accéder aux donnéesApplication de suivi de commits
Authorization ServerAuthentifie le Resource Owner, délivre les tokensKeycloak, Auth0, GitHub OAuth
Resource ServerAPI qui protège les données, valide les tokensAPI GitHub

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.

Les 4 acteurs OAuth 2.0 : le Resource Owner (l'utilisateur) s'authentifie et consent auprès de l'Authorization Server (Keycloak, Auth0) ; celui-ci émet un token reconnu par le Resource Server (l'API) et remet l'access token au client (l'application), qui le présente à l'API pour l'appeler.(1) authentification+ consentement(2) token(3) access token(4) access token→ appel d'APIResource Owner(utilisateur)Authorization Server(Keycloak, Auth0)Client(application)Resource Server(API)
Source · SVG
Échap pour fermer, + et − pour zoomer, défilement pour se déplacer. 100 %

Les scopes définissent quelles permissions le client demande. L'utilisateur (Resource Owner) peut les accepter ou les refuser.

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.

ServiceScopePermission accordée
GitHubrepoAccès complet aux dépôts privés et publics
GitHubpublic_repoAccès aux seuls dépôts publics
GitHubrepo:statusLecture et écriture des statuts de commit
GoogleprofileLire le profil public
GoogleemailLire l'adresse email
Keycloakoffline_accessObtenir un refresh token

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é.

Le Redirect URI (ou callback URL) est l'URL vers laquelle l'Authorization Server redirige l'utilisateur après l'authentification.

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èglePourquoi
Exact match obligatoireÉvite l'interception du code/token par un attaquant
Enregistré à l'avanceLe serveur ne redirige que vers les URIs connues
HTTPS obligatoireSauf http://localhost pour le développement

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.

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.

  1. Génération du vérifier

    Le client génère un code_verifier aléatoire (32 octets, base64url).

  2. Calcul du challenge

    Le client calcule le challenge : code_challenge = BASE64URL(SHA256(code_verifier)), et déclare la méthode code_challenge_method=S256. La valeur plain, qui transmet le vérifier tel quel, existe encore dans la RFC mais n'offre aucune protection.

  3. Requête d'autorisation

    Le client redirige vers l'Authorization Server avec le code_challenge.

  4. Authentification + consentement

    L'utilisateur s'authentifie et accepte les scopes.

  5. Retour avec le code

    L'Authorization Server redirige vers le client avec un authorization_code.

  6. Échange du code

    Le client envoie le code + code_verifier et reçoit les tokens.

// Étape 1-2 : Génération PKCE
const codeVerifier = crypto.randomBytes(32).toString('base64url');
const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64url');
// Étape 3 : Requête d'autorisation
const 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-CSRF
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
// Rediriger l'utilisateur vers authUrl
// Étape 6 : Échange du code côté callback
const 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();

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ètreRôleQuand l'utiliser
stateAnti-CSRF, lie la requête à la sessionToujours
code_challengePKCE, protège contre l'interceptionToujours pour un client public (RFC 9700), recommandé pour les autres
nonceAnti-replay pour OIDCAvec OIDC

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 redirection
const state = crypto.randomBytes(16).toString('hex');
sessionStorage.setItem('oauth_state', state);
// Au retour du callback
const returnedState = new URLSearchParams(location.search).get('state');
const savedState = sessionStorage.getItem('oauth_state');
if (returnedState !== savedState) {
throw new Error('State mismatch - possible CSRF attack');
}

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.

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éCauseProtection
Authorization code interceptionPas de PKCEPKCE obligatoire
Open redirectRedirect URI avec wildcardExact match
Token leak via refererToken dans l'URLNe pas mettre de token en query string
CSRFPas de stateToujours envoyer et valider state
Token trop longDurée de vie excessiveAccess token de quelques minutes (5-15 min), refresh token avec rotation
Scope trop largePermissions excessivesMoindre privilège

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.

PointOAuth 2.0 (RFC 6749)RFC 9700 (publiée)Brouillon OAuth 2.1
PKCEExtension séparée (RFC 7636)Obligatoire pour les clients publics, recommandé pour les autresExigé dans le flow Authorization Code
Implicit flowDisponibleÀ éviter (« SHOULD NOT »)Omis
ROPCDisponibleInterdit (« MUST NOT »)Omis
Redirect URIMatching flexible possibleComparaison exacteComparaison exacte de chaîne
Rotation des refresh tokensFacultativeRotation ou jeton lié au client, pour les clients publicsMême principe

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.

ErreurConséquenceSolution
Pas de PKCE sur client publicVulnérable à l'interceptionPKCE obligatoire
client_secret côté client (SPA)Secret exposéClient public sans secret, utiliser PKCE
Refresh token stocké en localStorageXSS = volCookie HttpOnly ou mémoire + refresh silencieux
Pas de validation du stateCSRF possibleGénérer et valider un state aléatoire
Access token de 24h24h d'accès post-compromissionTokens courts (5-15 min) + refresh

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

6 questions
6 min.
70% requis

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

Ce projet vous est utile ?

Les guides, formations, quiz et labs restent gratuits. Si vous les utilisez régulièrement, vous pouvez contribuer à leur maintenance et à leur développement.

Devenir membre de soutien (nouvel onglet) Soutenir une fois (nouvel onglet) En savoir plus

Suivez aussi le projet sur LinkedIn