Authorization

OAuth 2.0

OAuth 2.0 est un framework d'autorisation déléguée, et non d'authentification. Il permet à un utilisateur d'accorder à une application un accès limité à ses données sans partager son mot de passe.

intermediate16 min readUpdated 16 sept. 2026
authorize.ts
ts
// authorize.ts
import { randomBytes, createHash } from "node:crypto";

const verifier = randomBytes(32).toString("base64url");
const challenge = createHash("sha256").update(verifier).digest("base64url");

const url = new URL("https://auth.example.com/authorize");
url.search = new URLSearchParams({
  response_type: "code",
  client_id: "web-app",
  redirect_uri: "https://app.example.com/callback",
  scope: "openid email profile offline_access",
  state: randomBytes(16).toString("base64url"),
  code_challenge: challenge,
  code_challenge_method: "S256",
}).toString();

console.log(url.toString());
Première spec
2012
RFCs cœurs
RFC 6749 / 6750
RFC PKCE
RFC 7636
Rôles
4
Grant moderne
Authorization Code + PKCE
Couche d'identité
OpenID Connect

Pourquoi c'est important

À quoi sert OAuth

Accès délégué

Un utilisateur accorde à une application un accès limité et révocable à ses données sans jamais transmettre de mot de passe ou d'identifiant à long terme.

Permissions par scopes

Chaque demande d'accès spécifie des scopes explicites, afin que le client reçoive exactement l'accès consenti par l'utilisateur, et rien de plus.

Un standard universel

Le même flux fonctionne avec Google, GitHub, Okta ou votre propre serveur, ce qui signifie un seul modèle d'intégration pour chaque fournisseur d'identité.

Le tableau complet

Les quatre rôles en une image

Un propriétaire de ressource accorde à un client l'accès à un serveur de ressources, via un serveur d'autorisation qui émet des tokens.

Resource owner

Grant

L'utilisateur qui possède les données et décide quelle application peut y accéder, et pour quoi faire.

Client

Request

L'application qui demande l'accès, identifiée par un client id et, lorsqu'elle peut en conserver un, un secret.

Authorization server

Broker

Authentifie l'utilisateur, gère le consentement et émet les tokens d'accès, de rafraîchissement et d'identité.

Resource server

Host

L'API qui détient les données protégées et ne les renvoie qu'après avoir accepté un access token valide.

HTML5 en un coup d'oeil

Les composants clés

Authorize endpoint

La redirection du navigateur où l'utilisateur s'authentifie et donne son consentement.

Authorization code

Un code à courte durée de vie et à usage unique renvoyé à la redirect URI du client.

Token endpoint

Un appel POST en back-channel qui échange un code ou un refresh token contre des tokens.

Redirect URI

Le callback enregistré, vérifié strictement pour empêcher le vol de code.

Resource server

L'API qui accepte un bearer access token et renvoie les données.

Scopes

Permissions délimitées par des espaces qui restreignent les actions possibles d'un token.

Flux

Authorization Code + PKCE

Le flux par défaut pour le web, le mobile et les applications single-page. Deux redirections et un échange en back-channel.

  1. 1

    Redirection avec challenge

    Le client redirige le navigateur vers /authorize avec son client id, sa redirect URI, ses scopes, un état (state) et un code_challenge PKCE haché.

  2. 2

    Consentement de l'utilisateur

    Le serveur d'autorisation authentifie l'utilisateur et affiche les scopes demandés. L'utilisateur approuve ou refuse.

  3. 3

    Retour avec un code

    Le navigateur revient vers la redirect URI enregistrée avec un code d'autorisation à usage unique et l'état original.

  4. 4

    Échange du code et du verifier

    Le client envoie via POST le code et le code_verifier original au token endpoint et reçoit les tokens d'accès, de rafraîchissement et d'ID.

  5. 5

    Appel au serveur de ressources

    Le client envoie l'access token comme identifiant Bearer lors des requêtes API. Le serveur de ressources le valide et applique les scopes.

  6. 6

    Rafraîchissement à l'expiration

    Lorsque l'access token expire, le client échange le refresh token contre une nouvelle paire, puis révoque les tokens lors de la déconnexion.

Un bref aperçu

