[← 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)