From ca93d6a3b29eb9b60b8f3f77a3e6f5e6bfb95735 Mon Sep 17 00:00:00 2001 From: alma Date: Thu, 1 Jan 2026 18:52:01 +0100 Subject: [PATCH] improve sso login flow --- IFRAME_LOGOUT_FIX.md | 89 +++++++++++++++++++++++++++++++++++++++++ app/api/auth/options.ts | 51 ++++++++++++++++++++++- 2 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 IFRAME_LOGOUT_FIX.md diff --git a/IFRAME_LOGOUT_FIX.md b/IFRAME_LOGOUT_FIX.md new file mode 100644 index 00000000..049da544 --- /dev/null +++ b/IFRAME_LOGOUT_FIX.md @@ -0,0 +1,89 @@ +# Iframe Logout Session Invalidation Fix + +## Problem + +When a user logs out from an application inside an iframe: +1. The iframe application calls Keycloak logout endpoint +2. Keycloak session is invalidated +3. NextAuth dashboard still has a valid JWT token +4. When NextAuth tries to refresh the token, Keycloak returns: `{ error: 'invalid_grant', error_description: 'Session not active' }` +5. This causes a `JWT_SESSION_ERROR` and the user sees errors but isn't automatically signed out + +## Root Cause + +The `refreshAccessToken` function was catching all errors generically and setting `error: "RefreshAccessTokenError"`. When the session callback received this error, it would throw, causing a JWT_SESSION_ERROR but not properly signing the user out. + +## Solution + +### 1. Detect Session Invalidation + +In `refreshAccessToken`, we now specifically detect when Keycloak returns `invalid_grant` with "Session not active": + +```typescript +if (refreshedTokens.error === 'invalid_grant' || + refreshedTokens.error_description?.includes('Session not active') || + refreshedTokens.error_description?.includes('Token is not active')) { + return { + ...token, + error: "SessionNotActive", + }; +} +``` + +### 2. Clear Tokens in JWT Callback + +When we detect `SessionNotActive`, we clear the tokens in the JWT callback: + +```typescript +if (refreshedToken.error === "SessionNotActive") { + return { + ...refreshedToken, + accessToken: undefined, + refreshToken: undefined, + idToken: undefined, + }; +} +``` + +### 3. Return Null in Session Callback + +When tokens are missing or session is invalidated, the session callback returns `null`, which makes NextAuth treat the user as unauthenticated: + +```typescript +if (token.error === "SessionNotActive" || !token.accessToken) { + return null as any; // NextAuth will treat user as unauthenticated +} +``` + +## Result + +Now when a user logs out from an iframe application: +1. Keycloak session is invalidated +2. NextAuth detects the invalid session on next token refresh +3. Tokens are cleared +4. Session callback returns null +5. User is automatically treated as unauthenticated +6. NextAuth redirects to sign-in page (via AuthCheck component) + +## Files Modified + +- `app/api/auth/options.ts`: + - Enhanced `refreshAccessToken` to detect `invalid_grant` errors + - Clear tokens when session is invalidated + - Return null from session callback when session is invalid + +## Testing + +To test this fix: +1. Log in to the dashboard +2. Open an iframe application +3. Log out from the iframe application +4. Wait for NextAuth to try to refresh the token (or trigger a page refresh) +5. User should be automatically signed out and redirected to sign-in + +--- + +**Date**: 2024 +**Status**: ✅ Fixed +**Version**: 1.0 + diff --git a/app/api/auth/options.ts b/app/api/auth/options.ts index b60b4bf4..57a56bd2 100644 --- a/app/api/auth/options.ts +++ b/app/api/auth/options.ts @@ -95,6 +95,17 @@ async function refreshAccessToken(token: ExtendedJWT) { const refreshedTokens = await response.json(); if (!response.ok) { + // Check if the error is due to invalid session (e.g., user logged out from iframe) + if (refreshedTokens.error === 'invalid_grant' || + refreshedTokens.error_description?.includes('Session not active') || + refreshedTokens.error_description?.includes('Token is not active')) { + console.log("Keycloak session invalidated (likely logged out from iframe), marking token for removal"); + // Return token with specific error to trigger session invalidation + return { + ...token, + error: "SessionNotActive", + }; + } throw refreshedTokens; } @@ -106,8 +117,19 @@ async function refreshAccessToken(token: ExtendedJWT) { idToken: token.idToken, accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000, }; - } catch (error) { + } catch (error: any) { console.error("Error refreshing access token:", error); + + // Check if it's an invalid_grant error (session invalidated) + if (error?.error === 'invalid_grant' || + error?.error_description?.includes('Session not active') || + error?.error_description?.includes('Token is not active')) { + return { + ...token, + error: "SessionNotActive", + }; + } + return { ...token, error: "RefreshAccessTokenError", @@ -194,13 +216,38 @@ export const authOptions: NextAuthOptions = { } } + // Check if token is expired and needs refresh if (Date.now() < (token.accessTokenExpires as number) * 1000) { return token; } - return refreshAccessToken(token); + // Token expired, try to refresh + const refreshedToken = await refreshAccessToken(token); + + // If refresh failed due to invalid session, clear the token to force re-authentication + if (refreshedToken.error === "SessionNotActive") { + console.log("Keycloak session invalidated, clearing token to force re-authentication"); + // Return a token that will cause session callback to return null + return { + ...refreshedToken, + accessToken: undefined, + refreshToken: undefined, + idToken: undefined, + }; + } + + return refreshedToken; }, async session({ session, token }) { + // If session was invalidated or tokens are missing, return null to sign out + if (token.error === "SessionNotActive" || !token.accessToken) { + console.log("Session invalidated or tokens missing, user will be signed out"); + // Return null to make NextAuth treat user as unauthenticated + // This will trigger automatic redirect to sign-in page + return null as any; + } + + // For other errors, throw to trigger error handling if (token.error) { throw new Error(token.error as string); }