fix: clear expired jid cookie to prevent auth session login loop#5736
Conversation
…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 EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
Greptile SummaryThis PR fixes an infinite redirect loop on SSO re-login (OIDC/SAML/LDAP) caused by a stale expired
The fix is well-scoped and the root-cause analysis is thorough. Two minor issues worth addressing:
Confidence Score: 4/5
Important Files Changed
|
…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]>
|
Addressed both Greptile suggestions in 7c169da:
|
scott-ray-wilson
left a comment
There was a problem hiding this comment.
tested and verified cookie is now cleared
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
jidhttpOnly cookie retains an expired refresh token JWT after session expiry. Two issues compound:/api/v1/auth/token— When called with an expiredjid,validateRefreshTokenthrowsTokenExpiredError, 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./api/v1/auth/logout— Validates the refresh token before clearing cookies. With an expiredjid, 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-exchangeendpoint only sets a newjidcookie for Google/GitHub/GitLab auth methods, not for SSO. Fresh logins (no prior session) are unaffected because a missingjidreturns 404, which the frontend interceptor ignores.Changes
/api/v1/auth/token: WrapvalidateRefreshTokenin try/catch. On failure, clear thejidcookie (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 viareq.log.warnfor production observability. Thejidcookie now includesmaxAge: 0for RFC 6265 compliance, consistent with the other cookies in the same handler.res.cookie()beforethrowactually send theSet-Cookieheader? The Fastify error handler usesres.status().send(), so headers set viares.cookie()before throwing should be included. This is the key assumption for the/tokenfix.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.decodedToken/tokenVersionare declared withletbut the catch block re-throws, so code after the try/catch only runs on success. Type check passes.Steps to verify the change
JWT_REFRESH_LIFETIME)Type
Checklist
Link to Devin session: https://app.devin.ai/sessions/5803afb9edbb4f5297065d1872f10d54