Des requêtes signées au PKCE

  1. 2007

    OAuth 1.0 et requêtes signées

    Une première spécification signait chaque requête avec des secrets partagés, ce qui était sécurisé mais fastidieux pour les clients et les fournisseurs.

    07
  2. 2012

    OAuth 2.0 remplace les signatures

    La RFC 6749 introduit les bearer tokens et le modèle basé sur les rôles qui définit encore le protocole aujourd'hui.

    12
  3. 2014

    OpenID Connect ajoute l'identité

    OIDC ajoute un ID token et un endpoint userinfo par-dessus OAuth, permettant une véritable connexion (sign-in).

    14
  4. 2015

    PKCE protège les clients publics

    La RFC 7636 comble la faille d'interception de code pour les applications incapables de garder un client secret.

    15
  5. 2021

    OAuth 2.1 consolide les acquis

    Le draft déprécie les grants implicit et password et rend PKCE obligatoire, codifiant ainsi les meilleures pratiques.

    21

Le guide complet

OAuth 2.0: Tout ce que vous devez savoir

Ce qu’est OAuth 2.0, et ce qu’il n’est pas

OAuth 2.0 est un framework d’autorisation. Il permet à un utilisateur d’accorder à une application tierce un accès limité à ses ressources sans avoir à partager son mot de passe. Le résultat d’un flux réussi est un access token, et ce token décrit ce que le client est autorisé à faire, et non nécessairement qui est l’utilisateur.

C’est cette distinction qui induit constamment les gens en erreur. Le “Sign in with Google” est une combinaison d’OAuth 2.0 et d’OpenID Connect. La partie OAuth permet d’obtenir l’accès aux API de Google ; la partie OpenID Connect renvoie un ID token qui identifie réellement l’utilisateur. Si vous n’implémentez que OAuth et que vous traitez l’access token comme une preuve d’identité, vous avez mis en place un accès délégué en l’appelant authentification, ce qui constitue une erreur de catégorie avec des conséquences sur la sécurité.

Le protocole a été conçu pour répondre à un problème spécifique : permettre à un site d’impression photo de lire vos photos depuis un fournisseur de stockage sans jamais voir votre mot de passe de stockage. Gardez cette origine à l’esprit et les choix de conception deviendront logiques.

OpenID Connect : l’identité au premier plan

OpenID Connect est une fine couche d’identité superposée à OAuth 2.0. Elle ajoute le scope openid, un endpoint userinfo standard et, plus important encore, un ID token : un JWT dont les claims décrivent l’événement d’authentification.

{
  "iss": "https://accounts.example.com",
  "sub": "110169484474386276334",
  "aud": "web-app",
  "exp": 1760000000,
  "iat": 1759999100,
  "email": "[email protected]",
  "email_verified": true,
  "nonce": "n-0S6_WzA2Mj"
}

L’ID token est destiné au client, et non à l’API. Ne l’envoyez jamais à un serveur de ressources en guise d’access token. Vérifiez sa signature, iss, aud, exp et nonce avant de lui faire confiance, et utilisez sub, et non l’email, comme identifiant utilisateur stable. Les adresses email changent et peuvent être réattribuées ; le subject, lui, reste stable pendant toute la durée de vie du compte.

Les quatre rôles

OAuth définit quatre participants, et être précis sur chacun d’eux rend le reste du protocole évident.

  • Resource owner (Propriétaire de la ressource) — l’utilisateur qui possède les données et accorde l’accès.
  • Client — l’application qui demande l’accès, par exemple votre application web.
  • Authorization server (Serveur d’autorisation) — délivre les tokens après avoir authentifié l’utilisateur et obtenu son consentement.
  • Resource server (Serveur de ressources) — l’API qui accepte le token d’accès et renvoie les données.

Votre application web est le client. Google est l’authorization server. Les API de Google sont le resource server. L’utilisateur est le resource owner. Un seul fournisseur remplit souvent les deux rôles de serveur, c’est pourquoi cette distinction peut sembler théorique jusqu’à ce que vous construisiez la vôtre.

Types de grant (Grant types)

