[← 08 The relying party](08-client.md) · [index](README.md) · next: [10 — Should you run one at all](10-should-you.md) # 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: ```java .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 ```java MediaTypeRequestMatcher matcher = new MediaTypeRequestMatcher(MediaType.TEXT_HTML); matcher.setIgnoredMediaTypes(Set.of(MediaType.ALL)); ``` ## The difference, measured [`as-entrypoint-accept.txt`](../output/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 ```java 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](10-should-you.md)