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.
125 lines
5.2 KiB
Markdown
125 lines
5.2 KiB
Markdown
[← 07 Diagnostics](07-diagnostics.md) · [index](README.md) · next: [09 — The entry point and the Accept header](09-entry-point.md)
|
|
|
|
# The relying party
|
|
|
|
Source:
|
|
[`ClientSecurityConfig.java`](../../authorization-server/oidc-client/src/main/java/com/ankurm/client/ClientSecurityConfig.java),
|
|
[`HomeController.java`](../../authorization-server/oidc-client/src/main/java/com/ankurm/client/HomeController.java),
|
|
[`PkceConfig.java`](../../authorization-server/oidc-client/src/main/java/com/ankurm/client/PkceConfig.java).
|
|
|
|
## The whole client side, in one method
|
|
|
|
```java
|
|
http
|
|
.authorizeHttpRequests(auth -> auth.requestMatchers("/", "/error").permitAll()
|
|
.anyRequest().authenticated())
|
|
.oauth2Login(Customizer.withDefaults())
|
|
.oauth2Client(Customizer.withDefaults())
|
|
.logout(logout -> logout.logoutSuccessUrl("/"));
|
|
```
|
|
|
|
Plus one provider entry and one registration in `application.yaml`. Spring reads
|
|
`/.well-known/openid-configuration` at first use and fills in every endpoint from it.
|
|
|
|
## Run the client on 127.0.0.1, not localhost
|
|
|
|
The authorization server is on `localhost:9000` and the client on `127.0.0.1:8080`. Those
|
|
are different origins to a browser cookie jar. Put both on `localhost` and the two
|
|
`JSESSIONID` cookies collide — one app's session clobbers the other's — and you
|
|
get a login loop that looks like a Spring Security bug.
|
|
|
|
## Use the access token, not the id_token
|
|
|
|
```java
|
|
@GetMapping("/orders")
|
|
public String orders(@RegisteredOAuth2AuthorizedClient("demo-web") OAuth2AuthorizedClient client, …)
|
|
```
|
|
|
|
`@RegisteredOAuth2AuthorizedClient` hands you the access token Spring already holds. Reading
|
|
a token out of the `OidcUser` gives you the *id_token* instead, which produces a 401 from a
|
|
resource server with a token that looks perfectly valid — because it is; it is just
|
|
the wrong one. See [05](05-token-customisation.md).
|
|
|
|
## The full flow, hop by hop
|
|
|
|
[`as-client-flow.txt`](../output/as-client-flow.txt) is the real thing, driven with curl so
|
|
every redirect is visible:
|
|
|
|
```
|
|
302 http://127.0.0.1:8080/orders
|
|
302 http://127.0.0.1:8080/oauth2/authorization/demo-web
|
|
302 http://localhost:9000/oauth2/authorize?…&code_challenge=…&code_challenge_method=S256
|
|
200 http://localhost:9000/login
|
|
302 POST http://localhost:9000/login
|
|
302 http://localhost:9000/oauth2/authorize?…&continue
|
|
200 http://localhost:9000/oauth2/consent?…
|
|
302 POST http://localhost:9000/oauth2/authorize
|
|
302 http://127.0.0.1:8080/login/oauth2/code/demo-web?code=g51U-dZi…&state=…
|
|
200 http://127.0.0.1:8080/orders
|
|
```
|
|
|
|
Ten steps for one login. Ending with the resource server's answer rendered by the client:
|
|
|
|
```
|
|
{orders=[{id=1, total=42.00}], subject=alice, scopes=[orders.write, openid, profile,
|
|
orders.read], roles=[ADMIN, USER], tenant=acme, audience=[orders-api]}
|
|
```
|
|
|
|
## The client-side PKCE rule
|
|
|
|
`DefaultOAuth2AuthorizationRequestResolver.getBuilder(...)`, disassembled
|
|
([`as-pkce-applier.txt`](../output/as-pkce-applier.txt)), applies its PKCE customizer when
|
|
**either** the registration's authentication method is `NONE` **or**
|
|
`registration.getClientSettings().isRequireProofKey()`:
|
|
|
|
```
|
|
57: getstatic ClientAuthenticationMethod.NONE
|
|
64: invokevirtual ClientAuthenticationMethod.equals
|
|
67: ifne 80
|
|
71: invokevirtual ClientRegistration$ClientSettings.isRequireProofKey
|
|
77: ifeq 89
|
|
80: getstatic DEFAULT_PKCE_APPLIER
|
|
```
|
|
|
|
And `ClientRegistration.ClientSettings.Builder` initialises `requireProofKey` to `false` in
|
|
Spring Security 6.5.1 and to `true` in 7.1.1. So a confidential Spring client now sends
|
|
PKCE where it previously did not.
|
|
|
|
Note the consequence for configuration: setting an *authorization request customizer* can
|
|
turn PKCE on, but cannot turn it off, because the default applier runs inside `getBuilder`
|
|
independently of the customizer. To disable it you have to rebuild the `ClientRegistration`
|
|
with `requireProofKey(false)`, which is what the `nopkce` profile does.
|
|
|
|
## What a pre-7.0 client looks like against a 7.1 server
|
|
|
|
[`as-client-flow-nopkce.txt`](../output/as-client-flow-nopkce.txt):
|
|
|
|
```
|
|
302 http://127.0.0.1:8080/oauth2/authorization/demo-web
|
|
302 http://localhost:9000/oauth2/authorize?response_type=code&client_id=demo-web&…&nonce=…
|
|
>>> NO code_challenge
|
|
302 http://127.0.0.1:8080/login/oauth2/code/demo-web
|
|
?error=invalid_request
|
|
&error_description=OAuth%202.0%20Parameter%3A%20code_challenge
|
|
200 http://127.0.0.1:8080/login?error
|
|
```
|
|
|
|
The user never sees a login page. They land on the **client's** error page, and nothing in
|
|
the client's logs names the provider as the cause — the reason exists only in a query
|
|
string that the client discards. Fix it on either side: `requireProofKey(false)` on the
|
|
`RegisteredClient`, or `OAuth2AuthorizationRequestCustomizers.withPkce()` on the client.
|
|
Prefer the second.
|
|
|
|
## A dependency-cycle trap
|
|
|
|
A `@Bean` that takes `ClientRegistrationRepository` and returns one is a cycle, and Boot
|
|
refuses to start:
|
|
|
|
```
|
|
Relying upon circular references is discouraged and they are prohibited by default.
|
|
```
|
|
|
|
Post-process the repository Boot already built with a `static BeanPostProcessor` instead.
|
|
|
|
Next: [09 — The entry point and the Accept header](09-entry-point.md)
|