improve sso login flow

This commit is contained in:
alma 2026-01-01 18:52:01 +01:00
parent 29507ba7c6
commit ca93d6a3b2
2 changed files with 138 additions and 2 deletions

89
IFRAME_LOGOUT_FIX.md Normal file
View File

@ -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

View File

@ -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);
}