8.8 KiB
SSO Flow Analysis - Keycloak External Logout Issue
Current Flow Trace
Scenario: User logs out from Keycloak directly, then accesses dashboard
Step-by-step flow:
-
Initial State (Before Keycloak Logout)
- User is logged into Dashboard via NextAuth
- NextAuth JWT contains:
accessToken: Valid Keycloak OAuth tokenrefreshToken: Valid Keycloak refresh tokenidToken: Valid Keycloak ID token
- Keycloak session cookies are set in browser
- Iframe applications can authenticate via Keycloak cookies
-
User Logs Out from Keycloak Directly (External Application)
- External application calls:
POST /realms/{realm}/protocol/openid-connect/logout - Keycloak invalidates:
- ✅ Keycloak session cookies (cleared)
- ✅ Keycloak refresh token (invalidated)
- ✅ Keycloak access token (invalidated)
- ❌ NextAuth JWT still contains old tokens (NextAuth doesn't know about logout)
- ❌ NextAuth session cookie still valid (30-day expiration)
- External application calls:
-
User Accesses Dashboard
- Browser sends NextAuth session cookie
- NextAuth decrypts JWT
- JWT contains old (now invalid) tokens
- Token expiration check:
Date.now() < (token.accessTokenExpires as number) * 1000 - If token hasn't expired yet (by timestamp), NextAuth returns existing token
- Problem: Token is invalid in Keycloak, but NextAuth doesn't know yet
-
User Navigates to Iframe Application
ResponsiveIframecomponent mountsuseEffecttriggers:refreshSession()- Calls:
GET /api/auth/refresh-keycloak-session
-
Refresh Endpoint Execution
GET /api/auth/refresh-keycloak-session → getServerSession(authOptions) → Reads NextAuth JWT from cookie → JWT contains old refreshToken (invalid) → Calls Keycloak: POST /token with old refreshToken → Keycloak responds: { error: 'invalid_grant', error_description: 'Token is not active' } → Returns 401 with SessionInvalidated error -
ResponsiveIframe Handles Error
- Detects
SessionInvalidatederror - Redirects to
/signin - User signs in again
- Gets NEW tokens from Keycloak
- Detects
-
User Returns to Iframe (After Re-login)
- Problem: If NextAuth JWT callback hasn't run yet, it might still have old tokens
- OR: The new session is created, but iframe component might be using cached session
- OR: The refresh endpoint is called again before new session is fully established
Root Cause Analysis
Issue 1: Stale Token Detection
Problem: NextAuth only tries to refresh tokens when they're expired (by timestamp). If a token is invalidated externally (Keycloak logout), NextAuth won't know until it tries to refresh.
Current Flow:
JWT Callback:
if (Date.now() < token.accessTokenExpires * 1000) {
return token; // Returns stale token without checking Keycloak
}
// Only refreshes if expired by timestamp
What Should Happen:
- When accessing iframe, we proactively refresh to validate token
- But if refresh fails, we need to clear the NextAuth session immediately
Issue 2: Session Invalidation Timing
Problem: When refresh fails:
- Refresh endpoint returns
SessionInvalidated - ResponsiveIframe redirects to
/signin - User signs in, gets new tokens
- But: NextAuth JWT might still have old tokens cached until next JWT callback execution
Current Behavior:
- Redirect to signin happens
- User re-authenticates
- New session is created
- But old session might still be in browser cache/cookies
Issue 3: Infinite Redirect Loop Potential
Problem: If the refresh endpoint keeps failing:
- ResponsiveIframe redirects to
/signin - User signs in
- Returns to iframe
- Refresh endpoint called again
- If new session isn't fully established, it might still use old tokens
- Loop continues
Current Code Flow
ResponsiveIframe Component Flow
1. Component mounts with session
2. useEffect triggers refreshSession()
3. Calls GET /api/auth/refresh-keycloak-session
4. If 401 + SessionInvalidated:
→ window.location.href = '/signin'
→ User redirected
5. User signs in again
6. Returns to iframe page
7. Component mounts again
8. useEffect triggers refreshSession() again
9. If session still has old tokens → fails again
Refresh Endpoint Flow
GET /api/auth/refresh-keycloak-session
1. getServerSession(authOptions)
→ Reads JWT from cookie
→ JWT callback runs
→ If token expired: refreshAccessToken()
→ If token not expired: returns existing token (might be invalid!)
2. Uses session.refreshToken
3. Calls Keycloak refresh endpoint
4. If invalid_grant: Returns SessionInvalidated
JWT Callback Flow
async jwt({ token, account, profile }) {
// Initial login: account & profile present
if (account && profile) {
// Store tokens
}
// Subsequent requests
else if (token.accessToken) {
// Check expiration
if (Date.now() < token.accessTokenExpires * 1000) {
return token; // ⚠️ Returns token without validating with Keycloak
}
// Only refreshes if expired by timestamp
return refreshAccessToken(token);
}
}
The Problem
Key Issue: NextAuth JWT callback only checks token expiration by timestamp. It doesn't validate that the token is still valid in Keycloak. So:
- User logs out from Keycloak → Token invalidated
- NextAuth JWT still has token (not expired by timestamp)
- JWT callback returns existing token (assumes it's valid)
- Refresh endpoint tries to use invalid refresh token
- Fails, redirects to signin
- User signs in, but if JWT callback hasn't run with new account, might still have old token
Why It Gets Stuck
Looking at the logs:
Failed to refresh Keycloak session: { error: 'invalid_grant', error_description: 'Token is not active' }
GET /api/auth/refresh-keycloak-session 401
→ Redirects to /signin
→ User signs in
→ Returns to iframe
→ refresh-keycloak-session called again
→ Still fails (401)
Possible reasons:
- Session not fully updated: After signin, NextAuth creates new session, but refresh endpoint might be reading old session from cookie before it's updated
- Token not refreshed in JWT: The new tokens from signin might not be stored in JWT yet when refresh endpoint is called
- Cookie caching: Browser might be sending old session cookie
- Race condition: Refresh endpoint called before new session is established
Recommendations (Without Code Changes)
1. Check Session State After Signin
After user signs in and is redirected back:
- Verify that
getServerSession()returns new session with valid tokens - Check that JWT callback has run and stored new tokens
- Ensure session cookie is updated in browser
2. Add Delay/Retry Logic
In ResponsiveIframe:
- After redirect from signin, wait a moment before calling refresh endpoint
- Or check if session has been updated before calling refresh
- Add retry logic with exponential backoff
3. Validate Token Before Using
In refresh endpoint:
- Before using refreshToken, validate that accessToken is still valid
- Or check token age - if token is old, force refresh even if not expired
4. Clear Session on Invalid Token
When refresh fails with invalid_grant:
- Don't just redirect - also clear NextAuth session cookie
- Force complete re-authentication
- Ensure old session is completely removed
5. Check Keycloak Session Status
Before calling refresh endpoint:
- Check if Keycloak session is still active
- Use Keycloak's userinfo endpoint to validate access token
- Only refresh if token is actually invalid
Current Behavior Summary
What's Happening:
- ✅ User logs out from Keycloak → Keycloak invalidates tokens
- ✅ User accesses dashboard → NextAuth still has old tokens (not expired by timestamp)
- ✅ User goes to iframe → Refresh endpoint called
- ✅ Refresh fails → Detects invalid token
- ✅ Redirects to signin → User re-authenticates
- ⚠️ Issue 1: Storage initialization fails during signin (
createUserFolderStructurenot exported) - ⚠️ Issue 2: After re-authentication, refresh endpoint might still be using old session
- ⚠️ Result: Gets stuck in redirect loop or keeps failing
Root Cause: NextAuth doesn't proactively validate tokens with Keycloak. It only checks expiration timestamps. When tokens are invalidated externally, NextAuth doesn't know until it tries to use them.
Additional Issue Confirmed:
- Storage initialization fails during signin process
- Error:
createUserFolderStructure is not a function - This prevents complete signin initialization
- May contribute to session not being fully established
Analysis Date: 2024
Status: Issue Identified
Next Steps: Implement proactive token validation or improve session invalidation handling