Un “grant type” est la méthode utilisée par un client pour obtenir un jeton. Quatre d’entre eux sont importants aujourd’hui.

  • Authorization Code + PKCE — le choix par défaut pour le web, le mobile et les applications mono-page. L’utilisateur s’authentifie auprès du serveur d’autorisation, et le client échange un code à courte durée de vie contre des jetons.
  • Client Credentials — pour les communications machine-to-machine. Aucun utilisateur n’est impliqué ; le client s’authentifie avec ses propres identifiants.
  • Device Code — pour les appareils ayant des contraintes de saisie, comme les téléviseurs et les outils CLI.
  • Refresh Token — ce n’est pas un moyen de se connecter, mais la méthode standard pour renouveler un jeton d’accès sans effectuer une nouvelle redirection.

Deux types de grants sont pratiquement obsolètes. Le flux implicit renvoyait les jetons directement dans le fragment de l’URL, les exposant ainsi à l’historique et aux referrers. Le grant password demandait au client de gérer le mot de passe de l’utilisateur, ce qui va à l’encontre même du principe d’OAuth. OAuth 2.1 supprime ces deux méthodes, et aucun nouveau système ne devrait les utiliser.

Authorization Code + PKCE

C’est le flux à maîtriser. Le client redirige l’utilisateur vers le serveur d’autorisation avec un challenge haché, l’utilisateur donne son consentement, le serveur redirige ensuite vers le client avec un code à usage unique, et le client échange ce code ainsi que le vérificateur original contre des tokens.

Le code est inutile sans le vérificateur ; ainsi, un attaquant qui intercepterait la redirection ne pourrait pas finaliser l’échange. C’est ce qui rend PKCE sécurisé, même pour les clients publics comme les applications mobiles et les applications monopage (SPA), qui sont incapables de conserver un secret.

Ce flux comprend deux redirections via le navigateur et une requête en back-channel. Les redirections sont visibles et peuvent être manipulées, tandis que l’échange est un POST direct de serveur à serveur qu’un attaquant ne peut pas observer. L’idée même de cette architecture est de maintenir les données sensibles sur le back-channel.

L’URI de redirection et le state

L’URI de redirection (redirect URI) est l’adresse vers laquelle le serveur d’autorisation renvoie l’utilisateur. Elle doit être enregistrée avec précision et correspondre exactement. Un rapprochement approximatif est la source de vulnérabilités de type “open-redirect” qui permettent de fuiter des codes d’autorisation vers des hôtes contrôlés par un attaquant. N’acceptez jamais d’URI de redirection provenant d’un paramètre de requête et n’autorisez pas les sous-domaines avec des caractères génériques (wildcards).

Le paramètre state est une valeur opaque générée par le client et vérifiée lors du retour du callback. Il protège le point de terminaison du callback contre les attaques CSRF. Un attaquant qui tromperait une victime pour qu’elle complète un flux avec le code de l’attaquant échouera à la vérification du state.

const state = randomBytes(16).toString("base64url");
session.oauthState = state;

// later, in the callback:
if (query.state !== session.oauthState) {
  throw new Error("state_mismatch");
}

Pour OIDC, ajoutez un nonce et vérifiez-le à l’intérieur du jeton ID (ID token). Le state protège le callback du client contre le CSRF ; le nonce lie le jeton ID à cette requête spécifique et bloque le replay.

Le PKCE en détail

Le PKCE (Proof Key for Code Exchange), défini par la RFC 7636, est désormais obligatoire pour les clients publics. Il repose sur trois valeurs. Le client génère un code_verifier aléatoire, en dérive un code_challenge via un hachage SHA-256, envoie le challenge lors de la requête d’autorisation, puis envoie le verifier lors de la requête de jeton. Le serveur hache ensuite le verifier et compare les deux valeurs.

import { randomBytes, createHash } from "node:crypto";

const verifier = randomBytes(32).toString("base64url");
const challenge = createHash("sha256").update(verifier).digest("base64url");

Utilisez toujours la méthode S256. La méthode plain n’existe que pour des raisons de compatibilité et vide le processus de son sens, puisqu’un attaquant qui intercepte le challenge voit également le verifier. Stockez le verifier dans la session de l’utilisateur afin que le callback puisse le récupérer, et supprimez-le après une seule utilisation.

Construire la requête d’autorisation

