The UserInfo endpoint
All domains, identifiers, and tokens in these examples are fictional.
The printer has validated your ID token. It knows that user-2048 at the photo service authenticated at 08:59:45, and it still holds the other half of the token response: the access token demo-access-token-12, issued for openid profile email and good for ten minutes. That token is what unlocks your name, picture, and email address.
Asking the provider
The UserInfo endpoint, which Providers and relying parties listed among the roles, is a protected resource that the provider hosts. It returns claims about the person an access token was issued for. The printer took its address from the userinfo_endpoint field of the photo service's discovery document, and it calls the endpoint like any API that accepts bearer tokens:
GET /userinfo HTTP/1.1
Host: auth.photos.example
Authorization: Bearer demo-access-token-12
The endpoint must accept both GET and POST. GET with the token in the Authorization header is the recommended form, and for the reasons Using access tokens gave, the token never goes in the query string. The connection uses HTTPS, and the printer checks the server's certificate as it does for every call that carries a token. Here that check does a second job. The response is plain JSON with no signature, so the certificate is the printer's only evidence that the answer came from the photo service.
HTTP/1.1 200 OK
Content-Type: application/json
{
"sub": "user-2048",
"name": "Robin Park",
"given_name": "Robin",
"family_name": "Park",
"picture": "https://photos.example/avatars/user-2048.jpg",
"locale": "en-US",
"updated_at": 1788220800,
"email": "[email protected]",
"email_verified": true
}
These are the claims from Standard and custom claims, arriving where the printer asked for them. profile requested fourteen claims and the photo service returned the six it holds. email requested two, and both are here. sub is always present, whatever was requested.
A relying party can instead register to receive the response as a JWT, signed by the provider, encrypted for the relying party, or both. A signed response names its issuer and audience and is checked much like an ID token. Signed and encrypted messages, in Advanced OIDC, covers both. The printer uses plain JSON over HTTPS.
When the endpoint refuses a request, it answers as any bearer-token API does, with a WWW-Authenticate header. Called with a token that has expired, it might reply as follows, with the header wrapped here for display:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token",
error_description="The access token expired"
The error codes mean what Using access tokens described. invalid_token covers a token that is expired, revoked, or otherwise unacceptable, and invalid_request a malformed request. UserInfo expects an access token issued through an OpenID Connect sign-in. A token from the printer's photo connection, issued for photos.read alone, is likely to be refused, often with 403 and insufficient_scope.
Matching the subject
Before using any of those claims, the printer compares the sub in the response with the sub in the ID token it validated. They must match exactly, character for character, with no trimming and no change of case. If they differ, the printer must not use any value from the response.
The rule exists because the two answers prove different things:
| ID token | UserInfo response | |
|---|---|---|
| Arrives | In the token response. | From a separate call with the access token. |
| Addressed to | The printer, named in aud. | Whoever presents the access token. |
| Protected by | The provider's signature. | HTTPS, unless a signed response was registered. |
| Tied to this attempt | By the nonce. | Only through the access token the printer chose to send. |
The UserInfo response describes whoever the access token belongs to. The printer's only link between that token and your ID token is that they arrived together. If something breaks that link, the response can describe someone else entirely.
The specification has an attack in mind, called token substitution, in which someone swaps a token from one session into another. Flows that deliver an access token through the browser, which Understanding implicit and hybrid integrations describes, make the swap possible. Suppose an attacker replaced the access token in your sign-in with one issued for their own photo account. The ID token would still name you, so the printer would correctly sign you in. Without the comparison, it would then fill in your printer account with the attacker's name and email address, and your order confirmations, along with anything else the printer sends by email, would go to the attacker.
The printer's own flow is better protected. Both tokens come back together from its direct request to the token endpoint, and nobody in the browser can swap them. Mistakes inside the printer can still pair them wrongly: a cache keyed by the wrong value, a worker that picks up the most recent token response instead of this attempt's, or a retry that reuses a token from someone else's sign-in. The comparison catches all of them:
userinfo = get(provider.userinfo_endpoint, bearer=attempt.access_token)
if userinfo.sub != id_token.sub: # exact, case-sensitive comparison
record("userinfo_subject_mismatch", provider.issuer, correlation_id)
fail_sign_in()
copy_wanted_claims(account, userinfo)
The specification requires only that the values not be used. A mismatch means an attack or a bug, though, not a missing claim to work around, so this printer stops the sign-in and shows a general error. Its record names the stage, the issuer, and a correlation ID, and leaves out the tokens and the claims.
The comparison is meaningful only within one provider. A plain UserInfo response does not name its issuer, so the printer knows which provider answered only because it chose the endpoint. It must call the UserInfo endpoint from the configuration of the issuer whose ID token it validated. A sign-in with the mail service uses the mail service's endpoint, and the sub it returns should be a94f1c07e2.
When to call it
UserInfo looks a lot like the /me endpoint in From delegated access to sign-in, and it is easy to use it in the same way. The difference is the order. The printer calls UserInfo only after it has validated an ID token, and only to fill in details about the person that token names. A UserInfo response is never a sign-in. It does not say that the access token was issued to the printer, when you authenticated, or which attempt it belongs to. If a token response arrived without an ID token, there would be nothing to compare the response with, and the printer would treat the sign-in as failed, as Authentication errors described.
The natural moment is during sign-in, right after validation, while the access token is minutes old. The printer copies the claims it needs into your account and moves on. It does not call UserInfo on every page to show your name. That would add a request to another service to every page the printer serves, slow the printer down whenever the photo service is slow, and stop working ten minutes later when the access token expires. This sign-in came with no refresh token, so the printer could not obtain another access token without sending you back to the photo service.
If the call fails during sign-in, perhaps because the photo service is briefly unavailable, the sign-in itself can still succeed. The ID token has already established who you are. A returning customer keeps the details the printer stored last time, and a first-time customer is asked for whatever the printer cannot do without. The printer tries again at your next sign-in.
Your details can change between sign-ins, and the printer's copies catch up the next time you sign in, which is good enough for a greeting and a picture. A relying party that needs current claims while you are away has to ask for continuing access, which Offline access and refresh behavior describes. Claim stability and privacy looks at how the printer keeps its copies current, and how little it needs to keep at all.