[← 04 Consent page](04-consent-page.md) · [index](README.md) · next: [06 — The resource server side](06-resource-server.md) # Token customisation Source: [`TokenClaimsCustomizer.java`](../../authorization-server/auth-server/src/main/java/com/ankurm/authserver/token/TokenClaimsCustomizer.java). ## The bean is found by generic type, and nothing logs if it is not One bean of type `OAuth2TokenCustomizer` is picked up automatically by the JWT generator. No annotation, no registration step. Declare it as `OAuth2TokenCustomizer` — the type used for *opaque* tokens — and it is silently ignored. The generator resolves the bean by generic type and simply does not find it. Your claims are just absent, and nothing in the logs says why. ## What the default access token actually contains Diff two runs of the same flow, with and without the customiser ([`as-authcode-pkce.txt`](../output/as-authcode-pkce.txt) vs [`as-authcode-noclaims.txt`](../output/as-authcode-noclaims.txt)): ``` default (noclaims) with the customiser { { "aud": "demo-spa", <--> "aud": "orders-api", "roles": ["ADMIN", "USER"], "tenant": "acme", "exp": …, "exp": …, "iat": …, "iat": …, "iss": "http://localhost:9000", "iss": "http://localhost:9000", "jti": …, "jti": …, "nbf": …, "nbf": …, "scope": ["openid","orders.read"], "scope": ["openid","orders.read"], "sub": "alice" "sub": "alice" } } ``` Two things worth noticing. **`aud` defaults to the client id.** Not the API. There is no per-client audience setting on `RegisteredClient`, so if your resource servers validate audience — and they should — the token customiser is where you set it. A resource server that naively checks `aud == "orders-api"` will reject every default-issued token. **Roles are not there by default.** `scope` is, as `SCOPE_*` authorities. Anything else about the user — roles, tenant, entitlements — you put there or you make a network call per request. ## Guard on the grant type `client_credentials` has no user. `context.getPrincipal()` is the client's own authentication, and copying its authorities into a `roles` claim gives a machine token whatever the client authentication happened to carry. The customiser here excludes that grant explicitly. ## The id_token is a different token ```java if (OidcParameterNames.ID_TOKEN.equals(context.getTokenType().getValue())) { … } ``` The `id_token`'s audience is the **client**; the access token's is the **API**. From the transcript: ``` access token "aud": "orders-api" id_token "aud": "demo-spa", "azp": "demo-spa", "sid": "1ZK2c__DhcDY…" ``` Sending the `id_token` to a resource server is the classic mix-up. It verifies — same issuer, same signing key — and then fails the audience check: ``` HTTP/1.1 401 WWW-Authenticate: Bearer error="invalid_token", error_description="An error occurred while attempting to decode the Jwt: the required audience orders-api is missing", … ``` If nobody checks audience, it *passes*, and a token the client was allowed to read becomes a token the API accepts. That is the argument for [06](06-resource-server.md). Put authorisation data in the access token. Put profile data in the `id_token`. The `id_token` is for the client to render a username; it is not a credential for your APIs. ## Self-contained versus reference tokens `TokenSettings.accessTokenFormat` takes `SELF_CONTAINED` (a signed JWT, verified offline) or `REFERENCE` (an opaque string). The `opaque` profile flips `demo-service` to the latter ([`as-client-credentials-opaque.txt`](../output/as-client-credentials-opaque.txt)): ``` The access token is an opaque reference: unf4kl7MSFlYyNpNqcVFcIT4Hbny… Length 128. It carries no claims; the resource server must introspect it. POST /oauth2/introspect { "active": true, "sub": "demo-service", "scope": "orders.read", … } ``` The trade is instant revocation for a network round trip on every API call. Note that the introspection response reports `"aud": ["demo-service"]` — the customiser did not run, because opaque tokens go through `OAuth2TokenClaimsContext`, not `JwtEncodingContext`. Next: [06 — The resource server side](06-resource-server.md)