Skip to content

fix(auth): recover from a blacklisted session instead of looping - #540

Open
nicdavidson wants to merge 2 commits into
developfrom
fix/blacklisted-token-login-loop
Open

nicdavidson wants to merge 2 commits into
developfrom
fix/blacklisted-token-login-loop

Conversation

@nicdavidson

Copy link
Copy Markdown
Contributor

Problem

After changing the admin password in the UI, the admin interface can lock itself out until the user clears browser storage by hand.

  1. A password change blacklists the current session token (intended).
  2. If the dead token stays in the session_token cookie, every page load sends it with the bootstrap GET /api/v2/system/environment.
  3. DF answers 403 The token has been blacklisted: Session terminated. Please re-login.
  4. errorInterceptor only cleared the token on 401, and returned early for silent requests. The environment call is silent, so the token was never cleared and the login page never rendered.

Seen on df-dev: every load produced the 403 on /system/environment, while the same URL with no token returned 200.

Fix

  • errorInterceptor: a 401/403 whose message says the token was blacklisted, the session was terminated, or the token expired, on a request that carried a session token, now clears the token, routes to login (unless already on auth/*), and retries the request once without the token. This check runs before the silent opt-out, so bootstrap recovers. Public endpoints then succeed; protected ones return a plain 401 and take the existing auth branch. SESSION_RETRY (new HttpContextToken) limits it to one retry. The handler is restructured around a local send() so the retry gets the same success/error handling without calling inject() outside the injection context.
  • normalizeError: a 403 with a dead-session message normalizes to kind auth instead of forbidden, so component-level handling agrees.
  • DfPasswordService.updatePassword: only stores a session token the server actually returned.
  • dist/: rebuilt in a separate commit, per repo convention.

Tests

  • New error.interceptor.spec.ts:
    • recovers a silent bootstrap call made with a blacklisted token;
    • retries at most once;
    • leaves a plain permission 403 alone (toast, token kept);
    • still clears and redirects on a plain 401;
    • doesn't redirect while already on the login page.
  • New app-error.spec.ts case: a blacklisted 403 becomes auth, a permission 403 stays forbidden.
  • The three dead-session interceptor tests fail on current develop and pass with this change. The two behavior-preserving tests pass on both.
  • jest --config jest.config.ci.js: 21 suites, 169 tests pass. Prettier and ESLint are clean on the changed files; tsc -p tsconfig.app.json passes.

Not in this PR

  • Server side: DF returns 403 for a terminated session, and rejects public endpoints when an invalid token is attached instead of treating the request as anonymous. A 401 would be the more conventional status. The UI no longer depends on either.
  • Unrelated, seen in the same logs: PUT /api/v2/system/admin/1 (and the matching GET with related=...) returned Resource '1' not found for service 'admin' from the profile/admin page. Worth a separate look.

🤖 Generated with Claude Code

nicdavidson and others added 2 commits October 9, 2026 11:38
Changing the admin password blacklists the current session token. If the
dead token stays in the cookie, every page load sends it with the bootstrap
GET /system/environment and DF answers 403 "The token has been blacklisted:
Session terminated. Please re-login". The error interceptor only cleared
the token on 401, and returned early for silent requests (the environment
call is silent), so the login page never rendered until the user cleared
browser storage by hand.

- errorInterceptor: a 401/403 whose message says the token was blacklisted,
  terminated or expired, on a request that carried a session token, now
  clears the token, routes to login, and retries the request once without
  the token. This runs before the silent opt-out, so bootstrap recovers.
- normalizeError: such 403s normalize to kind auth instead of forbidden.
- DfPasswordService.updatePassword: only store a session token the server
  actually returned.
- Tests cover the bootstrap recovery, the single retry, plain 403s, plain
  401s, and staying put on the login page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nicdavidson added a commit that referenced this pull request Oct 9, 2026
…ckers-presentation-mode

Brings the dead-session recovery into the branch df-dev runs. Conflict in
error.interceptor.ts resolved by keeping the trial-lock branch first (it
must win over the dead-session check, and still applies to silent
requests), then the dead-session clear-and-retry, then the existing
severity routing. error.interceptor.spec.ts now holds both the trial-lock
and the dead-session suites. dist rebuilt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant