Add Spring Authorization Server project: OAuth2/OIDC provider, client and resource server
Three modules on Spring Boot 4.1.1 with Spring Authorization Server 7.1.1: the provider
itself, a relying party, and an API that trusts its tokens. Client registration, PKCE,
a custom consent page and token customisation, with profiles that make each failure
reproducible.
Every claim is backed by captured output in docs/output/as-*.txt, regenerated by
authorization-server/scripts/run-all.sh. Notable findings, verified against the jars:
- OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(HttpSecurity) was deleted
in 7.0, and both configuration classes moved into spring-security-config
- ClientSettings.requireProofKey flipped from false to true, on the authorization server
(1.5.8 -> 7.1.1) and on the OAuth2 client (6.5.1 -> 7.1.1)
- requireProofKey(false) does not make PKCE optional for a public client; the code
verifier is that client's only authentication at the token endpoint
- MediaTypeRequestMatcher(TEXT_HTML) matches Accept: */*, so the token endpoint answers
API callers with 302 -> /login unless setIgnoredMediaTypes(ALL) is called
Also renames the repository to spring-auth-demo and cross-links the new chapter set from
the existing documentation.
This commit is contained in:
106
docs/authorization-server/05-token-customisation.md
Normal file
106
docs/authorization-server/05-token-customisation.md
Normal file
@@ -0,0 +1,106 @@
|
||||
[← 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<JwtEncodingContext>` is picked up automatically by
|
||||
the JWT generator. No annotation, no registration step.
|
||||
|
||||
Declare it as `OAuth2TokenCustomizer<OAuth2TokenClaimsContext>` — 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)
|
||||
Reference in New Issue
Block a user