La requête d’autorisation est une redirection du navigateur, il s’agit donc d’une requête GET avec des paramètres de requête. L’utilisateur voit l’écran de consentement du fournisseur, et non votre application.

export function buildAuthorizeUrl(state: string, challenge: string) {
  const url = new URL("https://auth.example.com/authorize");
  url.search = new URLSearchParams({
    response_type: "code",
    client_id: "web-app",
    redirect_uri: "https://app.example.com/callback",
    scope: "openid email profile offline_access",
    state,
    code_challenge: challenge,
    code_challenge_method: "S256",
  }).toString();
  return url.toString();
}

response_type=code sélectionne le flux de code d’autorisation (authorization code flow). Le scope openid transforme la requête en une requête OIDC. offline_access est la convention courante pour demander un refresh token, et certains fournisseurs le conditionnent à une invite de consentement supplémentaire.

Échange de jetons

Le callback transmet code et state. Après avoir vérifié l’état (state), le client envoie le code via une requête POST au point de terminaison (endpoint) des jetons. Il s’agit d’un appel en arrière-plan (back-channel) depuis votre serveur, et non d’une redirection du navigateur.

export async function exchangeCode(code: string, verifier: string) {
  const res = 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,
      redirect_uri: "https://app.example.com/callback",
      client_id: "web-app",
      code_verifier: verifier,
    }),
  });

  if (!res.ok) throw new Error("token_exchange_failed");
  return res.json();
}

La réponse contient access_token, token_type, expires_in, généralement refresh_token, et pour OIDC, un id_token. Un client confidentiel, capable de conserver un secret, s’authentifie également lors de cette requête avec client_secret ou, idéalement, une assertion de clé privée. Un client public s’appuie uniquement sur PKCE.

Jetons d’accès, de rafraîchissement et d’identité (ID tokens)

Ces trois jetons ont des rôles distincts, et les confondre peut entraîner des bugs subtils.

  • Access token — présenté au serveur de ressources. Il s’agit souvent d’un JWT, mais la spécification exige seulement qu’il soit opaque pour le client. Il peut s’agir d’une chaîne aléatoire que le serveur recherche en base.
  • Refresh token — présenté uniquement au serveur d’autorisation pour obtenir un nouvel access token. Il a une durée de vie longue et est extrêmement sensible.
  • ID token — un JWT OIDC qui indique au client qui vient de se connecter. Il n’est jamais envoyé à une API.

Considérez l’access token comme un titre porteur : toute personne le détenant peut l’utiliser. Maintenez des durées de vie courtes, demandez les scopes les plus restreints possibles, et laissez le refresh token gérer la relation à long terme. Le format du jeton lui-même est détaillé dans le guide JWT.

Scopes et consentement

Les scopes expriment ce que le client demande. Le serveur d’autorisation les présente à l’utilisateur sous forme d’écran de consentement et encode le sous-ensemble accordé dans l’access token.

Demandez le minimum. Une application de calendrier qui demande un accès complet à la boîte mail fera fuir les utilisateurs et augmentera l’impact potentiel d’une faille de sécurité. Les fournisseurs publient également des scopes réservés : openid est requis pour OIDC, et offline_access contrôle généralement les refresh tokens.

Les scopes ne sont pas des rôles. Un scope décrit une capacité demandée par le client pour cet accord ; un rôle décrit ce que l’utilisateur représente au sein de votre système. Effectuez la correspondance entre les deux sur votre serveur, et ne supposez jamais que les scopes d’un fournisseur disent quoi que ce soit sur votre propre modèle d’autorisation. Pour cet aspect du problème, consultez la section RBAC.

Appeler le serveur de ressources

Une fois le jeton d’accès obtenu, les appels à l’API le transmettent dans l’en-tête Authorization en utilisant le schéma Bearer.

const res = await fetch("https://api.example.com/me", {
  headers: { authorization: `Bearer ${accessToken}` },
});

Le serveur de ressources valide le jeton, soit en vérifiant le JWT localement, soit par introspection, vérifie le scope, puis renvoie les données. Il ne voit jamais le refresh token ni l’ID token, et doit les rejeter s’il les reçoit.

Introspection et révocation

