Skip to content

fix(auth): keep a stale bearer token from blocking sign-in - #57

Open
ExtraToast wants to merge 1 commit into
mainfrom
fix/public-endpoints-stale-bearer
Open

ExtraToast wants to merge 1 commit into
mainfrom
fix/public-endpoints-stale-bearer

Conversation

@ExtraToast

@ExtraToast ExtraToast commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Found while investigating a reported sign-in 401; it is not the cause of that report. The reported failure was /api/oauth2/authorize answering 401 to a non-HTML Accept (#59), followed by twelve session-login attempts that returned 400 invalid-credentials. Seven days of Traefik access logs contain no 401 on session-login other than the probe that produced the table below. This PR closes a latent defect with the same symptom, not the incident.

permitAll does not stop BearerTokenAuthenticationFilter. oauth2ResourceServer sits on the application chain, so that filter runs on every path it covers, public ones included. It authenticates before authorization, so a request carrying an Authorization: Bearer header the decoder rejects fails there and HttpStatusEntryPoint(UNAUTHORIZED) answers before the controller runs.

Measured against production v0.7.0, POST /api/v1/auth/session-login with deliberately wrong credentials:

request status
no cookies, no Authorization 400 urn:problem-type:invalid-credentials
stale SESSION + XSRF-TOKEN cookies 400 — identical
Authorization: Bearer garbage.token.value 401, content-length: 0

Wrong credentials are a DomainException, which GlobalExceptionHandler maps to 400, and cookie state changes nothing — so any 401 from this endpoint can only come from a filter. register, login, refresh, confirm-email and /api/v1/health are affected identically.

The spec makes this reachable rather than hypothetical: client-spec/openapi/auth-api.json declares a global security: [{bearerAuth: []}], so every generated client is told to attach a bearer token to sign-in. sessionLogin in the published TypeScript client carries security: [{key: 'bearerAuth', scheme: 'bearer', type: 'http'}] for that reason. Any consumer configured with a token — a native client, a future UI — sends it to a login endpoint that then refuses the request.

Fix

Public endpoints answer on publicEndpointsSecurityFilterChain at order 2 (forward-auth moves to 3, application to 4). It matches PUBLIC_ENDPOINTS, keeps the CORS source, the same configureCsrf and CsrfCookieFilter so the XSRF-TOKEN cookie is still issued, and permitAll — but registers no resource server. The header is now inert where it carries no meaning. Bearer authentication on protected endpoints is untouched.

Validation

  • Seven new tests in SecurityFilterChainRoutingIntegrationTest.StaleBearerTokenOnPublicEndpoints: session-login (undecodable token, and a wrongly signed JWT), login, register, refresh, confirm-email, /api/v1/health. All seven were run against the unfixed chain first and all seven returned 401.
  • ./gradlew :api:integrationTest214 tests, 0 failures, including TokenSecurityIntegrationTest and SessionManagementIntegrationTest, which pin valid-bearer and session behaviour.
  • ./gradlew :api:test :api:detektMain :api:detektTest :api:detektIntegrationTest — clean.

Risk

  • Low. Every path moved was already permitAll on the application chain, and the CSRF configuration is the same object graph. The new chain's matcher is built from PUBLIC_ENDPOINTS itself, so the two lists cannot drift.
  • No web client in the estate attaches an Authorization header to this host today — checked across auth-ui, home-portal, agents-ui and the legacy app-ui. The exposure is the published clients plus the spec's blanket security, which is worth narrowing separately.

Links

BearerTokenAuthenticationFilter runs before authorization, so a request
carrying an undecodable Authorization header was answered with a bodyless
401 even on a permitAll path. A stale token in a browser profile made
sign-in impossible while a private window worked.

Public endpoints now answer on their own filter chain, which registers no
resource server and keeps the CORS and CSRF configuration of the
application chain.
@ExtraToast ExtraToast added type: bug Something is broken or behaving incorrectly. area: auth Authentication, authorization, sessions, or identity. component: security Security, permissions, secrets, or vulnerability handling. labels Sep 19, 2026
@ExtraToast ExtraToast self-assigned this Sep 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: auth Authentication, authorization, sessions, or identity. component: security Security, permissions, secrets, or vulnerability handling. type: bug Something is broken or behaving incorrectly.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant