ID token, access token, refresh token : qui fait quoi
ID token, access token et refresh token n’ont pas le même rôle. Voici qui les reçoit, à quoi ils servent et comment éviter de les confondre.
- Identity Security
- OIDC
- OAuth
- JWT
- SSO
Dans le login SSO avec OpenID Connect, l’application redirige l’utilisateur vers l’Identity Provider, récupère un code d’autorisation puis l’échange contre des tokens.
Et c’est souvent là que les ennuis commencent.
L’Identity Provider peut renvoyer un ID token, un access token et éventuellement un refresh token. Trois objets qui arrivent presque au même moment, parlent parfois du même utilisateur et finissent donc facilement rangés mentalement dans la catégorie très précise de « JWT de connexion ».
Sauf qu’ils n’ont ni le même destinataire, ni le même rôle, ni les mêmes règles d’utilisation.
Le problème
Après un login OIDC, quelques raccourcis deviennent tentants :
J’ai un ID token → je l’envoie à l’API
J’ai un access token → je l’utilise comme preuve de login
J’ai un refresh token → je le stocke avec les autres
Les trois mélangent des responsabilités différentes.
La bonne question n’est pas seulement :
Qu’est-ce que contient ce token ?
Il faut d’abord demander :
À qui est-il destiné, et pour quelle opération ?
C’est cette destination qui remet les trois objets à leur place.
Le modèle mental
Je les résume comme ceci :
ID token
→ parle de l’authentification
→ destiné au client OIDC
Access token
→ permet d’appeler une ressource protégée
→ destiné au resource server
Refresh token
→ permet d’obtenir de nouveaux access tokens
→ destiné au serveur d’autorisation
Ou, dans une architecture web :
Identity Provider
/ | \
/ | \
ID token Access token Refresh token
↓ ↓ ↓
Application API Token endpoint
Les trois peuvent être émis dans le même flux. Ils ne sont pas interchangeables.
Comment ça fonctionne
ID token : « qui vient d’être authentifié ? »
L’ID token est l’élément qu’OpenID Connect ajoute à OAuth 2.0 pour permettre au client d’établir l’identité de l’utilisateur authentifié.
La spécification OpenID Connect le définit comme un JWT contenant des claims sur l’authentification. On y retrouve notamment :
{
"iss": "https://idp.example.com",
"sub": "248289761001",
"aud": "my-web-app",
"exp": 1770000000
}
issindique qui a émis le token ;subidentifie le sujet ;auddoit contenir leclient_iddu client auquel l’ID token est destiné ;expindique jusqu’à quand il peut être accepté.
L’ID token sert donc au client OIDC. Ce n’est pas un ticket universel à envoyer à une API simplement parce qu’il contient une identité.
Référence : OpenID Connect Core 1.0.
Access token : « puis-je appeler cette ressource ? »
L’access token vient d’OAuth 2.0. C’est un credential permettant d’appeler une ressource protégée.
Client
↓ Authorization: Bearer <access_token>
Resource Server / API
Le resource server valide le token selon le mécanisme prévu puis décide s’il accepte la requête.
Un access token peut représenter des scopes, des permissions ou d’autres informations d’autorisation. Son format n’est cependant pas imposé par OAuth 2.0 : il peut être autoportant, par exemple sous forme de JWT, ou opaque pour le client.
Donc :
access token ≠ forcément JWT
Dans mon lab Keycloak, c’en est un. Ce n’est pas une propriété générale d’OAuth 2.0.
Référence : RFC 6749.
Refresh token : « donne-moi un nouvel access token »
Un access token a généralement une durée de vie limitée. Le refresh token permet au client d’en demander un nouveau sans relancer immédiatement une authentification interactive.
Son trajet n’est pas :
Refresh token → API
mais :
Client
↓ refresh_token
Token endpoint
↓
Nouvel access token
Le refresh token est un credential sensible. Il doit rester confidentiel et n’est pas destiné au resource server.
Le serveur d’autorisation peut également renvoyer un nouveau refresh token pendant le renouvellement. Dans ce cas, le client remplace l’ancien. Son émission n’est d’ailleurs pas obligatoire : elle dépend du serveur, du client et du flux utilisé.
Exemple concret
Alice se connecte à une application de notes de frais via l’Identity Provider de l’entreprise.
À la fin du flux OIDC :
ID token
→ le client traite le résultat de l’authentification d’Alice
Access token
→ l’application appelle l’API de notes de frais
Refresh token
→ l’application demande un nouvel access token quand nécessaire
L’appel API ressemble à ceci :
GET /api/expenses
Authorization: Bearer <access_token>
L’API ne reçoit pas l’ID token à la place, et encore moins le refresh token.
Trois tokens, trois destinations. Le système devient beaucoup plus simple dès qu’on arrête de les considérer comme trois variantes du même objet.
Dans mon lab
Dans mon Identity Security Lab, cette séparation existe réellement dans le code avec Keycloak, Next.js, Auth.js et une API Express.
Après le login OIDC, Auth.js récupère auprès de Keycloak :
id_token
access_token
refresh_token
L’access token part vers l’API
C’est l’access token Keycloak qui accompagne les appels vers l’API Express.
Dans le lab, il s’agit d’un JWT contenant notamment iss, sub, aud, exp et realm_access.roles. L’API vérifie sa signature avec le JWKS de Keycloak ainsi que l’issuer, l’audience et l’expiration avant d’utiliser les claims.
Son audience est configurée pour l’API identity-lab-api.
C’est important : un JWT peut être correctement signé tout en étant destiné à une autre ressource.
L’ID token reste côté client
Je conserve aussi l’ID token dans Auth.js, mais pas pour appeler l’API.
Dans mon implémentation, il sert notamment au logout SSO comme id_token_hint lorsque l’application termine également la session Keycloak.
Le refresh token ne sert qu’au renouvellement
Le refresh token est conservé par Auth.js et n’est pas exposé dans l’objet de session renvoyé au navigateur.
Quand l’access token approche de son expiration, le serveur Next.js appelle le token endpoint de Keycloak avec le refresh token, le client_id et le client_secret.
Le renouvellement est déclenché environ 30 secondes avant l’expiration. Cette marge évite de commencer un appel avec un token encore valide au moment du rendu mais sur le point d’expirer.
Si Keycloak renvoie un nouveau refresh token, le code l’utilise. Sinon, il conserve le précédent.
Le navigateur n’a pas besoin de tout voir
L’access token est nécessaire au code serveur pour appeler l’API, mais la route /api/auth/session le retire avant de renvoyer la session au navigateur.
La frontière est donc volontairement nette :
Navigateur
↓ session applicative
Next.js / Auth.js
├─ ID token
├─ access token
└─ refresh token
API Express
↑
access token uniquement
Le frontend serveur décode aussi l’access token pour afficher certains rôles, sans vérifier sa signature. C’est acceptable uniquement parce que ce résultat ne prend aucune décision de sécurité.
La vraie confiance commence dans l’API Express, qui valide le token avant d’appliquer le RBAC.
C’est une distinction utile :
Lire un JWT
≠
valider un JWT
≠
autoriser une action
Ce qui peut mal tourner
Envoyer l’ID token à l’API
Une API peut recevoir un JWT parfaitement formé et le refuser pour mauvaise audience.
C’est normal si on lui envoie un ID token : son aud vise normalement le client OIDC, pas le resource server.
Premier réflexe de diagnostic :
Quel token ai-je envoyé, et pour qui a-t-il été émis ?
Utiliser l’access token comme contrat d’identité du frontend
Décoder un access token et dépendre de sa structure peut fonctionner avec un fournisseur précis, jusqu’au jour où son format ou ses claims changent.
OAuth 2.0 ne garantit pas qu’un access token soit un JWT. OpenID Connect fournit justement l’ID token pour le contexte d’authentification du client.
Exposer le refresh token
Un refresh token permet d’obtenir de nouveaux access tokens. Le mettre dans du stockage accessible au JavaScript sans nécessité, le journaliser ou l’envoyer à une API métier augmente inutilement l’impact de sa compromission.
Sa destination normale reste le token endpoint.
Confondre expiration du token et fin de session
Un access token peut expirer alors que la session utilisateur existe toujours et qu’un renouvellement reste possible.
Inversement, le refresh token ou la session SSO peut devenir invalide et imposer un nouveau login.
Access token expiré
≠
utilisateur forcément déconnecté
Encore une séparation de cycle de vie. L’identité adore ça. Les bugs aussi.
À retenir
- ID token : destiné au client OIDC, il contient des claims sur l’authentification de l’utilisateur.
- Access token : destiné au resource server, il sert à accéder à une ressource protégée.
- Refresh token : destiné au serveur d’autorisation, il sert à obtenir de nouveaux access tokens.
- Un ID token est un JWT en OpenID Connect ; un access token n’est pas nécessairement un JWT.
- Un refresh token ne doit pas être envoyé à une API comme credential d’accès.
- La destination d’un token compte autant que son contenu.
- Décoder un JWT n’est pas le valider, et valider un JWT n’autorise pas encore une action métier.
La suite logique est maintenant de prendre l’access token et de regarder une erreur de raisonnement plus subtile : un jeton peut être parfaitement valide sans que l’action demandée soit autorisée.