Skip to content

fix: clear expired jid cookie to prevent auth session login loop#5736

Merged
scott-ray-wilson merged 2 commits intomainfrom
devin/1773754638-fix-session-expiry-login-loop
Mar 18, 2026
Merged

fix: clear expired jid cookie to prevent auth session login loop#5736
scott-ray-wilson merged 2 commits intomainfrom
devin/1773754638-fix-session-expiry-login-loop

Conversation

@devin-ai-integration
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot commented Mar 17, 2026

Context

When a user's session expires (max session length reached), attempting to re-login via OIDC/SAML/LDAP results in an infinite redirect loop displaying "Your session has expired. Redirecting to login page..." repeatedly. The only workaround is manually clearing cookies.

Root cause: The jid httpOnly cookie retains an expired refresh token JWT after session expiry. Two issues compound:

  1. /api/v1/auth/token — When called with an expired jid, validateRefreshToken throws TokenExpiredError, returning HTTP 403. The expired cookie persists in the browser, so every subsequent token refresh attempt fails the same way. The frontend interceptor detects this 403 + "token expired" message while an access token exists in memory (set by a successful SSO token exchange), triggering a redirect back to /login — creating the loop.

  2. /api/v1/auth/logout — Validates the refresh token before clearing cookies. With an expired jid, this throws the same error, so the logout fails and cookies are never cleared — the user can't even manually log out to break the loop.

This only affects SSO methods (OIDC, SAML, LDAP) because the /api/v1/sso/token-exchange endpoint only sets a new jid cookie for Google/GitHub/GitLab auth methods, not for SSO. Fresh logins (no prior session) are unaffected because a missing jid returns 404, which the frontend interceptor ignores.

Changes

  • /api/v1/auth/token: Wrap validateRefreshToken in try/catch. On failure, clear the jid cookie (maxAge: 0) before re-throwing. The first failed attempt removes the stale cookie; subsequent calls get a clean 404 instead of 403, breaking the loop.

  • /api/v1/auth/logout: Move cookie-clearing outside the try block so all session cookies are always cleared, even if the refresh token is expired/invalid. Failed token validation is logged at warn level via req.log.warn for production observability. The jid cookie now includes maxAge: 0 for RFC 6265 compliance, consistent with the other cookies in the same handler.

⚠️ Reviewer Notes

  1. Does res.cookie() before throw actually send the Set-Cookie header? The Fastify error handler uses res.status().send(), so headers set via res.cookie() before throwing should be included. This is the key assumption for the /token fix.
  2. Logout swallows token validation errors from validateRefreshToken/logout — if the DB is down, logout returns 200 without server-side session invalidation (cookies are still cleared client-side). These failures are logged at warn level (req.log.warn) so they remain visible in production.
  3. Type narrowingdecodedToken/tokenVersion are declared with let but the catch block re-throws, so code after the try/catch only runs on success. Type check passes.

Steps to verify the change

  1. Log in via OIDC SSO
  2. Wait for session to expire (or manually set a short JWT_REFRESH_LIFETIME)
  3. Attempt to re-login via OIDC — should succeed without looping
  4. Verify normal login/logout flows still work (password, Google, GitHub, GitLab)
  5. Verify that logging out with an expired session clears cookies and redirects to login

Type

  • Fix
  • Feature
  • Improvement
  • Breaking
  • Docs
  • Chore

Checklist

Link to Devin session: https://app.devin.ai/sessions/5803afb9edbb4f5297065d1872f10d54

…ens in logout

When a user's session expires, the jid httpOnly cookie retains an expired
refresh token JWT. This causes a login loop for OIDC/SAML/LDAP users because:

1. The /api/v1/auth/token endpoint returns 403 with the expired jid cookie,
   and the frontend interceptor detects this as a session expiry, redirecting
   back to /login in a loop.

2. The /api/v1/auth/logout endpoint fails to clear cookies because it
   validates the refresh token before clearing, which throws TokenExpiredError.

Changes:
- /api/v1/auth/token: Clear the jid cookie before re-throwing when
  validateRefreshToken fails. This removes the stale cookie so subsequent
  requests get a clean 404 instead of 403, breaking the loop.

