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:
77
docs/authorization-server/04-consent-page.md
Normal file
77
docs/authorization-server/04-consent-page.md
Normal file
@@ -0,0 +1,77 @@
|
||||
[← 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)
|
||||
Reference in New Issue
Block a user