Beta

Create a tenant

A new tenant starts with its own users, OAuth settings, audit history and logs. You are its first Tenant Admin.

BTL Admin

Validating an ID token

All domains, identifiers, and tokens in these examples are fictional.

The last lesson followed sign-ins that fail in plain view: an error in the redirect, or a token response with no ID token in it. This time the printer's token request at 09:00 UTC on 1 October succeeds, and the response carries an ID token saying that you, user-2048, have just signed in at the photo service.

Nothing about it looks wrong, but looking right is easy: anyone able to put a string in that position can manage it. The printer needs to establish four things before it believes a word: that the photo service issued this token, that it was issued to the printer, that it answers this sign-in attempt, and that it is current.

Before reading the claims

Here is the token again, decoded:

Header
{
  "alg": "RS256",
  "kid": "photos-rs-2026-09",
  "typ": "JWT"
}

Claims
{
  "iss": "https://auth.photos.example",
  "sub": "user-2048",
  "aud": "photo-printer",
  "exp": 1790845500,
  "iat": 1790845200,
  "auth_time": 1790845185,
  "nonce": "demo-nonce-3"
}

Until every check has passed, these are statements the printer has not confirmed. It does not look up an account for user-2048, create a session, or greet you by name. It also leaves the access token from the same response untouched. If an attacker managed to slip another person's authorization code into this exchange, both tokens in the response came from that code, and the ID token checks are how the printer finds out. Current OAuth security guidance says so directly: a client that relies on the nonce to detect injected codes must not use any token from the response, the access token included, until the nonce check has succeeded.

As Receiving the ID token explained, the printer verifies the signature even though the token came straight from the token endpoint over a connection it opened itself.

Much of what follows is familiar. Token formats and validation walked the photo API through the checks for a JWT access token, and Digital signatures explained why the algorithm and key identifier in a header are only hints. An ID token is also a JWT, signed by the same provider. What changes is who it is for and what it has to prove:

CheckJWT access token at the photo APIID token at the printer
AlgorithmES256, the algorithm the photo service uses for access tokens.RS256, the OpenID Connect default, unless the client registered another.
Typetyp must be at+jwt.The core specification defines no type for ID tokens. JWT says only that this is a JWT, and some providers leave typ out.
AudienceThe API's identifier, https://api.photos.example.The client ID, photo-printer. An azp claim, when present, must agree.
Link to a requestNone. Any request may carry the token until it expires.nonce must equal the value stored with this sign-in attempt.
Authentication detailsChecked only by an API that demands a recent or stronger sign-in.auth_time and acr, checked when the printer asked for them.

The type row protects only one side. The photo API's at+jwt check stops an ID token from being used as an access token, but nothing in an ID token's header stops other JWTs from being offered to the printer as ID tokens. The audience and the nonce carry that weight instead: a JWT addressed to anyone else, or produced for any other attempt, fails there.

The checks in order

The printer found this attempt when your browser returned with state demo-signin-3, and the attempt recorded what the printer expects: the issuer https://auth.photos.example, the nonce demo-nonce-3, and anything it asked for about how you authenticate. Those stored values, not the token's own claims, decide which provider's settings apply. The printer then works through the token:

  1. Accept only an expected algorithm. The header's alg must be on the printer's list for ID tokens from this provider, which here holds only RS256.
  2. Find the key. kid, here photos-rs-2026-09, must name a key in the photo service's key set, and that key must suit the algorithm. Signing keys and rotation covers these first two steps.
  3. Verify the signature over the exact bytes of the header and claims.
  4. Check the issuer. iss must equal the expected issuer exactly.
  5. Check the audience. aud must contain photo-printer and no audience the printer does not trust. If an azp claim is present, it must be photo-printer. Issuer, audience, and authorized party covers these claims.
  6. Check the time. The current time must be before exp, allowing a small tolerance for clock differences, and iat must be recent.
  7. Match the nonce. nonce must equal demo-nonce-3. A token without one fails.
  8. Check the authentication, if the printer asked. When the request set max_age or asked for a particular authentication context, auth_time and acr must satisfy it. Expiration and nonce checks covers this step and the two before it.

