1
0

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:
2026-08-24 08:12:36 +05:30
parent 4dc45d5e00
commit e9381dc5be
89 changed files with 5237 additions and 9 deletions

View File

@@ -0,0 +1,124 @@
[← 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)