From d488979109d062c3ef9d3c29bc2f2013c1cc1d93 Mon Sep 17 00:00:00 2001 From: alma Date: Fri, 2 May 2025 16:38:58 +0200 Subject: [PATCH] auth flow --- app/api/auth/[...nextauth]/route.ts | 30 ++++++++--- app/signin/page.tsx | 26 ++++++++-- lib/session.ts | 78 +++++++++++++++++++++++++++-- 3 files changed, 117 insertions(+), 17 deletions(-) diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index 80e67910..14ee41bc 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -83,16 +83,27 @@ export const authOptions: NextAuthOptions = { callbacks: { async jwt({ token, account, profile }: any) { if (account && profile) { - // Just store access token and critical fields + // Store access token token.accessToken = account.access_token; - // Make sure roles are available - if (profile.role) { + token.refreshToken = account.refresh_token; + + // Extract roles correctly from the raw Keycloak profile + if (profile.realm_access && profile.realm_access.roles) { + // Directly extract roles from the Keycloak profile structure + token.role = profile.realm_access.roles.map( + (role: string) => role.replace(/^ROLE_/, '').toLowerCase() + ); + } else if (profile.role) { + // Fallback to using the role property if already processed token.role = profile.role; - token.username = profile.username || ''; - token.first_name = profile.first_name || ''; - token.last_name = profile.last_name || ''; } + + // Store user information + token.username = profile.preferred_username || profile.username || ''; + token.first_name = profile.given_name || profile.first_name || ''; + token.last_name = profile.family_name || profile.last_name || ''; } + return token; }, async session({ session, token }: any) { @@ -102,7 +113,7 @@ export const authOptions: NextAuthOptions = { session.user.id = token.sub || ""; // Ensure roles are passed to the session - if (token.role) { + if (token.role && Array.isArray(token.role)) { session.user.role = token.role; session.user.username = token.username || ''; session.user.first_name = token.first_name || ''; @@ -114,6 +125,9 @@ export const authOptions: NextAuthOptions = { session.user.first_name = ''; session.user.last_name = ''; } + + // Add debug log to see what roles are being passed + console.log('Session user roles:', session.user.role); } return session; } @@ -133,7 +147,7 @@ export const authOptions: NextAuthOptions = { }, }, }, - debug: false, + debug: true, // Enable debug logs temporarily to see role information }; const handler = NextAuth(authOptions); diff --git a/app/signin/page.tsx b/app/signin/page.tsx index 95d37c78..aac82c3b 100644 --- a/app/signin/page.tsx +++ b/app/signin/page.tsx @@ -11,12 +11,13 @@ export default function SignIn() { const [message, setMessage] = useState(""); useEffect(() => { - // Clear cookies on errors or manual signout + // Always clear cookies on signin page load to ensure fresh authentication + clearAuthCookies(); + + // Set error message if present if (error) { console.log("Clearing auth cookies due to error:", error); - clearAuthCookies(); - // Set error message if (error === "RefreshTokenError" || error === "invalid_grant") { setMessage("Your session has expired. Please sign in again."); } else { @@ -25,9 +26,24 @@ export default function SignIn() { } }, [error]); - // Simple login function + // Login function with callbackUrl to maintain original destination const handleSignIn = () => { - signIn("keycloak", { callbackUrl: "/" }); + // Get the callback URL from the query parameters or use the home page + const callbackUrl = searchParams.get("callbackUrl") || "/"; + + // Add a timestamp parameter to avoid caching issues + const timestamp = new Date().getTime(); + const authParams = { + callbackUrl, + redirect: true, + // Adding a timestamp to force Keycloak to skip any cached session + authParams: { + prompt: "login", + t: timestamp.toString() + } + }; + + signIn("keycloak", authParams); }; return ( diff --git a/lib/session.ts b/lib/session.ts index 756b145b..2f8a6c51 100644 --- a/lib/session.ts +++ b/lib/session.ts @@ -114,17 +114,87 @@ export function clearAuthCookies() { // Get all cookies to check for chunked auth cookies const cookies = document.cookie.split(';'); - // Clear main auth cookies + // Try multiple path and domain combinations for thorough cleanup + const paths = ['/', '/auth', '/realms', '/admin']; + const domain = window.location.hostname; + const domains = [ + domain, + `.${domain}`, + domain.split('.').slice(-2).join('.'), + `.${domain.split('.').slice(-2).join('.')}` + ]; + + // Clear main auth cookies with all path/domain combinations authCookies.forEach(cookieName => { + // Basic deletion document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=None; Secure`; + + // Try more aggressive deletion with different paths and domains + paths.forEach(path => { + document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; SameSite=None; Secure`; + + domains.forEach(domainValue => { + document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; domain=${domainValue}; SameSite=None; Secure`; + }); + }); }); - // Check for and clear any chunked cookies + // Check for and clear any chunked cookies and cookies with dynamic names cookies.forEach(cookie => { const cookieName = cookie.split('=')[0].trim(); - // Check for chunked cookies (they end with a number) - if (cookieName.startsWith('next-auth.') && /\.\d+$/.test(cookieName)) { + + // Clear chunked cookies (end with a number) and any auth-related cookies + if (cookieName.startsWith('next-auth.') || + cookieName.includes('keycloak') || + cookieName.includes('auth') || + cookieName.includes('session') || + /\.\d+$/.test(cookieName)) { + + // Basic deletion document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=None; Secure`; + + // Try more aggressive deletion with different paths and domains + paths.forEach(path => { + document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; SameSite=None; Secure`; + + domains.forEach(domainValue => { + document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; domain=${domainValue}; SameSite=None; Secure`; + }); + }); } }); + + // Clear localStorage items related to authentication + try { + const authItems = [ + 'Meteor.loginToken', + 'Meteor.userId', + 'token', + 'refreshToken', + 'userId', + 'userName', + 'userEmail' + ]; + + authItems.forEach(item => { + localStorage.removeItem(item); + }); + + // Also look for any items with auth-related names + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && (key.includes('token') || key.includes('auth') || key.includes('session'))) { + localStorage.removeItem(key); + } + } + } catch (e) { + console.error('Failed to clear localStorage:', e); + } + + // Clear all sessionStorage + try { + sessionStorage.clear(); + } catch (e) { + console.error('Failed to clear sessionStorage:', e); + } } \ No newline at end of file