From 645907b38b8e3123549632b85ece26ba7a430784 Mon Sep 17 00:00:00 2001 From: alma Date: Fri, 2 May 2025 12:08:25 +0200 Subject: [PATCH] auth flow --- app/api/auth/[...nextauth]/route.ts | 19 +++++++++- app/components/responsive-iframe.tsx | 17 +++++++++ app/loggedout/page.tsx | 44 ++++++++++++++++++++-- app/signin/page.tsx | 56 +++++++++++++++------------- components/auth/signout-handler.tsx | 17 ++++++++- lib/session.ts | 20 +++++++++- 6 files changed, 140 insertions(+), 33 deletions(-) diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index 30e0081b..eb742de6 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -141,8 +141,21 @@ export const authOptions: NextAuthOptions = { strategy: "jwt", maxAge: 30 * 24 * 60 * 60, // 30 days }, + cookies: { + sessionToken: { + name: `next-auth.session-token`, + options: { + httpOnly: true, + sameSite: 'none', + path: '/', + secure: true, + domain: process.env.NEXTAUTH_COOKIE_DOMAIN || undefined, + }, + }, + }, callbacks: { async jwt({ token, account, profile }) { + // Only include essential data in the JWT to reduce size if (account && profile) { const keycloakProfile = profile as KeycloakProfile; const roles = keycloakProfile.realm_access?.roles || []; @@ -177,7 +190,7 @@ export const authOptions: NextAuthOptions = { return token; } - return refreshAccessToken(token); + return refreshAccessToken(token as JWT); }, async session({ session, token }) { if (token.error) { @@ -185,6 +198,8 @@ export const authOptions: NextAuthOptions = { } const userRoles = Array.isArray(token.role) ? token.role : []; + + // Only include essential user data session.user = { id: token.sub ?? '', email: token.email ?? null, @@ -196,6 +211,8 @@ export const authOptions: NextAuthOptions = { role: userRoles, nextcloudInitialized: false, }; + + // Only pass the access token, not the entire token session.accessToken = token.accessToken; return session; diff --git a/app/components/responsive-iframe.tsx b/app/components/responsive-iframe.tsx index 70bf24b5..d1b128d1 100644 --- a/app/components/responsive-iframe.tsx +++ b/app/components/responsive-iframe.tsx @@ -41,6 +41,21 @@ export function ResponsiveIframe({ src, className = '', allow, style, token }: R iframe.src = iframeURL.toString(); } }; + + // Handle authentication messages from iframe + const handleMessage = (event: MessageEvent) => { + // Only accept messages from our iframe + if (iframe.contentWindow !== event.source) return; + + const { type, data } = event.data || {}; + + // Handle auth related messages + if (type === 'AUTH_ERROR' || type === 'SESSION_EXPIRED') { + console.log('Auth error in iframe:', data); + // Optionally redirect to login page + // window.location.href = '/signin'; + } + }; // Initial setup calculateHeight(); @@ -49,12 +64,14 @@ export function ResponsiveIframe({ src, className = '', allow, style, token }: R // Event listeners window.addEventListener('resize', calculateHeight); window.addEventListener('hashchange', handleHashChange); + window.addEventListener('message', handleMessage); iframe.addEventListener('load', calculateHeight); // Cleanup return () => { window.removeEventListener('resize', calculateHeight); window.removeEventListener('hashchange', handleHashChange); + window.removeEventListener('message', handleMessage); iframe.removeEventListener('load', calculateHeight); }; }, []); diff --git a/app/loggedout/page.tsx b/app/loggedout/page.tsx index a6aa2ad4..b6a8b4c9 100644 --- a/app/loggedout/page.tsx +++ b/app/loggedout/page.tsx @@ -7,6 +7,19 @@ import Link from "next/link"; export default function LoggedOut() { const [sessionStatus, setSessionStatus] = useState<'checking' | 'cleared' | 'error'>('checking'); + // Listen for any messages from iframes + useEffect(() => { + const messageHandler = (event: MessageEvent) => { + // Handle any auth-related messages from iframes + if (event.data && event.data.type === 'AUTH_ERROR') { + console.log('Received auth error from iframe:', event.data); + } + }; + + window.addEventListener('message', messageHandler); + return () => window.removeEventListener('message', messageHandler); + }, []); + // Clear auth cookies again on this page as an extra precaution useEffect(() => { const checkAndClearSessions = async () => { @@ -44,11 +57,36 @@ export default function LoggedOut() { console.error('Error clearing localStorage:', e); } - // Double check for Keycloak specific cookies - const keycloakCookies = ['KEYCLOAK_SESSION', 'KEYCLOAK_IDENTITY', 'KC_RESTART']; + // Double check for Keycloak specific cookies and chunked cookies + const cookies = document.cookie.split(';'); + const cookieNames = cookies.map(cookie => cookie.split('=')[0].trim()); + + // Look for chunked cookies + const chunkedCookies = cookieNames.filter(name => /\.\d+$/.test(name)); + + const keycloakCookies = [ + 'KEYCLOAK_SESSION', + 'KEYCLOAK_IDENTITY', + 'KC_RESTART', + ...chunkedCookies + ]; + for (const cookieName of keycloakCookies) { document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${window.location.hostname}; SameSite=None; Secure;`; document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; + + // Also try with root domain + const rootDomain = window.location.hostname.split('.').slice(-2).join('.'); + document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.${rootDomain}; SameSite=None; Secure;`; + } + + // Notify any parent windows/iframes + try { + if (window.parent && window.parent !== window) { + window.parent.postMessage({ type: 'SESSION_CLEARED' }, '*'); + } + } catch (e) { + console.error('Error notifying parent window:', e); } setSessionStatus('cleared'); @@ -98,7 +136,7 @@ export default function LoggedOut() {
Sign In Again diff --git a/app/signin/page.tsx b/app/signin/page.tsx index bf95672c..ea931817 100644 --- a/app/signin/page.tsx +++ b/app/signin/page.tsx @@ -3,6 +3,7 @@ import { signIn, useSession } from "next-auth/react"; import { useEffect, useState } from "react"; import { useSearchParams } from "next/navigation"; +import { clearAuthCookies } from "@/lib/session"; export default function SignIn() { const { data: session } = useSession(); @@ -11,6 +12,14 @@ export default function SignIn() { const [isRedirecting, setIsRedirecting] = useState(false); const [isFromLogout, setIsFromLogout] = useState(false); + // If signedOut is true, make sure we clean up any residual sessions + useEffect(() => { + if (signedOut) { + console.log('User explicitly signed out, clearing any residual session data'); + clearAuthCookies(); + } + }, [signedOut]); + // Check if we came from the loggedout page useEffect(() => { const referrer = document.referrer; @@ -65,32 +74,29 @@ export default function SignIn() { backgroundRepeat: 'no-repeat' }} > -
-
- {showManualLoginButton ? ( - <> -

- {signedOut || isFromLogout ? 'You have been signed out' : 'Welcome Back'} -

-

- Click below to sign in -

-
- -
- - ) : ( -

+
+
+

+ {signedOut ? 'You have signed out' : 'Welcome Back'} +

+ + {isRedirecting && !showManualLoginButton && ( +

Redirecting to login... -

+

+ )} + + {showManualLoginButton ? ( + + ) : ( +
+
+
)}
diff --git a/components/auth/signout-handler.tsx b/components/auth/signout-handler.tsx index b80239c3..5379fdd7 100644 --- a/components/auth/signout-handler.tsx +++ b/components/auth/signout-handler.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect } from "react"; -import { useSession } from "next-auth/react"; +import { useSession, signOut } from "next-auth/react"; import { clearAuthCookies } from "@/lib/session"; export function SignOutHandler() { @@ -10,7 +10,10 @@ export function SignOutHandler() { useEffect(() => { const handleSignOut = async () => { try { - // First, clear all auth-related cookies to ensure we break any local sessions + // First, attempt to sign out from NextAuth explicitly + await signOut({ redirect: false }); + + // Then clear all auth-related cookies to ensure we break any local sessions clearAuthCookies(); // Get Keycloak logout URL @@ -56,6 +59,16 @@ export function SignOutHandler() { logoutHintInput.value = 'server'; form.appendChild(logoutHintInput); + // Notify iframe parents before logging out + try { + // Attempt to notify any iframes that might be using this authentication + if (window.parent && window.parent !== window) { + window.parent.postMessage({ type: 'LOGOUT_EVENT' }, '*'); + } + } catch (e) { + console.error('Error notifying parent of logout:', e); + } + // Append to body and submit document.body.appendChild(form); console.log('Submitting Keycloak logout form with server-side logout'); diff --git a/lib/session.ts b/lib/session.ts index 53931e4c..e31785aa 100644 --- a/lib/session.ts +++ b/lib/session.ts @@ -124,6 +124,21 @@ export function clearAuthCookies() { console.log(`Processing ${cookies.length} cookies`); + // Get all cookie names to detect chunks (like next-auth.session-token.0) + const allCookieNames = cookies.map(cookie => cookie.split('=')[0].trim()); + + // Find any chunked cookies + const chunkedCookies = allCookieNames.filter(name => { + return /\.\d+$/.test(name) && authCookiePrefixes.some(prefix => name.startsWith(prefix)); + }); + + if (chunkedCookies.length > 0) { + console.log(`Found ${chunkedCookies.length} chunked cookies:`, chunkedCookies); + } + + // Add detected chunked cookies to our specific cookies list + specificCookies.push(...chunkedCookies); + for (const cookie of cookies) { const [name] = cookie.split('='); const trimmedName = name.trim(); @@ -145,11 +160,12 @@ export function clearAuthCookies() { console.log(`Clearing cookie: ${trimmedName}`); // Try different combinations to ensure the cookie is cleared - const paths = ['/', '/auth', '/realms', '/admin']; + const paths = ['/', '/auth', '/realms', '/admin', '/api']; const domains = [ window.location.hostname, // Exact domain `.${window.location.hostname}`, // Domain with leading dot - window.location.hostname.split('.').slice(-2).join('.') // Root domain + window.location.hostname.split('.').slice(-2).join('.'), // Root domain + `.${window.location.hostname.split('.').slice(-2).join('.')}` // Root domain with leading dot ]; // Try each combination of path and domain