auth flow
This commit is contained in:
parent
500d7de548
commit
645907b38b
@ -141,8 +141,21 @@ export const authOptions: NextAuthOptions = {
|
|||||||
strategy: "jwt",
|
strategy: "jwt",
|
||||||
maxAge: 30 * 24 * 60 * 60, // 30 days
|
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: {
|
callbacks: {
|
||||||
async jwt({ token, account, profile }) {
|
async jwt({ token, account, profile }) {
|
||||||
|
// Only include essential data in the JWT to reduce size
|
||||||
if (account && profile) {
|
if (account && profile) {
|
||||||
const keycloakProfile = profile as KeycloakProfile;
|
const keycloakProfile = profile as KeycloakProfile;
|
||||||
const roles = keycloakProfile.realm_access?.roles || [];
|
const roles = keycloakProfile.realm_access?.roles || [];
|
||||||
@ -177,7 +190,7 @@ export const authOptions: NextAuthOptions = {
|
|||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
return refreshAccessToken(token);
|
return refreshAccessToken(token as JWT);
|
||||||
},
|
},
|
||||||
async session({ session, token }) {
|
async session({ session, token }) {
|
||||||
if (token.error) {
|
if (token.error) {
|
||||||
@ -185,6 +198,8 @@ export const authOptions: NextAuthOptions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userRoles = Array.isArray(token.role) ? token.role : [];
|
const userRoles = Array.isArray(token.role) ? token.role : [];
|
||||||
|
|
||||||
|
// Only include essential user data
|
||||||
session.user = {
|
session.user = {
|
||||||
id: token.sub ?? '',
|
id: token.sub ?? '',
|
||||||
email: token.email ?? null,
|
email: token.email ?? null,
|
||||||
@ -196,6 +211,8 @@ export const authOptions: NextAuthOptions = {
|
|||||||
role: userRoles,
|
role: userRoles,
|
||||||
nextcloudInitialized: false,
|
nextcloudInitialized: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Only pass the access token, not the entire token
|
||||||
session.accessToken = token.accessToken;
|
session.accessToken = token.accessToken;
|
||||||
|
|
||||||
return session;
|
return session;
|
||||||
|
|||||||
@ -41,6 +41,21 @@ export function ResponsiveIframe({ src, className = '', allow, style, token }: R
|
|||||||
iframe.src = iframeURL.toString();
|
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
|
// Initial setup
|
||||||
calculateHeight();
|
calculateHeight();
|
||||||
@ -49,12 +64,14 @@ export function ResponsiveIframe({ src, className = '', allow, style, token }: R
|
|||||||
// Event listeners
|
// Event listeners
|
||||||
window.addEventListener('resize', calculateHeight);
|
window.addEventListener('resize', calculateHeight);
|
||||||
window.addEventListener('hashchange', handleHashChange);
|
window.addEventListener('hashchange', handleHashChange);
|
||||||
|
window.addEventListener('message', handleMessage);
|
||||||
iframe.addEventListener('load', calculateHeight);
|
iframe.addEventListener('load', calculateHeight);
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', calculateHeight);
|
window.removeEventListener('resize', calculateHeight);
|
||||||
window.removeEventListener('hashchange', handleHashChange);
|
window.removeEventListener('hashchange', handleHashChange);
|
||||||
|
window.removeEventListener('message', handleMessage);
|
||||||
iframe.removeEventListener('load', calculateHeight);
|
iframe.removeEventListener('load', calculateHeight);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@ -7,6 +7,19 @@ import Link from "next/link";
|
|||||||
export default function LoggedOut() {
|
export default function LoggedOut() {
|
||||||
const [sessionStatus, setSessionStatus] = useState<'checking' | 'cleared' | 'error'>('checking');
|
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
|
// Clear auth cookies again on this page as an extra precaution
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkAndClearSessions = async () => {
|
const checkAndClearSessions = async () => {
|
||||||
@ -44,11 +57,36 @@ export default function LoggedOut() {
|
|||||||
console.error('Error clearing localStorage:', e);
|
console.error('Error clearing localStorage:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Double check for Keycloak specific cookies
|
// Double check for Keycloak specific cookies and chunked cookies
|
||||||
const keycloakCookies = ['KEYCLOAK_SESSION', 'KEYCLOAK_IDENTITY', 'KC_RESTART'];
|
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) {
|
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=/; domain=${window.location.hostname}; SameSite=None; Secure;`;
|
||||||
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
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');
|
setSessionStatus('cleared');
|
||||||
@ -98,7 +136,7 @@ export default function LoggedOut() {
|
|||||||
|
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<Link
|
<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"
|
className="inline-block px-8 py-3 bg-white text-gray-800 rounded hover:bg-gray-100 transition-colors mb-4"
|
||||||
>
|
>
|
||||||
Sign In Again
|
Sign In Again
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
import { signIn, useSession } from "next-auth/react";
|
import { signIn, useSession } from "next-auth/react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
|
import { clearAuthCookies } from "@/lib/session";
|
||||||
|
|
||||||
export default function SignIn() {
|
export default function SignIn() {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
@ -11,6 +12,14 @@ export default function SignIn() {
|
|||||||
const [isRedirecting, setIsRedirecting] = useState(false);
|
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||||
const [isFromLogout, setIsFromLogout] = 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
|
// Check if we came from the loggedout page
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const referrer = document.referrer;
|
const referrer = document.referrer;
|
||||||
@ -65,32 +74,29 @@ export default function SignIn() {
|
|||||||
backgroundRepeat: 'no-repeat'
|
backgroundRepeat: 'no-repeat'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="w-full max-w-md space-y-8">
|
<div className="w-full max-w-md p-8 bg-black/60 backdrop-blur-sm rounded-lg shadow-xl">
|
||||||
<div>
|
<div className="text-center">
|
||||||
{showManualLoginButton ? (
|
<h2 className="text-3xl font-bold text-white mb-6">
|
||||||
<>
|
{signedOut ? 'You have signed out' : 'Welcome Back'}
|
||||||
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-white">
|
</h2>
|
||||||
{signedOut || isFromLogout ? 'You have been signed out' : 'Welcome Back'}
|
|
||||||
</h2>
|
{isRedirecting && !showManualLoginButton && (
|
||||||
<p className="mt-2 text-center text-lg text-white/80">
|
<p className="text-white/80 mb-4">
|
||||||
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">
|
|
||||||
Redirecting to login...
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession, signOut } from "next-auth/react";
|
||||||
import { clearAuthCookies } from "@/lib/session";
|
import { clearAuthCookies } from "@/lib/session";
|
||||||
|
|
||||||
export function SignOutHandler() {
|
export function SignOutHandler() {
|
||||||
@ -10,7 +10,10 @@ export function SignOutHandler() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleSignOut = async () => {
|
const handleSignOut = async () => {
|
||||||
try {
|
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();
|
clearAuthCookies();
|
||||||
|
|
||||||
// Get Keycloak logout URL
|
// Get Keycloak logout URL
|
||||||
@ -56,6 +59,16 @@ export function SignOutHandler() {
|
|||||||
logoutHintInput.value = 'server';
|
logoutHintInput.value = 'server';
|
||||||
form.appendChild(logoutHintInput);
|
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
|
// Append to body and submit
|
||||||
document.body.appendChild(form);
|
document.body.appendChild(form);
|
||||||
console.log('Submitting Keycloak logout form with server-side logout');
|
console.log('Submitting Keycloak logout form with server-side logout');
|
||||||
|
|||||||
@ -124,6 +124,21 @@ export function clearAuthCookies() {
|
|||||||
|
|
||||||
console.log(`Processing ${cookies.length} cookies`);
|
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) {
|
for (const cookie of cookies) {
|
||||||
const [name] = cookie.split('=');
|
const [name] = cookie.split('=');
|
||||||
const trimmedName = name.trim();
|
const trimmedName = name.trim();
|
||||||
@ -145,11 +160,12 @@ export function clearAuthCookies() {
|
|||||||
console.log(`Clearing cookie: ${trimmedName}`);
|
console.log(`Clearing cookie: ${trimmedName}`);
|
||||||
|
|
||||||
// Try different combinations to ensure the cookie is cleared
|
// Try different combinations to ensure the cookie is cleared
|
||||||
const paths = ['/', '/auth', '/realms', '/admin'];
|
const paths = ['/', '/auth', '/realms', '/admin', '/api'];
|
||||||
const domains = [
|
const domains = [
|
||||||
window.location.hostname, // Exact domain
|
window.location.hostname, // Exact domain
|
||||||
`.${window.location.hostname}`, // Domain with leading dot
|
`.${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
|
// Try each combination of path and domain
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user