Skip to content

fix(security): route non-CSRF access denials to a generic 403 handler - #242

Open
detail-app[bot] wants to merge 1 commit into
devfrom
detail/bug-fix/fix-security-route-non-csrf-access-denials-to-a-ge-734cf5
Open

fix(security): route non-CSRF access denials to a generic 403 handler#242
detail-app[bot] wants to merge 1 commit into
devfrom
detail/bug-fix/fix-security-route-non-csrf-access-denials-to-a-ge-734cf5

Conversation

@detail-app

@detail-app detail-app Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Detail bug report: View on Detail

Closes #229

Bug

SecurityConfig.appSecurityFilterChain wired CsrfAccessDeniedHandler as the single, catch-all AccessDeniedHandler for the whole app filter chain. Spring's ExceptionTranslationFilter therefore routed every authenticated AccessDeniedException to that CSRF-specific handler, which always emits {"message":"CSRF token missing or invalid. Refresh the page and retry the request."} with HTTP 403 — including non-CSRF denials thrown from controllers (e.g. AuthenticatedUserController#revokeCurrentApiKey throws AccessDeniedException("API key identity is required for revocation") for any caller that isn't a ClerkApiKeyAuthenticationToken). The real authorization reason was discarded and the caller was falsely told the CSRF token was invalid.

Fix

Installed Spring's DelegatingAccessDeniedHandler in SecurityConfig as the app-wide AccessDeniedHandler:

  • MissingCsrfTokenException / InvalidCsrfTokenExceptionCsrfAccessDeniedHandler (CSRF messaging preserved).
  • Any other AccessDeniedException → new GenericAccessDeniedHandler, which emits a generic JSON 403 {"status":"error","message":"Access denied.","details":null} and never claims a CSRF failure or leaks the internal exception text.

CsrfAccessDeniedHandler is unchanged — only the wiring is corrected, so it no longer receives non-CSRF denials. The fix matches the bug report's recommended approach and uses Spring's built-in DelegatingAccessDeniedHandler rather than a hand-rolled dispatcher.

Testing

  • New RevokeApiKeyAuthIntegrationTest (Spring Boot @SpringBootTest with the dev-profile security chain) pins the three routing branches at the HTTP boundary: authenticated Clerk-JWT + valid CSRF → 403 Access denied. (the bug fix, body contains no CSRF); anonymous + no CSRF → 403 CSRF message; anonymous + valid CSRF → 401 from the entry point.
  • New GenericAccessDeniedHandlerTest pins the handler's contract: generic 403 body, no CSRF-string leak, no internal exception-message leak, committed-response guard, null-mapper fast-fail.
  • CSRF messaging regression is covered by the existing SecurityConfigTest (missing/mismatched/single-sided CSRF tokens, non-ak_ Bearer still CSRF-gated) and the endpoint auth paths by AuthenticatedUserEndpointSecurityIntegrationTest (ak_... 204, Clerk-outage 503, anonymous 401) — both unchanged and passing.
  • Routine checks all pass: make test (1,169 unit tests, 0 failures/errors), make build, make lint (Spotless/Palantir, SpotBugs, PMD, oxlint/eslint/svelte-check, ast-grep), and git diff --check.
  • End-to-end against a running dev-profile server confirmed the CSRF-missing → 403 CSRF message and anonymous + valid CSRF → 401 branches over HTTP. The authenticated Clerk-JWT branch could not be exercised live (minting a Clerk-signed session JWT requires the dev instance's private key, which isn't available); a forged JWT returns 401 at the decoder before reaching the controller, so this path is instead verified by the integration test, which injects an authenticated JwtAuthenticationToken via the repo's standard jwt() post-processor. The ak_... 204 happy path likewise could not run live (no real ak_... key / CLERK_SECRET_KEY); a fake key returns 503 with ClerkApiKeyVerifier requires CLERK_SECRET_KEY, and the success/outage paths are covered by the existing endpoint-security integration test.

Automatic Fixes PRs can be configured here.

SecurityConfig wired CsrfAccessDeniedHandler as the catch-all
AccessDeniedHandler, so every authenticated non-CSRF AccessDeniedException
(including controller-thrown denials) was serialized as a false "CSRF token
missing or invalid" 403. Install a DelegatingAccessDeniedHandler that routes
MissingCsrfTokenException/InvalidCsrfTokenException to the CSRF handler and
all other denials to a new GenericAccessDeniedHandler emitting a generic
"Access denied." JSON 403, so non-CSRF failures no longer claim a CSRF
failure while CSRF messaging is preserved.
@detail-app
detail-app Bot requested a review from WilliamAGH September 6, 2026 14:00
@detail-app detail-app Bot added the bug Something isn't working label Sep 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Detail Bug] API: /api/me/api-key returns CSRF error message for non-CSRF authorization failures

1 participant