skip to content

How do you customize the claims in issued tokens and configure JWT signing in Spring Authorization Server?

level: seniorimportance: should knowfreq 35%

answer

  1. OAuth2TokenCustomizer<JwtEncodingContext>
  2. branch on context.getTokenType()
  3. JWKSource<SecurityContext> = signing keys
  4. public key at /oauth2/jwks, kid in header
  5. load keys from secret store, rotate with multiple JWKs

basics

~20 s

Register an OAuth2TokenCustomizer<JwtEncodingContext> bean to add or change claims per token type. Provide a JWKSource<SecurityContext> bean holding your signing key(s); the server signs JWTs with the private key and publishes the public key at /oauth2/jwks.

solid answer

~40 s

To shape token contents, expose an `OAuth2TokenCustomizer<JwtEncodingContext>` bean. It's invoked while building each JWT; you inspect `context.getTokenType()` (ACCESS_TOKEN vs ID_TOKEN), the `RegisteredClient`, and the `Authentication`/principal, then call `context.getClaims().claim(...)` to add roles, tenant, etc. For signing, provide a `JWKSource<SecurityContext>` bean wrapping one or more asymmetric JWKs (RSA/EC) — each with a `keyID`. The server's `JwtEncoder`/`NimbusJwtEncoder` uses the private key to sign; the public key is exposed at `/oauth2/jwks` so resource servers verify offline. Include multiple keys (old + new) in the JWKSource to rotate without downtime: sign with the current key while old public keys remain published until existing tokens expire. Keys must be loaded from a secret store, not generated at startup (that would rotate on every restart and break in-flight tokens across instances).

code

java · 20 lines
java
@Bean
OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer() {
    return context -> {
        if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
            var authorities = context.getPrincipal().getAuthorities().stream()
                .map(GrantedAuthority::getAuthority).toList();
            context.getClaims().claim("roles", authorities);
        }
    };
}

@Bean
JWKSource<SecurityContext> jwkSource(KeyStoreConfig cfg) {
    RSAKey rsaKey = new RSAKey.Builder(cfg.publicKey())   // loaded from a keystore/secret
        .privateKey(cfg.privateKey())
        .keyID("key-2026-07")
        .build();
    JWKSet jwkSet = new JWKSet(rsaKey);                    // add retired public keys here to rotate
    return (selector, ctx) -> selector.select(jwkSet);
}

go deeper

for a junior

Know you can add custom claims and that JWTs are signed with a key the server publishes.

for a middle

Name OAuth2TokenCustomizer and the JWKSource bean and what each does.

for a senior

Branch customization by token type/principal; explain kid, offline verification, and loading keys from a secret store.

for a principal

Design zero-downtime key rotation, multi-instance key sharing, algorithm choice, and token-size/PII governance.

**Two customization axes: what's in the token, and how it's signed.** **1. Customizing claims — `OAuth2TokenCustomizer`.** The server builds tokens through an `OAuth2TokenGenerator`. For JWTs, you hook in a bean of type `OAuth2TokenCustomizer<JwtEncodingContext>`. It is called once per JWT being minted with a `JwtEncodingContext` that gives you: - `context.getTokenType()` — an `OAuth2TokenType`; compare with `OAuth2TokenType.ACCESS_TOKEN` or `OidcParameterNames.ID_TOKEN` (via `context.getTokenType().getValue().equals("id_token")`) to branch. - `context.getRegisteredClient()` — the client the token is for. - `context.getPrincipal()` — the `Authentication` (the user for authorization_code; the client for client_credentials). - `context.getAuthorizedScopes()`. - `context.getClaims()` — a `JwtClaimsSet.Builder` you mutate: `.claim("roles", authorities)`, `.claim("tenant", ...)`. Typical use: put authorities/roles into the access token so resource servers can authorize; enrich the id token with profile claims. For **opaque** access tokens you'd instead implement `OAuth2TokenCustomizer<OAuth2TokenClaimsContext>`. **2. Signing — `JWKSource<SecurityContext>`.** JWTs are signed with an **asymmetric key pair**. You must provide a `JWKSource<SecurityContext>` bean containing the key(s) as **JWKs** (JSON Web Keys): ```java RSAKey rsaKey = new RSAKey.Builder(publicKey).privateKey(privateKey).keyID("key-2026-07").build(); JWKSet set = new JWKSet(rsaKey); return (selector, ctx) -> selector.select(set); ``` The server wires a `NimbusJwtEncoder` over this source. The **private** key signs; the **public** key is published at `/oauth2/jwks` (with its `kid`), and every issued JWT header carries that `kid` so verifiers pick the right key. **Key rotation (the senior/principal point).** Because access tokens are verified offline against published keys, you must keep old public keys available until all tokens signed with them expire. Strategy: keep multiple JWKs in the `JWKSet` — a current signing key plus recently-retired public keys. Introduce a new key, start signing with it, but leave the previous public key(s) in `/oauth2/jwks` for at least the max token TTL, then remove them. Nimbus selects the signing key; verifiers match on `kid`. **Gotchas.** - **Never generate the key pair at application startup** in production. A fresh key each boot means: tokens issued before a restart fail verification, and multi-instance deployments each have different keys → intermittent 401s. Load from a keystore/secret manager (env, Vault, JKS/PKCS12). - The `JWKSource` bean is mandatory; without it JWT signing can't work. - Don't put secrets or huge payloads in tokens — they're bearer credentials often logged and size-limited. - Choosing RSA vs EC: EC keys are smaller/faster; ensure `id_token_signing_alg_values_supported` advertises what you use. **When to use.** Customize claims whenever resource servers need roles/tenant/entitlements in the token; manage the JWKSource deliberately in any real deployment (persisted keys + rotation).

  • Why is generating the RSA key pair at startup a bad idea in production?
    Every restart produces a new key, invalidating tokens signed with the old one; and multiple instances would each sign with different keys, causing intermittent verification failures. Load persisted keys from a secret store instead.
  • How do you rotate signing keys with zero downtime?
    Keep multiple JWKs in the JWKSource: add a new key, sign new tokens with it, but keep the old public key published at /oauth2/jwks until all tokens signed with it expire, then remove it. Verifiers match on the kid header.
  • How would you add claims to an opaque access token?
    Use OAuth2TokenCustomizer<OAuth2TokenClaimsContext> instead of the JwtEncodingContext variant, since opaque tokens don't go through the JWT encoder.

saying these in an interview costs you the question

  • Generating signing keys in-memory at startup for production
  • Not providing a JWKSource bean and expecting JWT signing to work
  • Rotating keys by swapping a single key with no overlap, breaking in-flight tokens
  • Putting secrets/PII heavily into access tokens

context