- /api/v1/auth/logout: Move cookie-clearing outside the try block so
  cookies are always cleared even if the refresh token is expired/invalid.

Co-Authored-By: ashwin <[email protected]>
@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@maidul98
Copy link
Copy Markdown
Collaborator

maidul98 commented Mar 17, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 17, 2026

Greptile Summary

This PR fixes an infinite redirect loop on SSO re-login (OIDC/SAML/LDAP) caused by a stale expired jid httpOnly cookie persisting in the browser after session expiry. Two targeted changes in auth-router.ts address the root cause:

  • /api/v1/auth/token: Wraps validateRefreshToken in a try/catch; on failure, clears the jid cookie (maxAge: 0) before re-throwing. On the first failed token refresh the stale cookie is removed, so subsequent calls return a clean 404 (which the frontend ignores) instead of a 403 that triggers the redirect loop.
  • /api/v1/auth/logout: Moves all cookie-clearing logic outside the try block so cookies are unconditionally cleared even when validateRefreshToken throws (e.g., because the token is already expired).

The fix is well-scoped and the root-cause analysis is thorough. Two minor issues worth addressing:

  • The jid cookie in the /logout handler is cleared without maxAge: 0, unlike the new token error path and the other two cookies in the same handler. This pre-existing inconsistency means the browser may not immediately delete the cookie on logout if it still has a future expiry.
  • The catch block in /logout silently swallows all errors with no logging, making it impossible in production to distinguish an expected expired-token skip from a genuine infrastructure failure (e.g., database down).

Confidence Score: 4/5

  • Safe to merge; the fix correctly addresses the root cause with minor observability and cookie-clearing consistency issues.
  • The core fix is sound: clearing the jid cookie before re-throwing in /token relies on Fastify preserving pre-throw response headers in its error handler, which is correct behaviour for @fastify/cookie. The logout change is straightforwardly correct — moving unconditional cookie-clearing outside the try block is the right pattern. The two remaining concerns (missing maxAge: 0 on jid in logout, and no logging in the catch block) are minor style/observability issues that don't affect the correctness of the primary fix.
  • No files require special attention beyond the two inline suggestions in backend/src/server/routes/v1/auth-router.ts.

Important Files Changed

Filename Overview
backend/src/server/routes/v1/auth-router.ts Fixes SSO re-login loop by (1) clearing jid cookie before re-throwing in /token on validation failure and (2) moving cookie-clearing outside the try block in /logout. Minor: jid in logout is cleared without maxAge: 0, inconsistent with the new token error path; catch block in logout silently swallows all errors with no logging.

Comments Outside Diff (1)

  1. backend/src/server/routes/v1/auth-router.ts, line 36-41 (link)

    P2 jid cookie cleared without maxAge: 0 in logout

    The jid cookie here is cleared by setting its value to "" but without maxAge: 0. Per RFC 6265, a cookie is only reliably deleted when Max-Age is set to 0 (or Expires is set to a past date). Without this, the browser will keep the cookie until its original expiry time, which means an expired-but-not-yet-browser-expired jid cookie may persist past this logout call.

    By contrast, the new /token error handler (line 107–113) clears the same cookie correctly with maxAge: 0. Although this inconsistency is pre-existing, the PR touches this exact area and it's worth aligning both paths:

Last reviewed commit: 7821af6

…maxAge: 0 on jid cookie

- Add req.log.warn in logout catch block for production observability
- Add maxAge: 0 to jid cookie clearing in logout for RFC 6265 compliance,
  consistent with the /token error handler

Co-Authored-By: ashwin <[email protected]>
@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

Addressed both Greptile suggestions in 7c169da:

  1. maxAge: 0 on jid cookie in logout — Added for RFC 6265 compliance, now consistent with the /token error handler and the other two cookies in the same handler.
  2. Logging in the catch block — Added req.log.warn(err, "Logout token validation failed; proceeding to clear cookies") so expired-token skips vs genuine infrastructure failures are distinguishable in production logs.

Copy link
Copy Markdown
Contributor

@scott-ray-wilson scott-ray-wilson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tested and verified cookie is now cleared

@scott-ray-wilson scott-ray-wilson merged commit 95b6933 into main Mar 18, 2026
12 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