skip to content

How does Spring Security handle and validate the OIDC id_token during oauth2Login()?

level: seniorimportance: should knowfreq 45%

answer

  1. id_token = signed JWT authenticating the user
  2. JWK signature + iss/aud/exp/nonce checks
  3. OidcIdTokenValidator + OidcIdTokenDecoderFactory
  4. nonce = replay protection
  5. id_token != access token (don't forward it)

basics

~20 s

For OIDC logins Spring parses the id_token JWT, checks its signature against the provider's public keys (JWK set), and verifies claims like issuer, audience, expiry, and the nonce. Only if all checks pass is the OidcUser created and the user logged in.

solid answer

~40 s

In an OIDC login the token endpoint returns an `id_token` — a signed JWT asserting the user's identity. During the authentication exchange, `OidcAuthorizationCodeAuthenticationProvider` decodes it via an `OidcIdTokenDecoderFactory`, which validates the JWS signature using the provider's JWK set (fetched from `jwkSetUri` / discovery) and then runs `OidcIdTokenValidator`. That validator enforces required claims: `iss` matches the provider's issuer, `aud` contains this client's `client_id`, the token isn't expired (`exp`) or used before `iat`/`nbf` allowance, `azp` is correct when multiple audiences exist, and crucially the `nonce` matches the value Spring generated for this login (replay protection). Only on success does Spring build the `OidcIdToken` and proceed to the `OidcUserService`. You can customize validation by supplying your own `OidcIdTokenDecoderFactory` (e.g. to add a max-age or custom claim check). Signature keys are cached and refreshed from the JWK endpoint.

code

java · 17 lines
java
// Add a custom id_token validator (e.g. require email_verified) while
// keeping Spring's default OIDC + timestamp validation intact.
@Bean
JwtDecoderFactory<ClientRegistration> idTokenDecoderFactory() {
    OidcIdTokenDecoderFactory factory = new OidcIdTokenDecoderFactory();
    factory.setJwtValidatorFactory(registration -> {
        OAuth2TokenValidator<Jwt> defaults =
            new OidcIdTokenValidator(registration); // iss/aud/exp/nonce/etc.
        OAuth2TokenValidator<Jwt> emailVerified = jwt ->
            Boolean.TRUE.equals(jwt.getClaimAsBoolean("email_verified"))
                ? OAuth2TokenValidatorResult.success()
                : OAuth2TokenValidatorResult.failure(
                    new OAuth2Error("email_not_verified"));
        return new DelegatingOAuth2TokenValidator<>(defaults, emailVerified);
    });
    return factory;
}

go deeper

for a junior

Know the id_token is a signed token proving who the user is, and Spring checks its signature and expiry.

for a middle

List the validated claims (iss, aud, exp, nonce) and that signature uses the provider's JWK set.

for a senior

Explain the decoder/validator classes, why each claim check exists, and how to add custom validators without disabling defaults.

for a principal

Reason about key rotation/caching, clock-skew and algorithm negotiation, audience-confusion attacks, and id_token vs access_token boundaries across services.

**What the id_token is.** OIDC adds an `id_token` on top of OAuth2: a JWT (JSON Web Token) *signed* by the provider (a JWS) whose claims *authenticate* the user — as opposed to the access token, which is about *authorization* to call APIs and is opaque to the client. The id_token is the proof the app relies on to say 'this is who logged in'. **When it appears.** The token endpoint returns an `id_token` alongside the access token whenever the login requested the `openid` scope. That's what makes Spring treat the flow as OIDC and eventually produce an `OidcUser`. **The validation pipeline.** 1. `OidcAuthorizationCodeAuthenticationProvider` receives the token response. 2. It obtains a `JwtDecoder` from `OidcIdTokenDecoderFactory` (default: `OidcIdTokenDecoderFactory`), keyed by `ClientRegistration`. The decoder is configured with the provider's `jwkSetUri` (from discovery/config) and the expected signing algorithm (default RS256). 3. **Signature check.** The decoder verifies the JWS signature using the provider's public keys from the JWK set. Keys are fetched and cached; on an unknown `kid` the set is refreshed. This proves the token really came from the provider and wasn't tampered with. 4. **Claim validation** via `OidcIdTokenValidator` (a `DelegatingOAuth2TokenValidator` combining a `JwtTimestampValidator` and OIDC-specific rules). It checks, per the OIDC spec: - `iss` (issuer) equals the configured/discovered issuer. - `aud` (audience) contains this client's `client_id`. - `azp` (authorized party) equals the client_id when there are multiple audiences. - `exp` not passed; `iat` present; a small clock skew is allowed. - `sub` present (the stable user id). - **`nonce`** matches the nonce Spring generated and stashed for this authorization request — this binds the token to *this* login and defeats replay/injection. 5. On success Spring wraps it as an `OidcIdToken` and hands off to `OidcUserService` to build the `OidcUser` (combining id_token claims with optional userinfo claims). **Why each check matters (attacks they stop).** - Signature → forged/tampered tokens. - `iss`/`aud`/`azp` → token substitution / tokens minted for a *different* client (audience confusion). - `exp`/`iat` → stale token replay. - `nonce` → authorization-code injection and id_token replay across sessions. **Customization.** Provide a `OidcIdTokenDecoderFactory` `@Bean` to change the expected algorithm, add a `setJwtValidatorFactory(...)` with extra validators (e.g. enforce `auth_time`/max_age, require `email_verified`, or a custom tenant claim). You generally should *not* disable the built-in validators. **Gotchas.** - Signature verification needs network access to the JWK endpoint; an unreachable JWK set breaks logins (keys are cached, but first fetch/rotation matters). - Clock skew between app and provider can cause spurious `exp`/`iat` failures — Spring allows a default skew but wildly wrong server clocks fail. - Some providers sign with algorithms other than RS256 (e.g. ES256 or HS256 for confidential clients); you must configure the expected algorithm/secret accordingly. - The id_token is for the *client* — do NOT forward it to your backend APIs as a bearer token; use the access token for that. Confusing the two is a classic mistake. - Front-channel logout / token lifetime: the id_token proves who logged in *at that moment*; ongoing authorization in your app is via the session, not by re-validating the id_token per request.

  • What is the nonce and which attack does it prevent?
    The nonce is a random value Spring generates per authorization request, sends in the auth request, and expects echoed inside the id_token. On callback Spring checks the id_token's nonce matches the one bound to the session. It prevents id_token replay and authorization-code injection, where an attacker tries to inject a token/code obtained in a different session.
  • Where do the keys that verify the id_token signature come from?
    From the provider's JWK Set endpoint (jwkSetUri), discovered via issuer-uri or configured directly. Spring's JwtDecoder fetches and caches the public keys; the JWT's kid header selects which key, and the set is refreshed if an unknown kid appears.

saying these in an interview costs you the question

  • Thinking the access token is the one that authenticates the user (it's the id_token)
  • Saying Spring trusts the id_token without signature verification
  • Forwarding the id_token to downstream APIs as a bearer token
  • Believing the nonce is optional or the same as the state parameter
  • Assuming id_token is re-validated on every request rather than establishing a session

context