1
0
Files
spring-auth-demo/docs/authorization-server/09-entry-point.md
Ankur Mhatre e9381dc5be 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.
2026-08-24 08:20:38 +05:30

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.

Next: 10 — Should you run one at all