1
0
Files
spring-auth-demo/docs/authorization-server/04-consent-page.md
Ankur Mhatre 38c0a5f358 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.
2026-08-24 08:12:36 +05:30

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)