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.
2.7 KiB
← 08 The relying party · index · next: 10 — Should you run one at all
The entry point and the Accept header
The symptom
A failed token request answers 302 -> /login instead of a JSON 401. Your API client
follows the redirect, gets 200 and an HTML login page, and reports “the token endpoint
returned HTML”.
The cause
The authorization server chain needs two behaviours from one entry point: send a browser
hitting /oauth2/authorize to the login page, and send a machine hitting /oauth2/token
a protocol error. The documented way to express that is:
.exceptionHandling(ex -> ex.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)))
On its own, that does not work. MediaTypeRequestMatcher treats */* as matching
text/html, and */* is what curl, most HTTP clients, and anything that does not set
Accept send. So the matcher fires for API callers too.
The fix
MediaTypeRequestMatcher matcher = new MediaTypeRequestMatcher(MediaType.TEXT_HTML);
matcher.setIgnoredMediaTypes(Set.of(MediaType.ALL));
The difference, measured
as-entrypoint-accept.txt, same request three ways
against both configurations:
Accept |
without setIgnoredMediaTypes |
with it |
|---|---|---|
*/* |
302 → /login | 401 |
application/json |
401 | 401 |
text/html |
302 → /login | 302 → /login |
The browser case is preserved either way. Only the */* case changes, and that is the case
every API client falls into.
Why only public clients hit it
A confidential client presenting a wrong secret never reaches the entry point at all:
OAuth2ClientAuthenticationFilter writes the error itself, so the Accept header makes no
difference and you get a clean 401. It is the public client — whose only
authentication mechanism is the code verifier — that falls through to the entry point
when there is nothing to authenticate with. Which means the bug is invisible until you add
your first SPA.
The mirror image in the test suite
this.mvc.perform(post("/oauth2/token")
.accept(MediaType.ALL)
.param("grant_type", "authorization_code")
.param("code", "bogus")
.param("client_id", "demo-spa"))
.andExpect(status().isUnauthorized());
Pinning it as a test matters because the fix is one line in an exceptionHandling lambda
and is exactly the kind of thing a later refactor drops.