Tous les jetons d’accès ne sont pas des JWT. Lorsqu’un jeton est opaque, le serveur de ressources demande au serveur d’autorisation s’il est toujours valide via l’introspection de jeton (token introspection), définie par la RFC 7662.

POST /introspect HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic <client credentials>

token=2YotnFZFEjr1zCsicMWpAA

L’introspection retourne active, ainsi que le scope, le sujet et la date d’expiration. Cette méthode est faisant foi mais nécessite un appel réseau ; il est donc conseillé de mettre le résultat en cache pendant quelques secondes.

La révocation, définie par la RFC 7009, permet à un client d’indiquer au serveur d’autorisation d’invalider un jeton, généralement lors de la déconnexion.

await fetch("https://auth.example.com/revoke", {
  method: "POST",
  headers: { "content-type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({ token: refreshToken, client_id: "web-app" }),
});

Révocquez systématiquement les refresh tokens lors de la déconnexion, et gardez à l’esprit que la révocation d’un refresh token n’invalide pas toujours les jetons d’accès déjà émis. C’est la courte durée de vie des jetons d’accès qui permet à la révocation de paraître immédiate.

Machine-to-machine : client credentials

Lorsqu’aucun utilisateur n’est impliqué, le client agit en son nom propre. Il s’authentifie auprès du point de terminaison du jeton (token endpoint) et reçoit un jeton d’accès limité à ses propres permissions.

const res = await fetch("https://auth.example.com/token", {
  method: "POST",
  headers: {
    "content-type": "application/x-www-form-urlencoded",
    authorization: `Basic ${btoa(`${clientId}:${clientSecret}`)}`,
  },
  body: new URLSearchParams({
    grant_type: "client_credentials",
    scope: "reports:read",
  }),
});

Il n’y a pas d’écran de consentement ni de jeton de rafraîchissement (refresh token) ; le service demande simplement un nouveau jeton lorsque l’ancien expire. Privilégiez l’authentification client via JWT à clé privée plutôt qu’un secret partagé lorsque le fournisseur le permet, et renouvelez vos secrets régulièrement. C’est le même cas d’usage que celui des clés API à longue durée de vie, mais avec une expiration et un périmètre (scoping) standardisés, comme détaillé dans la section API Keys.

Quand ne pas utiliser OAuth

OAuth sert à déléguer l’accès à un tiers. Si votre propre application web authentifie ses propres utilisateurs auprès de son propre backend, vous n’avez pas besoin d’OAuth. Un cookie de session, ou un JWT first-party que vous émettez vous-même, est plus simple et plus facile à sécuriser. Placer un serveur d’autorisation entre votre formulaire de connexion et votre base de données apporte de la complexité, pas de la sécurité.

Tournez-vous vers OAuth lorsque vous intégrez un fournisseur externe, que vous agissez en tant que fournisseur pour des clients tiers, ou que vous avez besoin d’un standard pour l’accès machine-to-machine. Sinon, commencez par l’Authentification par Session et n’ajoutez OAuth que lorsqu’un réel besoin de délégation apparaît. Comme chaque flux ici repose sur des redirections et des headers, le guide HTTP est un compagnon utile.

Pièges courants

  • Implicit flow — les jetons dans le fragment de l’URL fuitent via l’historique, les logs et les referrers. Utilisez l’Authorization Code avec PKCE.
  • Jetons dans le localStorage — une faille XSS se transforme alors en prise de contrôle de compte. Conservez les jetons dans des cookies httpOnly ou des sessions côté serveur.
  • Redirections ouvertes (Open redirects) — une correspondance trop permissive des URI de redirection permet à un attaquant de voler des codes. Faites correspondre l’URI enregistrée exactement.
  • Omission du paramètre state — sans lui, le callback est vulnérable aux attaques CSRF.
  • Scopes trop larges — demander tous les accès rend le consentement insignifiant et aggrave l’impact des failles de sécurité.
  • Access tokens à longue durée de vie — ils ne peuvent pas être révoqués rapidement. Gardez-les courts et effectuez une rotation des refresh tokens.
  • Utilisation de l’ID token comme identifiant d’API — il est destiné au client, et non au serveur de ressources.

Choisir le flux approprié

La plupart des décisions d’intégration se résument à deux questions : y a-t-il un utilisateur et le client peut-il garder un secret ?

  • Un utilisateur est présent, le client est public (SPA, application mobile, application desktop) : Authorization Code avec PKCE.
  • Un utilisateur est présent, le client est confidentiel (application web avec SSR) : Authorization Code avec PKCE plus authentification du client.
  • Pas d’utilisateur, le client est un service (cron job, microservice) : Client Credentials.
  • L’appareil n’a ni navigateur ni clavier (TV, CLI) : Device Code.

Tout le reste est obsolète. Si un fournisseur ne documente que le flux implicite, considérez cela comme un signal d’alarme et vérifiez si PKCE est disponible.

Le pattern backend-for-frontend

L’endroit le plus sûr pour stocker des tokens est un serveur que vous contrôlez. Le pattern backend-for-frontend place un serveur léger entre le navigateur et le serveur d’autorisation : le navigateur reçoit un cookie de session, et le serveur conserve les access et refresh tokens.

app.get("/callback", async (req, res) => {
  if (req.query.state !== req.session.oauthState) {
    return res.status(400).json({ error: "state_mismatch" });
  }

  const tokens = await exchangeCode(
    String(req.query.code),
    req.session.codeVerifier,
  );

  req.session.tokens = tokens;
  delete req.session.oauthState;
  delete req.session.codeVerifier;

  res.redirect("/dashboard");
});

Le navigateur ne voit jamais de token, ce qui empêche le vol via XSS, et le serveur peut effectuer le rafraîchissement silencieusement. Le compromis réside dans l’ajout d’un saut réseau et d’un serveur à gérer, ce qui est généralement bien moins coûteux que l’incident que vous évitez.

Clients natifs et mobiles

Les applications natives ne peuvent pas conserver de secret client, l’utilisation de PKCE est donc indispensable. Elles sont également confrontées à un problème de redirection : un schéma personnalisé tel que myapp://callback peut être usurpé par une application malveillante. La solution moderne consiste à utiliser un onglet du navigateur système, soit via l’API de session d’authentification de la plateforme, soit via un navigateur intégré qui partage les cookies avec le système.

N’utilisez jamais de WebView intégré pour OAuth. L’application pourrait lire le mot de passe de l’utilisateur, ce qui briserait la frontière de confiance que le protocole entier vise à protéger. Ouvrez le navigateur système, recevez le callback et conservez les tokens dans le stockage sécurisé de la plateforme.

Héberger votre propre serveur d’autorisation

Il n’est pas nécessaire d’en construire un. Des serveurs établis tels que Keycloak, Ory Hydra et les fournisseurs d’identité cloud implémentent déjà le protocole, les écrans de consentement, la gestion des clés et le stockage des tokens pour vous. Développer votre propre solution est un projet de plusieurs mois dont les modes de défaillance sont tous critiques pour la sécurité.

Si vous choisissez tout de même d’héberger le vôtre, vos responsabilités minimales sont : la correspondance exacte des URI de redirection, l’application du PKCE, des durées de vie courtes pour les access-tokens, la rotation des refresh-tokens avec détection de réutilisation, un endpoint JWKS et la révocation. Oubliez l’un de ces éléments et vous aurez déployé une vulnérabilité, pas une fonctionnalité.

Tester une intégration OAuth

Les tests de bout en bout (E2E) pour OAuth sont lents et fragiles car ils dépendent d’un fournisseur réel et d’un navigateur réel. Privilégiez plutôt des tests par couches.

  • Effectuez des tests unitaires sur la construction des URL, la dérivation PKCE et la comparaison d’état (state) en tant que fonctions pures.
  • Effectuez des tests d’intégration pour l’échange de jetons (token exchange) via un serveur d’autorisation simulé (mock) qui renvoie des jetons prédéfinis.
  • Conservez un seul test de fumée (smoke test) contre le fournisseur réel, marqué de manière à ne pas s’exécuter à chaque commit.
test("builds an authorize URL with PKCE", () => {
  const url = new URL(buildAuthorizeUrl("state-1", "challenge-1"));
  expect(url.searchParams.get("response_type")).toBe("code");
  expect(url.searchParams.get("code_challenge_method")).toBe("S256");
  expect(url.searchParams.get("state")).toBe("state-1");
});

Le callback est l’endroit où se cachent les bugs, testez donc explicitement les scénarios d’échec : un état discordant, un code rejoué et un jeton expiré doivent chacun produire une erreur claire plutôt qu’une session partiellement authentifiée.

Déconnexion et authentification unique (SSO)

Se déconnecter de votre application n’est pas la même chose que de se déconnecter du fournisseur d’identité. Une déconnexion complète effectue trois actions : elle détruit la session locale, révoque le refresh token au niveau du serveur d’autorisation et, pour l’authentification unique (single sign-on), met fin à la session du fournisseur afin que la connexion suivante ne la réutilise pas silencieusement.

app.post("/logout", async (req, res) => {
  if (req.session.tokens?.refresh_token) {
    await revoke(req.session.tokens.refresh_token);
  }
  req.session.destroy(() => res.redirect("/"));
});

L’authentification unique découle du même mécanisme : une fois qu’un utilisateur possède une session chez le fournisseur, les clients suivants reçoivent un code sans avoir à saisir à nouveau leur mot de passe. C’est pratique, et c’est aussi pourquoi la déconnexion sur un ordinateur partagé est plus cruciale qu’on ne le pense.

Rotation des refresh tokens et détection de réutilisation

OAuth n’impose pas que les refresh tokens soient à usage unique, mais les implémentations les plus robustes utilisent la rotation. Chaque rafraîchissement renvoie un nouveau refresh token et invalide le précédent. Si un ancien token est présenté à nouveau, le serveur sait qu’il a été volé et révoque l’ensemble de la famille de tokens.

export async function refresh(grant: { refresh_token: string; client_id: string }) {
  const stored = await db.refreshToken.findByHash(hash(grant.refresh_token));

  if (!stored || stored.revoked) {
    if (stored) await revokeFamily(stored.familyId);
    throw new Error("invalid_grant");
  }

  await db.refreshToken.revoke(stored.id);
  return issueTokens(stored.userId, stored.familyId);
}

La détection de réutilisation est l’atout majeur : un refresh token volé devient un signal d’alerte plutôt qu’une porte dérobée permanente. Associez cela à une expiration glissante pour qu’un utilisateur actif reste connecté, tandis qu’un token abandonné finisse par expirer.

Bonnes pratiques

  • Utilisez l’Authorization Code avec PKCE pour chaque client orienté utilisateur, y compris les SPA et les applications mobiles.
  • Utilisez les Client Credentials pour les communications machine-to-machine, avec une authentification par clé privée dans la mesure du possible.
  • Enregistrez des redirect URIs exactes et rejetez tout ce qui ne correspond pas caractère par caractère.
  • Envoyez et vérifiez toujours state ; ajoutez et vérifiez un nonce pour OIDC.
  • Demandez le minimum de scopes et considérez l’écran de consentement comme une étape significative.
  • Gardez des access tokens à courte durée de vie, effectuez une rotation des refresh tokens et révoquez-les lors de la déconnexion.
  • Vérifiez la signature de l’ID token, iss, aud, exp et nonce avant de lui faire confiance.
  • Conservez les client secrets et les clés privées dans un secret manager, jamais dans le frontend.
  • Mettez l’introspection en cache brièvement et appuyez-vous sur des durées de vie courtes pour une révocation rapide.

Erreurs courantes

  • Confondre OAuth avec l’authentification sans utiliser OpenID Connect.
  • Déployer le flux implicite (implicit flow) simplement pour éviter une redirection.
  • Stocker les access tokens dans le localStorage et les envoyer à n’importe quelle origine.
  • Inclure le client secret dans une application mono-page (SPA) où n’importe qui peut le lire.
  • Accepter n’importe quelle URI de redirection, ou une URI fournie via un paramètre de requête.
  • Oublier de valider state lors du callback.
  • Demander tous les scopes possibles sans jamais remettre la liste à jour.
  • Supposer que la révocation d’un refresh token invalide instantanément les access tokens.
  • Utiliser l’ID token comme bearer token pour votre API.

Et après ?

OAuth est la méthode permettant d’obtenir des jetons ; le guide sur les JWT explique comment ils sont construits et vérifiés. Si votre application est propriétaire (first-party) et que vous avez besoin d’une révocation immédiate, l’Auth par Session est un point de départ plus simple. Pour les clients machines n’impliquant jamais d’utilisateur, les Clés API présentent l’alternative pour des accès longue durée. Et comme chacun de ces flux transite par le réseau, un rappel sur le HTTP est recommandé.

En pratique

Autoriser, échanger, appeler, rafraîchir

Le cycle de vie complet de l'authorization code en quatre requêtes.

authorize.ts
import { randomBytes, createHash } from "node:crypto";

export function buildAuthorizeUrl(session: { state?: string }) {
  const verifier = randomBytes(32).toString("base64url");
  const challenge = createHash("sha256").update(verifier).digest("base64url");
  const state = randomBytes(16).toString("base64url");

  session.state = state;
  session.codeVerifier = verifier;

  const url = new URL("https://auth.example.com/authorize");
  url.search = new URLSearchParams({
    response_type: "code",
    client_id: "web-app",
    redirect_uri: "https://app.example.com/callback",
    scope: "openid email profile offline_access",
    state,
    code_challenge: challenge,
    code_challenge_method: "S256",
  }).toString();

  return url.toString();
}

Authorization Code + PKCE vs Implicit

PKCE évite que les tokens apparaissent dans l'URL ou l'historique du navigateur, et fonctionne pour les clients publics sans secret.

Préférer
// Browser is redirected, then the backend exchanges the code.
const url = new URL("https://auth.example.com/authorize");
url.search = new URLSearchParams({
  response_type: "code",
  client_id: "web-app",
  redirect_uri: "https://app.example.com/callback",
  scope: "openid email",
  state,
  code_challenge: challenge,
  code_challenge_method: "S256",
}).toString();
Éviter
// Tokens land in the URL fragment where history,
// logs and referrers can leak them.
const url = new URL("https://auth.example.com/authorize");
url.search = new URLSearchParams({
  response_type: "token",
  client_id: "web-app",
  redirect_uri: "https://app.example.com/callback",
  scope: "openid email",
}).toString();

scopes vs rôles

Un scope est ce que le client a demandé pour ce grant. Un rôle est ce que l'utilisateur est au sein de votre système. Ne confondez pas les deux.

Scope
// Requested capability, granted through consent.
const scope = "reports:read reports:export";

if (!req.user.scope.includes("reports:read")) {
  return res.status(403).json({ error: "insufficient_scope" });
}
Rôle
// Position in your own authorization model.
const role = "analyst";

// A provider scope never substitutes for a local
// role check; map scopes to roles on your server.
if (!["analyst", "admin"].includes(req.user.role)) {
  return res.status(403).json({ error: "forbidden" });
}

Compromis

Faut-il utiliser OAuth ?

OAuth est l'outil idéal pour la délégation et l'intégration, mais le mauvais choix pour une connexion propriétaire (first-party).

Strengths

  • Pas de partage de mot de passe

    Les utilisateurs accordent un accès limité via le fournisseur en qui ils ont déjà confiance, et peuvent le révoquer sans changer leurs identifiants.

  • Une intégration, plusieurs fournisseurs

    Le même flux d'authorization code fonctionne partout, donc ajouter un second fournisseur d'identité n'est qu'une question de configuration.

  • Révocable par conception

    Les access et refresh tokens peuvent être révoqués, et la courte durée de vie des access tokens limite l'impact d'une fuite.

Trade-offs

  • Ce n'est pas de l'authentification

    OAuth seul prouve qu'un client peut accéder à une ressource, pas qui est l'utilisateur. Vous avez besoin d'OpenID Connect pour l'identité.

  • Des risques de sécurité réels

    Des redirect URIs trop permissives, l'absence de state ou le stockage de tokens dans le navigateur mènent directement au vol de compte.

  • Le coût de la complexité

    Gérer son propre serveur d'autorisation implique la gestion des clés, des écrans de consentement, du stockage des tokens et des audits de sécurité.

FAQ

Foire aux questions

Keep learning

Related topics from the roadmap.

$ commencer à apprendre

Prêt à apprendre OAuth 2.0 ?

Notre tutoriel interactif vous guide à travers OAuth 2.0 pas à pas — avec des quiz et du vrai code que vous pouvez exécuter dans le navigateur.