A relying party that registered to receive encrypted ID tokens decrypts first. The printer has not, and Signed and encrypted messages, in Advanced OIDC, covers that case.

The specification lists the issuer and audience checks before the signature. The order matters less than the rule that every check runs and nothing is used until all of them have passed. Verifying the signature early has one advantage: every later comparison is made against values the photo service actually signed.

attempt  = pending_sign_in                     # found by state, bound to this browser
provider = providers[attempt.expected_issuer] # https://auth.photos.example
header, claims, signature = parse(id_token)   # nothing here is trusted yet

if header.alg not in provider.id_token_algorithms:        # ["RS256"]
    reject("algorithm")
key = provider.key_set.find(header.kid)                   # photos-rs-2026-09
if key is None or not key.usable_with(header.alg):
    reject("key")
if not verify(key, header.alg, signed_bytes, signature):
    reject("signature")
if claims.iss != provider.issuer:
    reject("issuer")
if provider.client_id not in audiences(claims.aud) or has_untrusted_audience(claims.aud):
    reject("audience")
if claims.azp is present and claims.azp != provider.client_id:
    reject("authorized party")
if now() >= claims.exp + LEEWAY or not recent(claims.iat):
    reject("time")
if claims.nonce != attempt.nonce:                         # a missing nonce fails too
    reject("nonce")
if attempt.max_age is set and too_old(claims.auth_time, attempt.max_age):
    reject("authentication age")
accept(claims)                                            # only now find the account

Every reject ends the sign-in. The attempt has already been claimed, so it cannot be tried again, no account is looked up, and no session is created. When validation fails follows what the person sees next and what the printer records.

As with access tokens, a maintained OpenID Connect library performs these checks for the printer. The printer's part is to give it the expected issuer, client ID, algorithms, and key set, to pass in the nonce from the attempt, and to confirm that the library rejects a token with a missing value rather than skipping the check.

What the printer keeps

Once the last check passes, the token has answered its question: who signed in, for this attempt, and how. The printer keeps the answer, not the token.

The issuer and subject together, https://auth.photos.example and user-2048, identify your photo account, and the printer uses that pair to find your printer account, as Connecting a sign-in to an account describes. It records auth_time, and acr when there is one, with the session it is about to create, so that later decisions about sensitive actions know how recent and how strong this sign-in was.

The nonce is finished. It left with the pending attempt when the printer claimed it, so no later token can match it. Nor does the token's five-minute lifetime carry over. As ID tokens and access tokens explained, exp limits when the printer may accept the token, not how long you stay signed in.

The ID token itself stays on the printer's server only if the printer will need it again, as a hint in a later sign-out request. It never goes into a cookie, a page, or a log.

The access token can now be used. The printer asked for profile and email, and with the code flow those claims come from the UserInfo endpoint, which the printer calls with demo-access-token-12. The UserInfo endpoint describes that call and the subject check that goes with it.

Each step in the list hides a decision about what the printer compares against. The next lesson starts with the two claims that name the parties: the issuer that made the statement, and the audience it was made for.

Try it in the Lab

PUT IT INTO PRACTICE

Check your understanding

Try these questions before moving on. If an answer isn't right, use the feedback and try again.

0 of 2 answered correctly

Enable JavaScript to answer these questions and save progress in this browser.

QUESTION 1 OF 2The ID token's signature has verified, but the other checks have not run yet. A developer wants to call the UserInfo endpoint with the access token now, to save time. What should happen?

QUESTION 2 OF 2How does the audience check for an ID token at the printer differ from the one for a JWT access token at the photo API?

We value your privacy

We use cookies and similar technologies to enhance your browsing experience, and analytics to understand our traffic. By clicking "Allow All", you consent to optional analytics. Cookie Policy

Learn identity