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.
78 lines
3.3 KiB
Markdown
78 lines
3.3 KiB
Markdown
[← 03 Clients and PKCE](03-clients-and-pkce.md) · [index](README.md) · next: [05 — Token customisation](05-token-customisation.md)
|
|
|
|
# The consent page
|
|
|
|
Wiring a custom consent page is one line:
|
|
|
|
```java
|
|
.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`](../../authorization-server/auth-server/src/main/java/com/ankurm/authserver/web/ConsentController.java)
|
|
and [`consent.html`](../../authorization-server/auth-server/src/main/resources/templates/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`](../output/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`](../output/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](05-token-customisation.md)
|