Sécurité et identité
OpenID Connect : ID token, discovery, nonce et gestion de session
Intermédiaire8 min de lectureMis à jour
Cette page a été traduite de l'anglais avec l'aide de l'IA, puis vérifiée par rapport à l'original. Lire la version anglaise
OpenID Connect transforme OAuth 2.0, framework de délégation d'accès, en protocole d'authentification unique en lui ajoutant ce qu'un access token n'a jamais fourni : une assertion vérifiable sur l'identité de l'utilisateur connecté et la manière dont il s'est authentifié. Ce guide couvre l'ID token et ses claims, le document de discovery et le jeu de clés, les scopes standard et UserInfo, ce contre quoi protègent respectivement nonce et state, la déconnexion et la gestion de session, et la façon de valider un ID token sans rien négliger.
Ce que vous allez apprendre
- Décrire comment OpenID Connect ajoute une couche d'identité au flux authorization code d'OAuth 2.0
- Lire un ID token et expliquer chacun des claims obligatoires qu'il contient
- Utiliser le document de discovery et l'endpoint JWKS plutôt que de coder en dur la configuration du fournisseur
- Expliquer ce que nonce, state et les claims c_hash et at_hash empêchent chacun
- Valider correctement un ID token et savoir quand l'utiliser plutôt qu'un access token
- Choisir entre déconnexion front-channel et back-channel pour une session fédérée
OAuth 2.0 donne à une application la permission d'appeler une API. Il ne lui dit pas qui est l'utilisateur, quand il s'est authentifié, ni s'il est toujours là : l'access token a été émis pour une API et peut rester opaque pour tous les autres. Les applications avaient pourtant besoin précisément de ces informations ; pendant des années, elles ont donc déduit l'identité en appelant avec l'access token un endpoint de profil propre à chaque fournisseur. Cette approche a un vrai défaut : un access token obtenu n'importe où, pour n'importe quel client, répondra sans broncher à un tel appel.
OpenID Connect corrige ce problème en définissant un second jeton, adressé au client lui-même. L'ID token est un JSON Web Token signé dont l'audience est votre application, dont l'émetteur est celui que vous avez fixé, et dont les claims décrivent l'événement d'authentification. Autour de cette idée unique, la spécification ajoute la discovery, un ensemble standard de claims et de scopes, un endpoint UserInfo ainsi qu'une sémantique de session et de déconnexion — de quoi construire une authentification unique interopérable entre des produits d'éditeurs différents. Ce guide suppose que vous connaissez déjà le flux authorization code d'OAuth 2.0 ; tout ce qui suit est ce qu'OpenID Connect y ajoute.
Ce qu'apporte le protocole
Une requête OpenID Connect est une requête d'autorisation OAuth qui contient le scope openid. Ce seul scope fait basculer le fournisseur en mode OpenID Connect, et la réponse du token endpoint comporte alors un id_token en plus de l'access token.
GET /authorize?response_type=code
&scope=openid%20profile%20email
&client_id=web-portal
&redirect_uri=https%3A%2F%2Fportal.example.com%2Fcallback
&state=Rm9vYmFy
&nonce=n-0S6_WzA2Mj
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256 HTTP/1.1
Host: sso.example.com
Le vocabulaire change légèrement : le serveur d'autorisation devient l'OpenID Provider (OP) et le client la Relying Party (RP). La réponse du token endpoint a l'air familière, à un ajout près.
{
"access_token": "IiFwYXNzZWQgdG8gdGhlIEFQSSI",
"token_type": "Bearer",
"expires_in": 300,
"refresh_token": "8xLOxBtZp8",
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InNpZy0yMDI2LTAzIn0.eyJpc3MiOi..."
}
Il existe trois modes de réponse. Le flux authorization code est celui à utiliser : tout ce qui est sensible revient par le back channel. Le flux implicite et le flux hybride renvoient des jetons via le navigateur et sont de fait abandonnés pour les nouveaux développements, pour les mêmes raisons qui ont conduit OAuth à retirer l'implicit grant.
Anatomie de l'ID token
Décoder le payload d'un ID token — avec un décodeur JWT côté client, jamais en vous fiant, dans le code, à une valeur non vérifiée — révèle les claims qui portent tout le sens du protocole.
{
"iss": "https://sso.example.com",
"sub": "9f2c1b7e-4a55-4f2a-bd58-6f0f1a2c3d44",
"aud": "web-portal",
"exp": 1789012645,
"iat": 1789012345,
"auth_time": 1789012300,
"nonce": "n-0S6_WzA2Mj",
"acr": "urn:mace:incommon:iap:silver",
"amr": ["pwd", "hwk"],
"azp": "web-portal",
"email": "[email protected]",
"email_verified": true,
"name": "Jordan Lee",
"preferred_username": "jordan"
}
iss et sub forment ensemble le seul identifiant stable d'un utilisateur. Le sujet est unique et jamais réattribué au sein d'un même émetteur : un compte local doit donc être indexé sur ce couple, et non sur une adresse e-mail que la personne peut modifier à tout moment. aud doit être égal à votre identifiant client ; azp désigne la partie autorisée lorsqu'un jeton est émis pour plusieurs audiences. auth_time indique quand l'utilisateur s'est réellement authentifié, et non quand le jeton a été émis : c'est ce qui vous permet d'exiger une connexion récente pour une opération sensible en envoyant max_age ou prompt=login. acr et amr décrivent le niveau et les méthodes de cette authentification — pwd pour un mot de passe, otp pour un code à usage unique, hwk pour une clé matérielle — et permettent à une relying party d'imposer l'authentification multifacteur sur les écrans d'administration.
Les scopes standard correspondent à des ensembles de claims standard : profile couvre le nom, la photo, la locale et autres informations similaires ; email couvre email et email_verified ; address et phone couvrent ce que leur nom indique. Ne demander que ce dont vous avez besoin préserve l'honnêteté de l'écran de consentement et réduit le volume de données personnelles dont vous avez la charge.
Discovery et clés
Rien ne devrait être codé en dur à propos d'un fournisseur, hormis l'URL de son émetteur. À partir d'elle, le document de discovery, publié à un emplacement bien connu, vous donne tous les endpoints et toutes les capacités.
GET /.well-known/openid-configuration HTTP/1.1
Host: sso.example.com
{
"issuer": "https://sso.example.com",
"authorization_endpoint": "https://sso.example.com/authorize",
"token_endpoint": "https://sso.example.com/token",
"userinfo_endpoint": "https://sso.example.com/userinfo",
"jwks_uri": "https://sso.example.com/.well-known/jwks.json",
"end_session_endpoint": "https://sso.example.com/logout",
"response_types_supported": ["code"],
"id_token_signing_alg_values_supported": ["RS256", "ES256"],
"scopes_supported": ["openid", "profile", "email", "offline_access"],
"code_challenge_methods_supported": ["S256"]
}
Le jwks_uri expose les clés publiques de signature du fournisseur sous forme de JSON Web Key Set. Chaque clé porte un kid, et l'en-tête du jeton indique le kid utilisé, ce qui rend la rotation transparente : le fournisseur publie la nouvelle clé avant de commencer à signer avec, les clients rechargent le jeu de clés lorsqu'ils rencontrent un kid inconnu, et l'ancienne clé est retirée une fois les jetons en circulation expirés. Mettez le jeu de clés en cache, respectez ses en-têtes de cache et limitez la fréquence des rechargements pour qu'un kid inconnu ne puisse pas servir à bombarder le fournisseur.
Nonce, state et les claims de hachage
Trois mécanismes distincts protègent contre trois faiblesses distinctes, et on les confond souvent.
state vous appartient. Vous le générez, le conservez dans votre session et le comparez au retour du navigateur. Il garantit que le callback correspond à un flux lancé par ce navigateur, ce qui neutralise les attaques CSRF sur l'endpoint de redirection ainsi que le login CSRF, où un attaquant cherche à vous connecter à votre insu sur son propre compte.
nonce est recopié par le fournisseur dans l'ID token. En comparant le nonce du jeton à la valeur que vous avez stockée, vous prouvez que le jeton a été émis pour cette tentative d'authentification et ne peut pas être rejoué depuis une autre. Il est obligatoire dans les flux implicite et hybride, et fortement recommandé dans le flux authorization code.
c_hash et at_hash apparaissent lorsqu'un code ou un access token transite par le front channel avec un ID token. Ce sont des empreintes tronquées de ces valeurs : la relying party peut ainsi vérifier que le code et le jeton reçus sont bien ceux que le fournisseur a émis en même temps que cet ID token.
PKCE reste en vigueur par-dessus tout cela : il protège l'échange du code lui-même et les bonnes pratiques OAuth actuelles l'exigent quel que soit le type de client.
Valider correctement un ID token
La validation est une checklist, et en sauter une seule ligne a déjà été à l'origine de vulnérabilités bien réelles. Vérifiez la signature avec une clé du JWKS du fournisseur sélectionnée par kid, en limitant les algorithmes acceptés à ceux que le fournisseur annonce ; vérifiez que iss correspond exactement ; vérifiez que aud contient votre identifiant client et, si azp est présent, qu'il lui est égal ; contrôlez exp et iat avec une petite tolérance de décalage d'horloge ; comparez le nonce ; enfin, si vous en avez demandé un, vérifiez auth_time au regard de votre max_age.
// nimbus-jose-jwt: validator configured once from the discovery document
IDTokenValidator validator = new IDTokenValidator(
new Issuer("https://sso.example.com"),
new ClientID("web-portal"),
JWSAlgorithm.RS256,
new URL("https://sso.example.com/.well-known/jwks.json"));
try {
IDTokenClaimsSet claims = validator.validate(SignedJWT.parse(idToken), expectedNonce);
String subject = claims.getSubject(); // stable only together with the issuer
Date authenticatedAt = claims.getAuthenticationTime();
if (authenticatedAt.toInstant().isBefore(Instant.now().minus(Duration.ofMinutes(15)))) {
throw new ReauthenticationRequiredException(); // step-up for a sensitive action
}
} catch (BadJOSEException | JOSEException e) {
throw new AuthenticationFailedException("id_token rejected", e);
}
N'écrivez ce code vous-même qu'en l'absence d'alternative. Une bibliothèque — ou le client OpenID Connect intégré à Spring Security — implémente la liste complète, y compris les restrictions d'algorithmes et la gestion de la rotation des clés que le code écrit à la main oublie généralement. Les pièges propres à la vérification de tout JWT, dont la confusion d'algorithme, sont traités dans sécuriser des API REST avec JWT.
UserInfo, et quel jeton va où
L'endpoint UserInfo renvoie des claims sur l'utilisateur authentifié en échange d'un access token valide portant le scope openid.
GET /userinfo HTTP/1.1
Host: sso.example.com
Authorization: Bearer IiFwYXNzZWQgdG8gdGhlIEFQSSI
Il est utile lorsque des claims changent en cours de session ou lorsque vous gardez volontairement l'ID token compact, mais il coûte un appel réseau, et sa réponse doit contenir un sub identique à celui de l'ID token — sinon, c'est qu'une substitution de jeton a eu lieu. Pour tout ce dont vous avez besoin à chaque requête, privilégiez les claims de l'ID token.
La répartition des rôles mérite d'être énoncée clairement, car s'y tromper est l'erreur de conception OpenID Connect la plus courante :
| Jeton | Audience | Rôle | Envoyé à |
|---|---|---|---|
| ID token | Votre client | Qui s'est connecté, comment et quand | Nulle part — consommé par le client |
| Access token | Une API | Ce que le porteur peut y faire | Le serveur de ressources |
| Refresh token | Le fournisseur | Obtenir de nouveaux jetons sans interaction | Le token endpoint uniquement |
Sessions et déconnexion
Une connexion fédérée crée au moins deux sessions : une chez le fournisseur et une dans chaque application. Y mettre fin de façon cohérente est la partie de l'authentification unique que les implémentations négligent le plus souvent.
La déconnexion initiée par la RP redirige l'utilisateur vers l'end_session_endpoint du fournisseur avec un id_token_hint et une post_logout_redirect_uri, ce qui met fin à la session chez le fournisseur et ramène l'utilisateur sur une page que vous avez enregistrée à l'avance.
Avec le front-channel logout, le fournisseur charge dans une iframe cachée une URL de déconnexion pour chaque application. C'est simple, mais cela dépend des cookies tiers et du chargement réussi de chaque iframe : dans les navigateurs modernes, le mécanisme échoue donc silencieusement.
Le back-channel logout est l'option fiable. Le fournisseur envoie par POST un logout token signé — un JWT contenant un sid ou un sub et un claim events qui désigne l'événement de déconnexion — directement à chaque client enregistré, de serveur à serveur.
POST /backchannel-logout HTTP/1.1
Host: portal.example.com
Content-Type: application/x-www-form-urlencoded
logout_token=eyJhbGciOiJSUzI1NiIsImtpZCI6InNpZy0yMDI2LTAzIn0...
Validez ce jeton exactement comme un ID token, avec une règle supplémentaire : il ne doit pas contenir de nonce. Détruisez ensuite la session locale identifiée par sid. Comme cela suppose un suivi session par session, les applications qui reposent uniquement sur des cookies stateless doivent ajouter un index de sessions côté serveur — un prix raisonnable pour une déconnexion qui fonctionne vraiment. Si vous vous intégrez à un fournisseur d'entreprise plus ancien, le mécanisme équivalent y est le single logout de SAML, qui se heurte aux mêmes difficultés sous une forme plus compliquée.
Points clés à retenir
- OpenID Connect, c'est OAuth 2.0 plus le scope
openid, un ID token adressé à votre client et des claims d'identité standard. - L'ID token vous dit qui s'est authentifié, quand et par quelles méthodes ;
issetsubensemble constituent la seule clé utilisateur stable. - Lisez chaque endpoint et chaque clé depuis le document de discovery et le JWKS, afin que la rotation des clés et les changements chez le fournisseur ne nécessitent aucun redéploiement.
statelie la redirection à votre session,noncelie l'ID token à cette tentative de connexion, PKCE lie le code à votre client : utilisez les trois.- Validez la signature, l'émetteur, l'audience, l'expiration, le nonce et, le cas échéant,
auth_time; utilisez une bibliothèque plutôt que des vérifications écrites à la main. - Envoyez les access tokens aux API, gardez les ID tokens dans le client, et implémentez le back-channel logout si la déconnexion unique doit être fiable.
Sources et lectures complémentaires
Questions fréquentes
- Faut-il envoyer l'ID token à mon API ?
- Non. L'ID token est destiné à l'application cliente — son audience est votre identifiant client — et sert à indiquer à cette application qui s'est connecté. Une API doit accepter des access tokens, dont l'audience désigne l'API et dont les scopes décrivent ce que l'appelant a le droit de faire. Envoyer un ID token à une API l'oblige à accepter un jeton dont les scopes n'ont jamais été prévus pour elle, ce qui casse la validation de l'audience.
- Quelle est la différence entre nonce et state ?
- State protège le retour vers votre application : vous le générez, le stockez en session et le comparez au retour, ce qui empêche un attaquant de lancer un flux qui se terminerait dans le navigateur d'une victime. Nonce protège l'ID token lui-même : le fournisseur le recopie dans le jeton, si bien qu'un jeton rejoué depuis une autre tentative de connexion échoue à la comparaison. En pratique, les deux sont indispensables.
- Puis-je faire confiance au claim email de n'importe quel fournisseur ?
- Seulement si email_verified vaut true et que vous faites confiance à cet émetteur précis pour l'avoir vérifié. Rapprocher des comptes sur une adresse non vérifiée est un vecteur bien connu de prise de contrôle de compte, car un utilisateur peut enregistrer chez un fournisseur peu regardant une adresse qu'il ne contrôle pas. Identifiez les comptes locaux par le couple émetteur et sujet, et traitez l'e-mail comme un attribut d'affichage que vous pouvez mettre à jour après vérification.
- Comment savoir qu'une session fédérée a pris fin ?
- L'expiration des jetons ne permet pas de le déduire. Le back-channel logout est le mécanisme fiable : le fournisseur envoie par POST un logout token signé à chaque client ayant déclaré un endpoint de déconnexion, et le client détruit la session locale correspondante. Le front-channel logout s'appuie sur des iframes cachées ; il est plus simple à déployer mais peu fiable dès que les cookies tiers ou le réseau sont restreints.