skip to content

How does Spring Authorization Server issue access tokens versus OIDC id tokens, and how do they differ?

level: middleimportance: must knowfreq 50%

answer

  1. Access = authorization (call APIs)
  2. Id = authentication (who logged in)
  3. openid scope => id token
  4. client_credentials => no id token
  5. JWT default, REFERENCE = opaque via introspect

basics

~20 s

The access token authorizes API calls (it says what the client may do); the id token is proof of the user's identity for the client (it says who logged in). The id token is only issued when the client requests the openid scope.

solid answer

~40 s

An **access token** is for *authorization*: the client presents it to resource servers to call protected APIs, and it carries scopes/authorities. An **id token** is for *authentication*: an OIDC-only JWT returned to the *client* describing the authenticated user (sub, iss, aud, exp, nonce, and profile claims). Spring Authorization Server issues an access token for OAuth2 grants (authorization_code, client_credentials, etc.). It additionally issues an id token from the token endpoint only when the request includes the `openid` scope on an authorization_code (or similar user-present) flow — client_credentials has no user, so no id token. By default access tokens are self-contained JWTs signed via the `JWKSource`, but a RegisteredClient's `TokenSettings` can make them opaque (`OAuth2TokenFormat.REFERENCE`), validated through /oauth2/introspect. Both tokens are produced by the `OAuth2TokenGenerator` pipeline.

code

java · 13 lines
java
RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
    .clientId("web-app")
    .clientSecret("{noop}secret")
    .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
    .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
    .redirectUri("https://app.example.com/callback")
    .scope(OidcScopes.OPENID)          // <-- required to receive an id token
    .scope("read")                     // <-- ends up in the access token scope
    .tokenSettings(TokenSettings.builder()
        .accessTokenTimeToLive(Duration.ofMinutes(5))
        .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)  // JWT (default)
        .build())
    .build();

go deeper

for a junior

Access token = call APIs; id token = who logged in.

for a middle

Tie id-token issuance to the openid scope and the user-present flows; note client_credentials has none.

for a senior

Explain JWT vs opaque formats, TokenSettings TTLs, and the OAuth2TokenGenerator/JWKSource signing chain.

for a principal

Trade off self-contained vs reference tokens for revocation, scale, and audience isolation across many resource servers.

**Two tokens, two purposes.** Beginners conflate these; interviewers probe it constantly. - **Access token** — answers *"what may the bearer do?"*. The client sends it to a **resource server** (API) in the `Authorization: Bearer` header. It carries `scope`/authorities. The resource server, not the client, is its audience. - **Id token** — answers *"who authenticated?"*. It is an OIDC concept, a **JWT delivered to the client** at the token endpoint. Its audience (`aud`) is the client itself. The client reads it to establish a user session; it is *not* meant to be sent to APIs. **Id token claims.** Standard claims include `iss` (issuer), `sub` (stable user id), `aud` (the clientId), `exp`/`iat` (expiry/issued-at), `auth_time`, and `nonce` (echoes the client's nonce to prevent replay). With scopes like `profile`/`email`, more user claims appear. **When each is issued in Spring Authorization Server.** - `authorization_code` (a user logs in via the browser): access token + refresh token (if configured) + **id token when `openid` scope is present**. - `client_credentials` (machine-to-machine, no user): access token only — **never an id token**, because there is no authenticated end user. - `refresh_token`: a new access token (and a new id token if openid scope carried over). **Token format.** By default the access token is a **signed JWT** (`OAuth2TokenFormat.SELF_CONTAINED`) — self-describing, verifiable offline by resource servers using keys from `jwks_uri`. You can instead choose **opaque/reference tokens** per client via `TokenSettings.builder().accessTokenFormat(OAuth2TokenFormat.REFERENCE)`; those are random strings the resource server must validate by calling `/oauth2/introspect`. Id tokens are **always JWTs** (OIDC requires it). **The generation pipeline.** Internally an `OAuth2TokenGenerator<OAuth2Token>` (a `DelegatingOAuth2TokenGenerator` composing a `JwtGenerator`, `OAuth2AccessTokenGenerator`, and `OAuth2RefreshTokenGenerator`) produces the tokens. The `JwtGenerator` signs JWTs using a `JwtEncoder` backed by the `JWKSource<SecurityContext>`. You customize claims with an `OAuth2TokenCustomizer<JwtEncodingContext>` bean (see the customization question). **Signing.** JWTs are signed with an asymmetric key (typically RSA or EC); the private key signs, and the public key is published at `/oauth2/jwks` so resource servers verify signatures without a shared secret. The `JWKSource` bean supplies these keys. **Gotchas.** - Requesting an id token without the `openid` scope silently yields none — a common integration bug. - Don't send id tokens to APIs; their audience is the client, and resource servers should reject them. - TTLs differ and are configurable via `TokenSettings` (`accessTokenTimeToLive`, etc.); access tokens are short-lived, refresh tokens longer. **When to use which format.** Prefer self-contained JWT access tokens for scale (offline verification); choose opaque tokens when you need instant revocation and centralized control (every call hits introspection).

  • A client_credentials request returns no id token. Bug or expected?
    Expected — client_credentials is machine-to-machine with no authenticated user, so OIDC issues no id token regardless of scopes.
  • How does a resource server validate a self-contained JWT access token?
    It fetches the public keys from the server's jwks_uri (via discovery) and verifies the JWT signature, issuer, audience and expiry locally — no call back to the auth server.
  • When would you choose opaque (REFERENCE) access tokens instead of JWTs?
    When you need immediate revocation / central control; resource servers then validate via /oauth2/introspect on every request instead of verifying offline.

saying these in an interview costs you the question

  • Saying the id token is used to call APIs (it authenticates the user to the client)
  • Claiming an id token is issued without the openid scope
  • Expecting an id token from client_credentials
  • Thinking access tokens are always JWTs (can be opaque REFERENCE)

context