auth flow
This commit is contained in:
parent
32f77425de
commit
ed199d7a00
@ -1,11 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { clearAuthCookies } from "@/lib/session";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function LoggedOut() {
|
||||
const [sessionStatus, setSessionStatus] = useState<'checking' | 'cleared' | 'error'>('checking');
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const forceLogout = new URLSearchParams(window.location.search).get('forceLogout') === 'true';
|
||||
|
||||
// Listen for any messages from iframes
|
||||
useEffect(() => {
|
||||
@ -27,6 +29,18 @@ export default function LoggedOut() {
|
||||
// Additional browser storage clearing
|
||||
console.log('Performing complete browser storage cleanup');
|
||||
|
||||
// Add a hidden iframe to directly call Keycloak logout endpoint
|
||||
// This ensures the server-side Keycloak session is properly terminated
|
||||
if (process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER) {
|
||||
console.log('Adding Keycloak logout iframe');
|
||||
const keycloakBaseUrl = process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER;
|
||||
const logoutEndpoint = `${keycloakBaseUrl}/protocol/openid-connect/logout`;
|
||||
|
||||
if (iframeRef.current) {
|
||||
iframeRef.current.src = logoutEndpoint;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get any user ID from localStorage or sessionStorage for server-side cleanup
|
||||
let userId = '';
|
||||
try {
|
||||
@ -124,6 +138,8 @@ export default function LoggedOut() {
|
||||
'rc_token',
|
||||
'rc_uid',
|
||||
'Meteor.loginToken',
|
||||
'AUTH_SESSION_ID',
|
||||
'AUTH_SESSION_ID_LEGACY',
|
||||
...chunkedCookies
|
||||
];
|
||||
|
||||
@ -136,6 +152,13 @@ export default function LoggedOut() {
|
||||
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.${rootDomain}; SameSite=None; Secure;`;
|
||||
}
|
||||
|
||||
// Additional paths that Keycloak might use
|
||||
['/auth', '/realms'].forEach(path => {
|
||||
keycloakCookies.forEach(cookieName => {
|
||||
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; domain=${window.location.hostname}; SameSite=None; Secure;`;
|
||||
});
|
||||
});
|
||||
|
||||
// Notify any parent windows/iframes
|
||||
try {
|
||||
if (window.parent && window.parent !== window) {
|
||||
@ -165,6 +188,13 @@ export default function LoggedOut() {
|
||||
backgroundRepeat: 'no-repeat'
|
||||
}}
|
||||
>
|
||||
{/* Hidden iframe for direct Keycloak logout */}
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
style={{ display: 'none' }}
|
||||
title="keycloak-logout"
|
||||
/>
|
||||
|
||||
<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-4">
|
||||
@ -192,14 +222,14 @@ export default function LoggedOut() {
|
||||
|
||||
<div className="mt-6">
|
||||
<Link
|
||||
href="/signin?signedOut=true"
|
||||
href="/signin?fresh=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
|
||||
</Link>
|
||||
|
||||
<p className="text-white/60 text-sm mt-4">
|
||||
Note: If you're automatically signed in again, try clearing your browser cookies or restarting your browser.
|
||||
Note: You'll need to enter your credentials when signing in again.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -9,9 +9,57 @@ export default function SignIn() {
|
||||
const { data: session } = useSession();
|
||||
const searchParams = useSearchParams();
|
||||
const signedOut = searchParams.get('signedOut') === 'true';
|
||||
const forceFreshLogin = searchParams.get('fresh') === 'true';
|
||||
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||
const [isFromLogout, setIsFromLogout] = useState(false);
|
||||
|
||||
// Clear all Keycloak cookies when the fresh parameter is present
|
||||
useEffect(() => {
|
||||
if (forceFreshLogin) {
|
||||
console.log('Fresh login requested, clearing all Keycloak cookies');
|
||||
|
||||
// Clear auth cookies to ensure a fresh login
|
||||
clearAuthCookies();
|
||||
|
||||
// Extra cleanup for Keycloak-specific cookies with different paths
|
||||
const keycloakCookies = [
|
||||
'KEYCLOAK_SESSION',
|
||||
'KEYCLOAK_IDENTITY',
|
||||
'KEYCLOAK_REMEMBER_ME',
|
||||
'KC_RESTART',
|
||||
'AUTH_SESSION_ID',
|
||||
'AUTH_SESSION_ID_LEGACY',
|
||||
'JSESSIONID'
|
||||
];
|
||||
|
||||
const domains = [
|
||||
window.location.hostname,
|
||||
`.${window.location.hostname}`,
|
||||
window.location.hostname.split('.').slice(-2).join('.'),
|
||||
`.${window.location.hostname.split('.').slice(-2).join('.')}`
|
||||
];
|
||||
|
||||
// Clear each cookie with various paths and domains
|
||||
keycloakCookies.forEach(cookieName => {
|
||||
domains.forEach(domain => {
|
||||
const paths = ['/', '/auth', '/realms'];
|
||||
paths.forEach(path => {
|
||||
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; domain=${domain}; SameSite=None; Secure`;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Also check for chunked cookies
|
||||
const cookies = document.cookie.split(';');
|
||||
cookies.forEach(cookie => {
|
||||
const cookieName = cookie.split('=')[0].trim();
|
||||
if (/\.\d+$/.test(cookieName)) {
|
||||
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=None; Secure`;
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [forceFreshLogin]);
|
||||
|
||||
// If signedOut is true, make sure we clean up any residual sessions
|
||||
useEffect(() => {
|
||||
if (signedOut) {
|
||||
@ -33,8 +81,13 @@ export default function SignIn() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Only automatically sign in if not explicitly signed out and not coming from logout
|
||||
if (!signedOut && !isFromLogout && !isRedirecting && !session) {
|
||||
// Only automatically sign in if:
|
||||
// - Not explicitly signed out
|
||||
// - Not coming from logout
|
||||
// - Not forcing a fresh login
|
||||
// - Not already redirecting
|
||||
// - No session exists
|
||||
if (!signedOut && !isFromLogout && !forceFreshLogin && !isRedirecting && !session) {
|
||||
setIsRedirecting(true);
|
||||
console.log('Triggering automatic sign-in');
|
||||
// Add a small delay to avoid immediate redirect which can cause loops
|
||||
@ -45,7 +98,7 @@ export default function SignIn() {
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [signedOut, isRedirecting, isFromLogout, session]);
|
||||
}, [signedOut, isRedirecting, isFromLogout, forceFreshLogin, session]);
|
||||
|
||||
useEffect(() => {
|
||||
if (session?.user && !session.user.nextcloudInitialized) {
|
||||
@ -62,7 +115,14 @@ export default function SignIn() {
|
||||
}
|
||||
}, [session]);
|
||||
|
||||
const showManualLoginButton = signedOut || isFromLogout || isRedirecting;
|
||||
const showManualLoginButton = signedOut || isFromLogout || isRedirecting || forceFreshLogin;
|
||||
|
||||
// Determine the button text based on the context
|
||||
const buttonText = forceFreshLogin
|
||||
? 'Sign In (Fresh Login)'
|
||||
: signedOut
|
||||
? 'Sign In Again'
|
||||
: 'Sign In';
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -91,13 +151,19 @@ export default function SignIn() {
|
||||
onClick={() => signIn("keycloak", { callbackUrl: "/" })}
|
||||
className="w-full bg-white text-gray-800 py-3 rounded hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
Sign In
|
||||
{buttonText}
|
||||
</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>
|
||||
)}
|
||||
|
||||
{(signedOut || forceFreshLogin) && (
|
||||
<p className="text-white/70 text-sm mt-4">
|
||||
You'll need to enter your credentials again for security reasons
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -35,7 +35,7 @@ export function SignOutHandler() {
|
||||
}
|
||||
}
|
||||
|
||||
// Then, attempt to sign out from NextAuth explicitly
|
||||
// Then, attempt to sign out from NextAuth explicitly with force option
|
||||
await signOut({ redirect: false });
|
||||
|
||||
// Clear Rocket Chat authentication tokens
|
||||
@ -67,7 +67,7 @@ export function SignOutHandler() {
|
||||
// Then clear all auth-related cookies to ensure we break any local sessions
|
||||
clearAuthCookies();
|
||||
|
||||
// Get Keycloak logout URL
|
||||
// Get Keycloak logout URL with additional parameters to force session expiration
|
||||
if (process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER) {
|
||||
console.log('Preparing complete Keycloak logout');
|
||||
|
||||
@ -75,7 +75,7 @@ export function SignOutHandler() {
|
||||
const keycloakBaseUrl = process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER;
|
||||
const logoutEndpoint = `${keycloakBaseUrl}/protocol/openid-connect/logout`;
|
||||
|
||||
// Create form for POST logout (more reliable)
|
||||
// Create form for POST logout (more reliable than GET)
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = logoutEndpoint;
|
||||
@ -100,7 +100,7 @@ export function SignOutHandler() {
|
||||
const redirectInput = document.createElement('input');
|
||||
redirectInput.type = 'hidden';
|
||||
redirectInput.name = 'post_logout_redirect_uri';
|
||||
redirectInput.value = `${window.location.origin}/loggedout`;
|
||||
redirectInput.value = `${window.location.origin}/loggedout?forceLogout=true`;
|
||||
form.appendChild(redirectInput);
|
||||
|
||||
// Add logout_hint=server to explicitly request server-side session cleanup
|
||||
@ -109,6 +109,27 @@ export function SignOutHandler() {
|
||||
logoutHintInput.name = 'logout_hint';
|
||||
logoutHintInput.value = 'server';
|
||||
form.appendChild(logoutHintInput);
|
||||
|
||||
// Add state parameter with random value to prevent CSRF
|
||||
const stateInput = document.createElement('input');
|
||||
stateInput.type = 'hidden';
|
||||
stateInput.name = 'state';
|
||||
stateInput.value = Math.random().toString(36).substring(2);
|
||||
form.appendChild(stateInput);
|
||||
|
||||
// Set initiate_login_uri parameter to force login screen on next login
|
||||
const initiateLoginInput = document.createElement('input');
|
||||
initiateLoginInput.type = 'hidden';
|
||||
initiateLoginInput.name = 'initiate_login_uri';
|
||||
initiateLoginInput.value = `${window.location.origin}/signin?fresh=true`;
|
||||
form.appendChild(initiateLoginInput);
|
||||
|
||||
// Add UI locales parameter
|
||||
const uiLocalesInput = document.createElement('input');
|
||||
uiLocalesInput.type = 'hidden';
|
||||
uiLocalesInput.name = 'ui_locales';
|
||||
uiLocalesInput.value = 'fr';
|
||||
form.appendChild(uiLocalesInput);
|
||||
|
||||
// Notify iframe parents before logging out
|
||||
try {
|
||||
@ -120,17 +141,46 @@ export function SignOutHandler() {
|
||||
console.error('Error notifying parent of logout:', e);
|
||||
}
|
||||
|
||||
// Completely remove all Keycloak session cookies before logout
|
||||
// This helps prevent automatic re-login
|
||||
const keycloakCookies = [
|
||||
'KEYCLOAK_SESSION',
|
||||
'KEYCLOAK_IDENTITY',
|
||||
'KEYCLOAK_REMEMBER_ME',
|
||||
'KC_RESTART',
|
||||
'KEYCLOAK_SESSION_LEGACY',
|
||||
'KEYCLOAK_IDENTITY_LEGACY',
|
||||
'AUTH_SESSION_ID',
|
||||
'AUTH_SESSION_ID_LEGACY',
|
||||
'JSESSIONID'
|
||||
];
|
||||
|
||||
const domains = [
|
||||
window.location.hostname,
|
||||
`.${window.location.hostname}`,
|
||||
window.location.hostname.split('.').slice(-2).join('.'),
|
||||
`.${window.location.hostname.split('.').slice(-2).join('.')}`
|
||||
];
|
||||
|
||||
keycloakCookies.forEach(cookieName => {
|
||||
domains.forEach(domain => {
|
||||
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${domain}; SameSite=None; Secure`;
|
||||
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/realms/; domain=${domain}; SameSite=None; Secure`;
|
||||
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/auth/; domain=${domain}; SameSite=None; Secure`;
|
||||
});
|
||||
});
|
||||
|
||||
// Append to body and submit
|
||||
document.body.appendChild(form);
|
||||
console.log('Submitting Keycloak logout form with server-side logout');
|
||||
form.submit();
|
||||
} else {
|
||||
console.log('No Keycloak configuration found, performing simple redirect');
|
||||
window.location.href = '/loggedout';
|
||||
window.location.href = '/loggedout?forceLogout=true';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during logout:', error);
|
||||
window.location.href = '/loggedout';
|
||||
window.location.href = '/loggedout?forceLogout=true';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -1,27 +1,80 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
// Maximum cookie size in bytes (a bit less than 4KB to be safe)
|
||||
const MAX_COOKIE_SIZE = 3800;
|
||||
// Maximum cookie size in bytes (even more conservative to prevent chunking issues)
|
||||
const MAX_COOKIE_SIZE = 3500;
|
||||
|
||||
// This middleware runs before any request
|
||||
export function middleware(request: NextRequest) {
|
||||
// Force NextAuth environment variables at runtime
|
||||
// Force NextAuth environment variables at runtime to prevent cookie chunking
|
||||
process.env.NEXTAUTH_COOKIE_SIZE_LIMIT = MAX_COOKIE_SIZE.toString();
|
||||
|
||||
// Set defaults for cookie security
|
||||
// Disable cookie callbacks which can increase cookie size
|
||||
process.env.NEXTAUTH_CALLBACK = 'false';
|
||||
process.env.NEXTAUTH_SESSION_STORE_SESSION_TOKEN = 'false';
|
||||
process.env.NEXTAUTH_JWT_STORE_RAW_TOKEN = 'false';
|
||||
|
||||
// Continue with the request
|
||||
// Check if this is a logout-related request
|
||||
const url = request.nextUrl.pathname;
|
||||
const isLogoutPath = url.includes('/signout') || url.includes('/loggedout');
|
||||
const isSigninPath = url.includes('/signin');
|
||||
const hasForceLogoutParam = request.nextUrl.searchParams.has('forceLogout');
|
||||
|
||||
if (isLogoutPath || hasForceLogoutParam) {
|
||||
// On logout pages, we want to ensure cookies are cleaned up
|
||||
const response = NextResponse.next();
|
||||
|
||||
// List of authentication-related cookies to clear on logout
|
||||
const authCookies = [
|
||||
'next-auth.session-token',
|
||||
'next-auth.callback-url',
|
||||
'next-auth.csrf-token',
|
||||
'next-auth.pkce.code-verifier',
|
||||
'__Secure-next-auth.session-token',
|
||||
'__Host-next-auth.csrf-token'
|
||||
];
|
||||
|
||||
// Clear each cookie with appropriate settings
|
||||
authCookies.forEach(cookieName => {
|
||||
// Try to detect and clear chunked cookies too
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const chunkName = i > 0 ? `${cookieName}.${i}` : cookieName;
|
||||
|
||||
// Clear with domain
|
||||
if (process.env.NEXTAUTH_COOKIE_DOMAIN) {
|
||||
response.cookies.delete({
|
||||
name: chunkName,
|
||||
domain: process.env.NEXTAUTH_COOKIE_DOMAIN,
|
||||
path: '/'
|
||||
});
|
||||
}
|
||||
|
||||
// Also clear without domain
|
||||
response.cookies.delete({
|
||||
name: chunkName,
|
||||
path: '/'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// For the signin page with fresh=true param, pass a header indicating fresh login
|
||||
if (isSigninPath && request.nextUrl.searchParams.get('fresh') === 'true') {
|
||||
const response = NextResponse.next();
|
||||
response.headers.set('X-Force-Fresh-Login', 'true');
|
||||
return response;
|
||||
}
|
||||
|
||||
// Continue with the request for all other paths
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// Configure the middleware to run on specific paths
|
||||
export const config = {
|
||||
matcher: [
|
||||
// Apply to all routes except static files and api routes that aren't auth
|
||||
// Apply to all routes except static files
|
||||
'/((?!_next/static|_next/image|favicon.ico|public).*)',
|
||||
],
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user