Skip to content

fix(spring): clear the SecurityContext when a caller's token is rejected - #37

Merged
ilpanich merged 1 commit into
mainfrom
claude/rule8-guardrail-tests
Aug 4, 2026
Merged

fix(spring): clear the SecurityContext when a caller's token is rejected#37
ilpanich merged 1 commit into
mainfrom
claude/rule8-guardrail-tests

Conversation

@ilpanich

@ilpanich ilpanich commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Adds the CONTRACT.md §10.1 rule-8 guardrail regression tests — and fixes a real gap the new tests exposed.

The rule

Rules 1-7 ask whether the token is good. Rule 8 asks whether it is the token the decision is even about. SEC-085 satisfied all seven and was still an authentication bypass: the PHP guard routed a failed verification into a second, successful one against the application's own session, admitting the caller as the app's service account.

The gap this found

AxiamAuthenticationFilter rejected correctly — 401, chain not invoked — but never cleared the ambient SecurityContext. A caller presenting a failed token left behind whatever identity was already on the thread: placed there by an earlier filter, or left over on a pooled container thread. A later read of SecurityContextHolder then sees an identity the caller never authenticated as — the servlet analogue of the SEC-085 substitution.

The filter's own comment already stated the intent — "never let an unexpected exception fall through to an authenticated SecurityContext" — but the code only ever avoided setting one, never cleared a pre-existing one.

writeJsonError now clears the context. It runs only on rejection paths: a request presenting no credential at all returns earlier and never reaches it, so session-authenticated traffic that carries no AXIAM token is untouched.

Tests (4)

  • aFailedCallerTokenIsRejectedAndNoIdentityIsAuthenticated — expired token, correctly signed, right tenant. Fails rule 2 and nothing else.
  • aRejectedCallerDoesNotInheritAPreexistingAuthentication — the one that found the bug. Primes the ambient context with the app's own identity, asserts the precondition that it really is present, then asserts a rejected caller does not inherit it.
  • theAuthenticatedIdentityIsAlwaysTheCallersOwn — the positive half. A guard preferring an ambient credential would pass the negative tests while still being wrong.
  • theFilterConstructorExposesNoSecondCredential — pins the dependency surface (JwksVerifier + tenant id, nothing more) and asserts no field is session-shaped, so the property can't be quietly undone later.

Falsified by construction: test 2 failed against the filter as it stood, which is how the gap surfaced.

Verified locally: mvn test green (full suite).

🤖 Generated with Claude Code

https://claude.ai/code/session_01SkTHvZQMV47t3UwkEtmB1D


Generated by Claude Code

Adds the CONTRACT.md §10.1 rule-8 guardrail regression tests, and fixes
a real gap the new tests exposed in AxiamAuthenticationFilter.

Rule 8 — "subject of the decision" — asks not whether the token is good
but whether it is the token the decision is even about. SEC-085
satisfied rules 1-7 and was still an authentication bypass: the PHP
guard routed a failed verification into a second, successful one
against the application's own session.

The Spring filter rejected correctly (401, chain not invoked) but never
cleared the ambient SecurityContext. A caller presenting a failed token
therefore left behind whatever identity was already on the thread —
placed there by an earlier filter, or left over on a pooled container
thread. A later read of SecurityContextHolder then sees an identity the
caller never authenticated as, which is the servlet analogue of the
SEC-085 substitution. The filter's own comment already stated the
intent ("never let an unexpected exception fall through to an
authenticated SecurityContext"); it only ever avoided *setting* one,
not clearing a pre-existing one.

writeJsonError now clears the context. It runs only on rejection paths:
a request presenting no credential at all returns earlier and never
reaches it, so session-authenticated traffic carrying no AXIAM token is
untouched.

Found by writing the guardrail test, and falsified by it — the test
fails against the filter as it stood.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SkTHvZQMV47t3UwkEtmB1D
@ilpanich
ilpanich merged commit 26debe1 into main Aug 4, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants