improve sso login flow
This commit is contained in:
parent
29507ba7c6
commit
ca93d6a3b2
89
IFRAME_LOGOUT_FIX.md
Normal file
89
IFRAME_LOGOUT_FIX.md
Normal 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
|
||||||
|
|
||||||
@ -95,6 +95,17 @@ async function refreshAccessToken(token: ExtendedJWT) {
|
|||||||
const refreshedTokens = await response.json();
|
const refreshedTokens = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
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;
|
throw refreshedTokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,8 +117,19 @@ async function refreshAccessToken(token: ExtendedJWT) {
|
|||||||
idToken: token.idToken,
|
idToken: token.idToken,
|
||||||
accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000,
|
accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error("Error refreshing access token:", error);
|
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 {
|
return {
|
||||||
...token,
|
...token,
|
||||||
error: "RefreshAccessTokenError",
|
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) {
|
if (Date.now() < (token.accessTokenExpires as number) * 1000) {
|
||||||
return token;
|
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 }) {
|
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) {
|
if (token.error) {
|
||||||
throw new Error(token.error as string);
|
throw new Error(token.error as string);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user