auth flow
This commit is contained in:
parent
500d7de548
commit
645907b38b
@ -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;
|
||||
|
||||
@ -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);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@ -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() {
|
||||
|
||||
<div className="mt-6">
|
||||
<Link
|
||||
href="/signin"
|
||||
href="/signin?signedOut=true"
|
||||
className="inline-block px-8 py-3 bg-white text-gray-800 rounded hover:bg-gray-100 transition-colors mb-4"
|
||||
>
|
||||
Sign In Again
|
||||
|
||||
@ -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'
|
||||
}}
|
||||
>
|
||||
<div className="w-full max-w-md space-y-8">
|
||||
<div>
|
||||
{showManualLoginButton ? (
|
||||
<>
|
||||
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-white">
|
||||
{signedOut || isFromLogout ? 'You have been signed out' : 'Welcome Back'}
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-lg text-white/80">
|
||||
Click below to sign in
|
||||
</p>
|
||||
<div className="mt-6 flex justify-center">
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsRedirecting(true);
|
||||
signIn("keycloak", { callbackUrl: "/" });
|
||||
}}
|
||||
className="px-8 py-3 bg-white text-gray-800 rounded hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-white">
|
||||
<div className="w-full max-w-md p-8 bg-black/60 backdrop-blur-sm rounded-lg shadow-xl">
|
||||
<div className="text-center">
|
||||
<h2 className="text-3xl font-bold text-white mb-6">
|
||||
{signedOut ? 'You have signed out' : 'Welcome Back'}
|
||||
</h2>
|
||||
|
||||
{isRedirecting && !showManualLoginButton && (
|
||||
<p className="text-white/80 mb-4">
|
||||
Redirecting to login...
|
||||
</h2>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{showManualLoginButton ? (
|
||||
<button
|
||||
onClick={() => signIn("keycloak", { callbackUrl: "/" })}
|
||||
className="w-full bg-white text-gray-800 py-3 rounded hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user