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.
3.3 KiB
← 03 Clients and PKCE · index · next: 05 — Token customisation
The consent page
Wiring a custom consent page is one line:
.authorizationEndpoint(endpoint -> endpoint.consentPage("/oauth2/consent"))
The path is your own MVC controller, served by the browser chain, not the protocol chain.
Source:
ConsentController.java
and consent.html.
The form contract
The undocumented part is what the form has to send back. Getting any of it wrong produces a redirect loop rather than an error.
| requirement | consequence of getting it wrong |
|---|---|
POST to /oauth2/authorize, not to the consent path |
404 or a fresh authorization request |
echo state as the consent page received it |
redirect loop |
echo client_id |
invalid_request |
one scope parameter per approved scope |
consent appears to succeed, token comes back short |
| include the CSRF token | 403 |
omit openid from the checkboxes |
harmless, but unticking it does nothing |
The state is not the client's state
This is the one that costs an afternoon. From
as-authcode-pkce.txt:
GET /oauth2/authorize?…&state=xyz123
-> 302 /oauth2/consent?scope=openid%20orders.read&client_id=demo-spa
&state=RXHrz8avEvUmNxYMLZoT0CyJS2E0t99pJtMJ5fyJBVM%3D
The client sent state=xyz123. The consent page is handed
RXHrz8avEvUmNxYMLZoT0CyJS2E0t99pJtMJ5fyJBVM= — the authorization server's own
correlation handle for the pending request. Echo the client's value instead and the endpoint
cannot find the pending authorization, so it starts a new one, which redirects to the
consent page again. The loop looks like a session problem and is not.
The client's state comes back at the end, untouched, in the redirect to the client:
-> 302 http://127.0.0.1:8080/authorized?code=B6iUSZ…&state=xyz123
Approving and denying
Approve: POST with one scope parameter per approved scope.
Deny: POST with no scope parameters at all. The endpoint then redirects to the client
with error=access_denied.
Consent is remembered
OAuth2AuthorizationConsentService stores what the user approved, keyed by client and
principal. A second authorization for scopes already approved skips the page entirely.
That is why run-all.sh restarts the authorization server between the two client-flow
runs — otherwise the second one silently takes the no-consent path and proves nothing.
The in-memory implementation loses all of it on restart, and is per-instance. Two replicas of your authorization server will ask the same user twice.
Turning consent off
The noconsent profile sets requireAuthorizationConsent(false)
(as-authcode-noconsent.txt). Correct for a
first-party client you own and ship together with the provider. Wrong the moment a third
party registers, because consent is the only point at which the user is told what they are
agreeing to.
Next: 05 — Token customisation