auth flow
This commit is contained in:
parent
a9294b0231
commit
ce09b18a6f
@ -1,308 +1,58 @@
|
||||
import NextAuth, { NextAuthOptions } from "next-auth";
|
||||
import KeycloakProvider from "next-auth/providers/keycloak";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
|
||||
interface KeycloakProfile {
|
||||
sub: string;
|
||||
email?: string;
|
||||
name?: string;
|
||||
roles?: string[];
|
||||
preferred_username?: string;
|
||||
given_name?: string;
|
||||
family_name?: string;
|
||||
realm_access?: {
|
||||
roles: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface DecodedToken {
|
||||
realm_access?: {
|
||||
roles: string[];
|
||||
};
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
declare module "next-auth" {
|
||||
interface Session {
|
||||
user: {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
email?: string | null;
|
||||
image?: string | null;
|
||||
username: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
role: string[];
|
||||
nextcloudInitialized?: boolean;
|
||||
};
|
||||
accessToken?: string;
|
||||
}
|
||||
|
||||
interface JWT {
|
||||
sub?: string;
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
accessTokenExpires?: number;
|
||||
role?: string[];
|
||||
username?: string;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
error?: string;
|
||||
email?: string | null;
|
||||
name?: string | null;
|
||||
}
|
||||
}
|
||||
|
||||
function getRequiredEnvVar(name: string): string {
|
||||
const value = process.env[name];
|
||||
if (!value) {
|
||||
throw new Error(`Missing required environment variable: ${name}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async function refreshAccessToken(token: JWT) {
|
||||
try {
|
||||
console.log('Attempting to refresh access token');
|
||||
const response = await fetch(`${process.env.KEYCLOAK_ISSUER}/protocol/openid-connect/token`, {
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
client_id: process.env.KEYCLOAK_CLIENT_ID!,
|
||||
client_secret: process.env.KEYCLOAK_CLIENT_SECRET!,
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: token.refreshToken,
|
||||
}),
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
const refreshedTokens = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Token refresh failed with status:', response.status);
|
||||
console.error('Error response:', refreshedTokens);
|
||||
throw refreshedTokens;
|
||||
}
|
||||
|
||||
console.log('Token refresh successful');
|
||||
return {
|
||||
...token,
|
||||
accessToken: refreshedTokens.access_token,
|
||||
refreshToken: refreshedTokens.refresh_token ?? token.refreshToken,
|
||||
accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000,
|
||||
error: undefined, // Clear any previous errors
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error refreshing access token:", error);
|
||||
return {
|
||||
...token,
|
||||
error: "RefreshAccessTokenError",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Simple, minimal implementation - NO REFRESH TOKEN LOGIC
|
||||
export const authOptions: NextAuthOptions = {
|
||||
providers: [
|
||||
KeycloakProvider({
|
||||
clientId: getRequiredEnvVar("KEYCLOAK_CLIENT_ID"),
|
||||
clientSecret: getRequiredEnvVar("KEYCLOAK_CLIENT_SECRET"),
|
||||
issuer: getRequiredEnvVar("KEYCLOAK_ISSUER"),
|
||||
authorization: {
|
||||
params: {
|
||||
scope: "openid profile email roles"
|
||||
}
|
||||
},
|
||||
profile(profile) {
|
||||
// Simplified profile logging to reduce console noise
|
||||
console.log('Keycloak profile received');
|
||||
|
||||
// Get roles from realm_access
|
||||
const roles = profile.realm_access?.roles || [];
|
||||
|
||||
// Clean up roles by removing ROLE_ prefix and converting to lowercase
|
||||
const cleanRoles = roles.map((role: string) =>
|
||||
role.replace(/^ROLE_/, '').toLowerCase()
|
||||
);
|
||||
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.name ?? profile.preferred_username,
|
||||
email: profile.email,
|
||||
first_name: profile.given_name ?? '',
|
||||
last_name: profile.family_name ?? '',
|
||||
username: profile.preferred_username ?? profile.email?.split('@')[0] ?? '',
|
||||
role: cleanRoles,
|
||||
}
|
||||
},
|
||||
clientId: process.env.KEYCLOAK_CLIENT_ID || "",
|
||||
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET || "",
|
||||
issuer: process.env.KEYCLOAK_ISSUER || "",
|
||||
}),
|
||||
],
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
maxAge: 12 * 60 * 60, // Reduce to 12 hours to help with token size
|
||||
maxAge: 8 * 60 * 60, // 8 hours only
|
||||
},
|
||||
callbacks: {
|
||||
// Simple JWT callback - no refresh logic
|
||||
async jwt({ token, account }) {
|
||||
if (account) {
|
||||
// Initial sign-in, store tokens
|
||||
token.accessToken = account.access_token;
|
||||
token.sub = account.providerAccountId;
|
||||
}
|
||||
return token;
|
||||
},
|
||||
// Simple session callback
|
||||
async session({ session, token }) {
|
||||
session.accessToken = token.accessToken;
|
||||
if (session.user) {
|
||||
session.user.id = token.sub || "";
|
||||
}
|
||||
return session;
|
||||
}
|
||||
},
|
||||
// Redirect to signin page for any errors
|
||||
pages: {
|
||||
signIn: '/signin',
|
||||
error: '/signin',
|
||||
},
|
||||
// Set reasonable cookie options
|
||||
cookies: {
|
||||
sessionToken: {
|
||||
name: `next-auth.session-token`,
|
||||
name: 'next-auth.session-token',
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
path: '/',
|
||||
secure: true,
|
||||
domain: process.env.NEXTAUTH_COOKIE_DOMAIN || undefined,
|
||||
maxAge: 12 * 60 * 60, // Match session maxAge
|
||||
},
|
||||
},
|
||||
},
|
||||
jwt: {
|
||||
// Maximum JWT size to prevent chunking
|
||||
maxAge: 12 * 60 * 60, // Reduce to 12 hours
|
||||
},
|
||||
callbacks: {
|
||||
async jwt({ token, account, profile }) {
|
||||
// Initial sign in
|
||||
if (account && profile) {
|
||||
const keycloakProfile = profile as KeycloakProfile;
|
||||
const roles = keycloakProfile.realm_access?.roles || [];
|
||||
|
||||
// Only include admin, owner, user roles (most critical)
|
||||
const criticalRoles = roles
|
||||
.filter(role =>
|
||||
role.includes('admin') ||
|
||||
role.includes('owner') ||
|
||||
role.includes('user')
|
||||
)
|
||||
.map(role => role.replace(/^ROLE_/, '').toLowerCase());
|
||||
|
||||
// Store absolute minimal data in the token
|
||||
token.accessToken = account.access_token;
|
||||
token.refreshToken = account.refresh_token;
|
||||
token.accessTokenExpires = account.expires_at ?? 0;
|
||||
token.sub = keycloakProfile.sub;
|
||||
token.role = criticalRoles.length > 0 ? criticalRoles : ['user']; // Only critical roles
|
||||
token.username = keycloakProfile.preferred_username?.substring(0, 30) ?? '';
|
||||
token.error = undefined;
|
||||
|
||||
// Don't store first/last name in the token to save space
|
||||
// Applications can get these from the userinfo endpoint if needed
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
// Check if token has expired
|
||||
const tokenExpiresAt = token.accessTokenExpires ? token.accessTokenExpires as number : 0;
|
||||
const currentTime = Date.now();
|
||||
const hasExpired = currentTime >= tokenExpiresAt;
|
||||
|
||||
// If the token is still valid, return it
|
||||
if (!hasExpired) {
|
||||
return token;
|
||||
}
|
||||
|
||||
// If refresh token is missing, force sign in
|
||||
if (!token.refreshToken) {
|
||||
console.warn('No refresh token available, session cannot be refreshed');
|
||||
return {
|
||||
...token,
|
||||
error: "RefreshAccessTokenError"
|
||||
};
|
||||
}
|
||||
|
||||
// Try to refresh the token
|
||||
const refreshedToken = await refreshAccessToken(token as JWT);
|
||||
|
||||
// If there was an error refreshing, mark token for re-authentication
|
||||
if (refreshedToken.error) {
|
||||
console.warn('Token refresh failed, user will need to reauthenticate');
|
||||
return {
|
||||
...refreshedToken,
|
||||
error: "RefreshAccessTokenError"
|
||||
};
|
||||
}
|
||||
|
||||
return refreshedToken;
|
||||
},
|
||||
async session({ session, token }) {
|
||||
try {
|
||||
// Handle the error from jwt callback
|
||||
if (token.error === "RefreshAccessTokenError") {
|
||||
console.warn("Session encountered a refresh token error, redirecting to login");
|
||||
// Return minimal session with error flag that will trigger re-auth in client
|
||||
return {
|
||||
...session,
|
||||
error: "RefreshTokenError",
|
||||
user: {
|
||||
id: token.sub ?? '',
|
||||
role: ['user'], // Default role
|
||||
username: '', // Empty username
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
name: null,
|
||||
email: null,
|
||||
image: null,
|
||||
nextcloudInitialized: false,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const userRoles = Array.isArray(token.role) ? token.role : [];
|
||||
|
||||
// Create an extremely minimal user object
|
||||
session.user = {
|
||||
id: token.sub ?? '',
|
||||
email: token.email ?? null,
|
||||
name: token.name ?? null,
|
||||
image: null,
|
||||
username: token.username ?? '',
|
||||
first_name: token.first_name ?? '',
|
||||
last_name: token.last_name ?? '',
|
||||
role: userRoles,
|
||||
// Don't include nextcloudInitialized or other non-essential fields
|
||||
};
|
||||
|
||||
// Only store access token, not the entire token
|
||||
session.accessToken = token.accessToken;
|
||||
|
||||
return session;
|
||||
} catch (error) {
|
||||
console.error("Error in session callback:", error);
|
||||
// Return minimal session with error flag
|
||||
return {
|
||||
...session,
|
||||
error: "SessionError",
|
||||
user: {
|
||||
id: token.sub ?? '',
|
||||
role: ['user'],
|
||||
username: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
name: null,
|
||||
email: null,
|
||||
image: null,
|
||||
nextcloudInitialized: false,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
pages: {
|
||||
signIn: '/signin',
|
||||
error: '/signin',
|
||||
},
|
||||
debug: false, // Disable debug to reduce cookie size from logging
|
||||
debug: false,
|
||||
};
|
||||
|
||||
const handler = NextAuth(authOptions);
|
||||
export { handler as GET, handler as POST };
|
||||
|
||||
interface JWT {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
accessTokenExpires: number;
|
||||
}
|
||||
|
||||
interface Profile {
|
||||
sub?: string;
|
||||
email?: string;
|
||||
name?: string;
|
||||
roles?: string[];
|
||||
}
|
||||
|
||||
|
||||
308
app/api/auth/[...nextauth]/route.ts.bak
Normal file
308
app/api/auth/[...nextauth]/route.ts.bak
Normal file
@ -0,0 +1,308 @@
|
||||
import NextAuth, { NextAuthOptions } from "next-auth";
|
||||
import KeycloakProvider from "next-auth/providers/keycloak";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
|
||||
interface KeycloakProfile {
|
||||
sub: string;
|
||||
email?: string;
|
||||
name?: string;
|
||||
roles?: string[];
|
||||
preferred_username?: string;
|
||||
given_name?: string;
|
||||
family_name?: string;
|
||||
realm_access?: {
|
||||
roles: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface DecodedToken {
|
||||
realm_access?: {
|
||||
roles: string[];
|
||||
};
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
declare module "next-auth" {
|
||||
interface Session {
|
||||
user: {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
email?: string | null;
|
||||
image?: string | null;
|
||||
username: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
role: string[];
|
||||
nextcloudInitialized?: boolean;
|
||||
};
|
||||
accessToken?: string;
|
||||
}
|
||||
|
||||
interface JWT {
|
||||
sub?: string;
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
accessTokenExpires?: number;
|
||||
role?: string[];
|
||||
username?: string;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
error?: string;
|
||||
email?: string | null;
|
||||
name?: string | null;
|
||||
}
|
||||
}
|
||||
|
||||
function getRequiredEnvVar(name: string): string {
|
||||
const value = process.env[name];
|
||||
if (!value) {
|
||||
throw new Error(`Missing required environment variable: ${name}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async function refreshAccessToken(token: JWT) {
|
||||
try {
|
||||
console.log('Attempting to refresh access token');
|
||||
const response = await fetch(`${process.env.KEYCLOAK_ISSUER}/protocol/openid-connect/token`, {
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
client_id: process.env.KEYCLOAK_CLIENT_ID!,
|
||||
client_secret: process.env.KEYCLOAK_CLIENT_SECRET!,
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: token.refreshToken,
|
||||
}),
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
const refreshedTokens = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Token refresh failed with status:', response.status);
|
||||
console.error('Error response:', refreshedTokens);
|
||||
throw refreshedTokens;
|
||||
}
|
||||
|
||||
console.log('Token refresh successful');
|
||||
return {
|
||||
...token,
|
||||
accessToken: refreshedTokens.access_token,
|
||||
refreshToken: refreshedTokens.refresh_token ?? token.refreshToken,
|
||||
accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000,
|
||||
error: undefined, // Clear any previous errors
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error refreshing access token:", error);
|
||||
return {
|
||||
...token,
|
||||
error: "RefreshAccessTokenError",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const authOptions: NextAuthOptions = {
|
||||
providers: [
|
||||
KeycloakProvider({
|
||||
clientId: getRequiredEnvVar("KEYCLOAK_CLIENT_ID"),
|
||||
clientSecret: getRequiredEnvVar("KEYCLOAK_CLIENT_SECRET"),
|
||||
issuer: getRequiredEnvVar("KEYCLOAK_ISSUER"),
|
||||
authorization: {
|
||||
params: {
|
||||
scope: "openid profile email roles"
|
||||
}
|
||||
},
|
||||
profile(profile) {
|
||||
// Simplified profile logging to reduce console noise
|
||||
console.log('Keycloak profile received');
|
||||
|
||||
// Get roles from realm_access
|
||||
const roles = profile.realm_access?.roles || [];
|
||||
|
||||
// Clean up roles by removing ROLE_ prefix and converting to lowercase
|
||||
const cleanRoles = roles.map((role: string) =>
|
||||
role.replace(/^ROLE_/, '').toLowerCase()
|
||||
);
|
||||
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.name ?? profile.preferred_username,
|
||||
email: profile.email,
|
||||
first_name: profile.given_name ?? '',
|
||||
last_name: profile.family_name ?? '',
|
||||
username: profile.preferred_username ?? profile.email?.split('@')[0] ?? '',
|
||||
role: cleanRoles,
|
||||
}
|
||||
},
|
||||
}),
|
||||
],
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
maxAge: 12 * 60 * 60, // Reduce to 12 hours to help with token size
|
||||
},
|
||||
cookies: {
|
||||
sessionToken: {
|
||||
name: `next-auth.session-token`,
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
path: '/',
|
||||
secure: true,
|
||||
domain: process.env.NEXTAUTH_COOKIE_DOMAIN || undefined,
|
||||
maxAge: 12 * 60 * 60, // Match session maxAge
|
||||
},
|
||||
},
|
||||
},
|
||||
jwt: {
|
||||
// Maximum JWT size to prevent chunking
|
||||
maxAge: 12 * 60 * 60, // Reduce to 12 hours
|
||||
},
|
||||
callbacks: {
|
||||
async jwt({ token, account, profile }) {
|
||||
// Initial sign in
|
||||
if (account && profile) {
|
||||
const keycloakProfile = profile as KeycloakProfile;
|
||||
const roles = keycloakProfile.realm_access?.roles || [];
|
||||
|
||||
// Only include admin, owner, user roles (most critical)
|
||||
const criticalRoles = roles
|
||||
.filter(role =>
|
||||
role.includes('admin') ||
|
||||
role.includes('owner') ||
|
||||
role.includes('user')
|
||||
)
|
||||
.map(role => role.replace(/^ROLE_/, '').toLowerCase());
|
||||
|
||||
// Store absolute minimal data in the token
|
||||
token.accessToken = account.access_token;
|
||||
token.refreshToken = account.refresh_token;
|
||||
token.accessTokenExpires = account.expires_at ?? 0;
|
||||
token.sub = keycloakProfile.sub;
|
||||
token.role = criticalRoles.length > 0 ? criticalRoles : ['user']; // Only critical roles
|
||||
token.username = keycloakProfile.preferred_username?.substring(0, 30) ?? '';
|
||||
token.error = undefined;
|
||||
|
||||
// Don't store first/last name in the token to save space
|
||||
// Applications can get these from the userinfo endpoint if needed
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
// Check if token has expired
|
||||
const tokenExpiresAt = token.accessTokenExpires ? token.accessTokenExpires as number : 0;
|
||||
const currentTime = Date.now();
|
||||
const hasExpired = currentTime >= tokenExpiresAt;
|
||||
|
||||
// If the token is still valid, return it
|
||||
if (!hasExpired) {
|
||||
return token;
|
||||
}
|
||||
|
||||
// If refresh token is missing, force sign in
|
||||
if (!token.refreshToken) {
|
||||
console.warn('No refresh token available, session cannot be refreshed');
|
||||
return {
|
||||
...token,
|
||||
error: "RefreshAccessTokenError"
|
||||
};
|
||||
}
|
||||
|
||||
// Try to refresh the token
|
||||
const refreshedToken = await refreshAccessToken(token as JWT);
|
||||
|
||||
// If there was an error refreshing, mark token for re-authentication
|
||||
if (refreshedToken.error) {
|
||||
console.warn('Token refresh failed, user will need to reauthenticate');
|
||||
return {
|
||||
...refreshedToken,
|
||||
error: "RefreshAccessTokenError"
|
||||
};
|
||||
}
|
||||
|
||||
return refreshedToken;
|
||||
},
|
||||
async session({ session, token }) {
|
||||
try {
|
||||
// Handle the error from jwt callback
|
||||
if (token.error === "RefreshAccessTokenError") {
|
||||
console.warn("Session encountered a refresh token error, redirecting to login");
|
||||
// Return minimal session with error flag that will trigger re-auth in client
|
||||
return {
|
||||
...session,
|
||||
error: "RefreshTokenError",
|
||||
user: {
|
||||
id: token.sub ?? '',
|
||||
role: ['user'], // Default role
|
||||
username: '', // Empty username
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
name: null,
|
||||
email: null,
|
||||
image: null,
|
||||
nextcloudInitialized: false,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const userRoles = Array.isArray(token.role) ? token.role : [];
|
||||
|
||||
// Create an extremely minimal user object
|
||||
session.user = {
|
||||
id: token.sub ?? '',
|
||||
email: token.email ?? null,
|
||||
name: token.name ?? null,
|
||||
image: null,
|
||||
username: token.username ?? '',
|
||||
first_name: token.first_name ?? '',
|
||||
last_name: token.last_name ?? '',
|
||||
role: userRoles,
|
||||
// Don't include nextcloudInitialized or other non-essential fields
|
||||
};
|
||||
|
||||
// Only store access token, not the entire token
|
||||
session.accessToken = token.accessToken;
|
||||
|
||||
return session;
|
||||
} catch (error) {
|
||||
console.error("Error in session callback:", error);
|
||||
// Return minimal session with error flag
|
||||
return {
|
||||
...session,
|
||||
error: "SessionError",
|
||||
user: {
|
||||
id: token.sub ?? '',
|
||||
role: ['user'],
|
||||
username: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
name: null,
|
||||
email: null,
|
||||
image: null,
|
||||
nextcloudInitialized: false,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
pages: {
|
||||
signIn: '/signin',
|
||||
error: '/signin',
|
||||
},
|
||||
debug: false, // Disable debug to reduce cookie size from logging
|
||||
};
|
||||
|
||||
const handler = NextAuth(authOptions);
|
||||
export { handler as GET, handler as POST };
|
||||
|
||||
interface JWT {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
accessTokenExpires: number;
|
||||
}
|
||||
|
||||
interface Profile {
|
||||
sub?: string;
|
||||
email?: string;
|
||||
name?: string;
|
||||
roles?: string[];
|
||||
}
|
||||
|
||||
@ -1,199 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import { signIn, useSession } from "next-auth/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { clearAuthCookies } from "@/lib/session";
|
||||
|
||||
export default function SignIn() {
|
||||
const { data: session } = useSession();
|
||||
const searchParams = useSearchParams();
|
||||
const signedOut = searchParams.get('signedOut') === 'true';
|
||||
const forceFreshLogin = searchParams.get('fresh') === 'true';
|
||||
const error = searchParams.get('error');
|
||||
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||
const [isFromLogout, setIsFromLogout] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
// Set error message based on the error param
|
||||
const error = searchParams.get("error");
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
// Clear cookies on errors or manual signout
|
||||
if (error) {
|
||||
let message = "An authentication error occurred";
|
||||
console.log("Clearing auth cookies due to error:", error);
|
||||
clearAuthCookies();
|
||||
|
||||
if (error === "RefreshTokenError") {
|
||||
message = "Your session has expired. Please sign in again.";
|
||||
} else if (error === "SessionError") {
|
||||
message = "There was a problem with your session. Please sign in again.";
|
||||
} else if (error === "invalid_grant") {
|
||||
message = "Your authentication has expired. Please sign in again.";
|
||||
// Set error message
|
||||
if (error === "RefreshTokenError" || error === "invalid_grant") {
|
||||
setMessage("Your session has expired. Please sign in again.");
|
||||
} else {
|
||||
setMessage("There was a problem with authentication. Please sign in.");
|
||||
}
|
||||
|
||||
setErrorMessage(message);
|
||||
console.log(`Authentication error: ${error}`);
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
// Clear all Keycloak cookies when the fresh parameter is present
|
||||
useEffect(() => {
|
||||
if (forceFreshLogin || error) {
|
||||
console.log('Fresh login requested or error detected, clearing all 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, error]);
|
||||
|
||||
// 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;
|
||||
const isFromLoggedOutPage = referrer &&
|
||||
(referrer.includes('/loggedout') || referrer.includes('/signout'));
|
||||
|
||||
if (isFromLoggedOutPage) {
|
||||
console.log('Detected navigation from logout page, preventing auto-login');
|
||||
setIsFromLogout(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Only automatically sign in if:
|
||||
// - Not explicitly signed out
|
||||
// - Not coming from logout
|
||||
// - Not forcing a fresh login
|
||||
// - No error present
|
||||
// - Not already redirecting
|
||||
// - No session exists
|
||||
if (!signedOut && !isFromLogout && !forceFreshLogin && !error && !isRedirecting && !session) {
|
||||
setIsRedirecting(true);
|
||||
console.log('Triggering automatic sign-in');
|
||||
// Add a small delay to avoid immediate redirect which can cause loops
|
||||
const timer = setTimeout(() => {
|
||||
// Trigger Keycloak sign-in
|
||||
signIn("keycloak", { callbackUrl: "/" });
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [signedOut, isRedirecting, isFromLogout, forceFreshLogin, error, session]);
|
||||
|
||||
useEffect(() => {
|
||||
if (session?.user && !session.user.nextcloudInitialized) {
|
||||
// Initialize Nextcloud
|
||||
fetch('/api/nextcloud/init', {
|
||||
method: 'POST'
|
||||
}).then(response => {
|
||||
if (!response.ok) {
|
||||
console.error('Failed to initialize Nextcloud');
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('Error initializing Nextcloud:', error);
|
||||
});
|
||||
}
|
||||
}, [session]);
|
||||
|
||||
const showManualLoginButton = signedOut || isFromLogout || isRedirecting || forceFreshLogin || error;
|
||||
|
||||
// Determine the button text based on the context
|
||||
const buttonText = error
|
||||
? 'Sign In Again'
|
||||
: forceFreshLogin
|
||||
? 'Sign In (Fresh Login)'
|
||||
: signedOut
|
||||
? 'Sign In Again'
|
||||
: 'Sign In';
|
||||
// Simple login function
|
||||
const handleSignIn = () => {
|
||||
signIn("keycloak", { callbackUrl: "/" });
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="min-h-screen flex items-center justify-center"
|
||||
style={{
|
||||
backgroundImage: "url('/signin.jpg')",
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat'
|
||||
}}
|
||||
>
|
||||
<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">
|
||||
{error ? 'Session Expired' : signedOut ? 'You have signed out' : 'Welcome Back'}
|
||||
</h2>
|
||||
|
||||
{errorMessage && (
|
||||
<div className="rounded bg-red-800/40 p-3 mb-6 text-white">
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isRedirecting && !showManualLoginButton && (
|
||||
<p className="text-white/80 mb-4">
|
||||
Redirecting to login...
|
||||
</p>
|
||||
)}
|
||||
|
||||
{showManualLoginButton ? (
|
||||
<button
|
||||
onClick={() => signIn("keycloak", { callbackUrl: "/" })}
|
||||
className="w-full bg-white text-gray-800 py-3 rounded hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
{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 || error) && (
|
||||
<p className="text-white/70 text-sm mt-4">
|
||||
You'll need to enter your credentials again for security reasons
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
||||
<div className="max-w-md w-full p-8 bg-white rounded-lg shadow-lg">
|
||||
<h1 className="text-2xl font-bold text-center mb-6">Sign In</h1>
|
||||
|
||||
{message && (
|
||||
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleSignIn}
|
||||
className="w-full bg-blue-500 hover:bg-blue-600 text-white font-semibold py-2 px-4 rounded"
|
||||
>
|
||||
Sign in with Keycloak
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,61 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { useSession, signOut } from "next-auth/react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useEffect, useCallback } from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export function AuthCheck({ children }: { children: React.ReactNode }) {
|
||||
const { data: session, status } = useSession();
|
||||
const { status } = useSession();
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
||||
// Create a memoized function to handle sign out to prevent excessive rerenders
|
||||
const handleSessionError = useCallback((error: string) => {
|
||||
console.log(`Session error detected: ${error}, signing out`);
|
||||
|
||||
// Force a clean sign out and redirect to login
|
||||
signOut({
|
||||
callbackUrl: `/signin?error=${encodeURIComponent(error)}`,
|
||||
redirect: true
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Handle expired sessions immediately
|
||||
if (session?.error) {
|
||||
handleSessionError(session.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle unauthenticated status (after checking for errors)
|
||||
// Simple redirect to login page if not authenticated
|
||||
if (status === "unauthenticated" && !pathname.includes("/signin")) {
|
||||
console.log("User is not authenticated, redirecting to signin page");
|
||||
router.push("/signin");
|
||||
}
|
||||
}, [status, session, router, pathname, handleSessionError]);
|
||||
}, [status, router, pathname]);
|
||||
|
||||
// Show loading state
|
||||
// Simple loading state
|
||||
if (status === "loading") {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Chargement...</p>
|
||||
</div>
|
||||
<div className="flex justify-center items-center min-h-screen">
|
||||
<div className="animate-spin h-10 w-10 border-4 border-blue-500 rounded-full border-t-transparent"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Do not render with session errors
|
||||
if (session?.error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Do not render if not authenticated and not on signin page
|
||||
// Don't render on unauthenticated
|
||||
if (status === "unauthenticated" && !pathname.includes("/signin")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Authentication is valid, render children
|
||||
// Render children if authenticated
|
||||
return <>{children}</>;
|
||||
}
|
||||
186
lib/session.ts
186
lib/session.ts
@ -90,179 +90,41 @@ export async function invalidateServiceTokens(session: ExtendedSession) {
|
||||
await Promise.all(invalidatePromises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all authentication-related cookies from the browser
|
||||
*/
|
||||
export function clearAuthCookies() {
|
||||
const cookies = document.cookie.split(';');
|
||||
console.log('Clearing all auth cookies');
|
||||
|
||||
// List of known auth-related cookie prefixes and specific cookies
|
||||
const authCookiePrefixes = [
|
||||
'next-auth.',
|
||||
'__Secure-next-auth.',
|
||||
'__Host-next-auth.',
|
||||
'KEYCLOAK_',
|
||||
'KC_',
|
||||
'JSESSIONID',
|
||||
'OAuth_Token_Request_State',
|
||||
'OAUTH2_CLIENT_ID',
|
||||
'OAUTH2_STATE',
|
||||
'XSRF-TOKEN',
|
||||
'AUTH_SESSION_',
|
||||
'identity',
|
||||
'session',
|
||||
'connect.sid'
|
||||
];
|
||||
|
||||
// Specific Keycloak cookies that need to be cleared
|
||||
const specificCookies = [
|
||||
// List of common auth-related cookies
|
||||
const authCookies = [
|
||||
'next-auth.session-token',
|
||||
'next-auth.callback-url',
|
||||
'next-auth.csrf-token',
|
||||
'__Secure-next-auth.session-token',
|
||||
'__Host-next-auth.csrf-token',
|
||||
'next-auth.pkce.code_verifier',
|
||||
'KEYCLOAK_SESSION',
|
||||
'KEYCLOAK_IDENTITY',
|
||||
'KEYCLOAK_REMEMBER_ME',
|
||||
'KC_RESTART',
|
||||
'KEYCLOAK_SESSION_LEGACY',
|
||||
'KEYCLOAK_IDENTITY_LEGACY',
|
||||
'AUTH_SESSION_ID',
|
||||
'AUTH_SESSION_ID_LEGACY',
|
||||
'JSESSIONID'
|
||||
];
|
||||
|
||||
console.log(`Processing ${cookies.length} cookies`);
|
||||
// Get all cookies to check for chunked auth cookies
|
||||
const cookies = document.cookie.split(';');
|
||||
|
||||
// Get all cookie names to detect chunks (like next-auth.session-token.0)
|
||||
const allCookieNames = cookies.map(cookie => cookie.split('=')[0].trim());
|
||||
// Clear main auth cookies
|
||||
authCookies.forEach(cookieName => {
|
||||
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=None; Secure`;
|
||||
});
|
||||
|
||||
// First attempt: Forcefully delete all auth and session cookies by name
|
||||
for (const cookie of cookies) {
|
||||
const [name] = cookie.split('=');
|
||||
const trimmedName = name.trim();
|
||||
|
||||
// Check if this is an auth-related cookie
|
||||
const isAuthCookie = authCookiePrefixes.some(prefix =>
|
||||
trimmedName.startsWith(prefix)
|
||||
) || specificCookies.includes(trimmedName);
|
||||
|
||||
// Also clear cookies with auth-related terms
|
||||
const containsAuthTerm =
|
||||
trimmedName.toLowerCase().includes('auth') ||
|
||||
trimmedName.toLowerCase().includes('token') ||
|
||||
trimmedName.toLowerCase().includes('session') ||
|
||||
trimmedName.toLowerCase().includes('login') ||
|
||||
trimmedName.toLowerCase().includes('id');
|
||||
|
||||
if (isAuthCookie || containsAuthTerm || /\.\d+$/.test(trimmedName)) {
|
||||
console.log(`Clearing cookie: ${trimmedName}`);
|
||||
|
||||
// Try different combinations to ensure the cookie is cleared
|
||||
const paths = ['/', '/auth', '/realms', '/admin', '/api', '/signin', '/login', '/account'];
|
||||
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 with leading dot
|
||||
"" // No domain
|
||||
];
|
||||
|
||||
// Try each combination of path and domain
|
||||
for (const path of paths) {
|
||||
// Try without domain
|
||||
document.cookie = `${trimmedName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path};`;
|
||||
|
||||
// Try with SameSite and Secure attributes
|
||||
document.cookie = `${trimmedName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; SameSite=None; Secure;`;
|
||||
document.cookie = `${trimmedName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; SameSite=Lax;`;
|
||||
document.cookie = `${trimmedName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; SameSite=Strict;`;
|
||||
|
||||
// Try with different domains
|
||||
for (const domain of domains) {
|
||||
if (domain) {
|
||||
document.cookie = `${trimmedName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; domain=${domain};`;
|
||||
document.cookie = `${trimmedName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; domain=${domain}; SameSite=None; Secure;`;
|
||||
document.cookie = `${trimmedName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; domain=${domain}; SameSite=Lax;`;
|
||||
document.cookie = `${trimmedName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; domain=${domain}; SameSite=Strict;`;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check for and clear any chunked cookies
|
||||
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)) {
|
||||
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=None; Secure`;
|
||||
}
|
||||
}
|
||||
|
||||
// Second attempt: Clear by prefix pattern (helps with dynamically named cookies)
|
||||
for (const prefix of authCookiePrefixes) {
|
||||
// Set of paths and domains to try
|
||||
const paths = ['/', '/auth', '/realms', '/admin', '/api', '/signin', '/login', '/account'];
|
||||
const domains = [
|
||||
window.location.hostname,
|
||||
`.${window.location.hostname}`,
|
||||
window.location.hostname.split('.').slice(-2).join('.'),
|
||||
`.${window.location.hostname.split('.').slice(-2).join('.')}`
|
||||
];
|
||||
|
||||
for (const path of paths) {
|
||||
for (const domain of domains) {
|
||||
document.cookie = `${prefix}*=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; domain=${domain}; SameSite=None; Secure;`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear localStorage items that might be related to authentication
|
||||
try {
|
||||
const authLocalStoragePrefixes = ['token', 'auth', 'session', 'keycloak', 'kc', 'user', 'oidc', 'login', 'next-auth'];
|
||||
|
||||
console.log(`Checking localStorage (${localStorage.length} items)`);
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key) {
|
||||
const keyLower = key.toLowerCase();
|
||||
if (authLocalStoragePrefixes.some(prefix => keyLower.includes(prefix))) {
|
||||
console.log(`Clearing localStorage: ${key}`);
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error clearing localStorage:', e);
|
||||
}
|
||||
|
||||
// Also try to clear sessionStorage
|
||||
try {
|
||||
console.log('Clearing sessionStorage');
|
||||
sessionStorage.clear();
|
||||
} catch (e) {
|
||||
console.error('Error clearing sessionStorage:', e);
|
||||
}
|
||||
|
||||
// Check for any IndexedDB databases related to auth
|
||||
try {
|
||||
if (window.indexedDB) {
|
||||
window.indexedDB.databases().then(databases => {
|
||||
databases.forEach(db => {
|
||||
if (db.name &&
|
||||
(db.name.includes('auth') ||
|
||||
db.name.includes('keycloak') ||
|
||||
db.name.includes('token'))) {
|
||||
console.log(`Deleting IndexedDB database: ${db.name}`);
|
||||
window.indexedDB.deleteDatabase(db.name);
|
||||
}
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('Error accessing IndexedDB databases:', err);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error clearing IndexedDB:', e);
|
||||
}
|
||||
|
||||
// Third attempt: check again for any remaining auth cookies
|
||||
const remainingCookies = document.cookie.split(';');
|
||||
for (const cookie of remainingCookies) {
|
||||
const [name] = cookie.split('=');
|
||||
const trimmedName = name.trim();
|
||||
|
||||
if (trimmedName.includes('auth') ||
|
||||
trimmedName.includes('session') ||
|
||||
trimmedName.includes('token') ||
|
||||
trimmedName.includes('.')) {
|
||||
console.log(`Still trying to clear cookie: ${trimmedName}`);
|
||||
document.cookie = `${trimmedName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
||||
document.cookie = `${trimmedName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${window.location.hostname};`;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
136
middleware.ts
136
middleware.ts
@ -17,125 +17,31 @@ export default async function middleware(req: NextRequest) {
|
||||
const url = req.nextUrl;
|
||||
const response = NextResponse.next();
|
||||
|
||||
// Maximum size to prevent cookie chunking - make it even more conservative
|
||||
const MAX_COOKIE_SIZE = 3000; // even more conservative limit in bytes
|
||||
// Simple cookie cleanup on logout or signin error
|
||||
const isLogout = url.pathname === '/loggedout' || url.pathname === '/signout';
|
||||
const isSigninError = url.pathname === '/signin' && url.searchParams.has('error');
|
||||
|
||||
// Function to set all required nextAuth environment variables
|
||||
const setNextAuthEnvVars = () => {
|
||||
// Set strict cookie size limits
|
||||
process.env.NEXTAUTH_COOKIE_SIZE_LIMIT = String(MAX_COOKIE_SIZE);
|
||||
if (isLogout || isSigninError) {
|
||||
// Clear all auth-related cookies when logging out or on error
|
||||
const authCookies = [
|
||||
'next-auth.session-token',
|
||||
'next-auth.csrf-token',
|
||||
'next-auth.callback-url',
|
||||
'__Secure-next-auth.session-token',
|
||||
'__Host-next-auth.csrf-token',
|
||||
'KEYCLOAK_SESSION',
|
||||
'KEYCLOAK_IDENTITY',
|
||||
'KC_RESTART',
|
||||
'JSESSIONID',
|
||||
'AUTH_SESSION_ID',
|
||||
'AUTH_SESSION_ID_LEGACY'
|
||||
];
|
||||
|
||||
// Force cookie compression to reduce size
|
||||
process.env.NEXTAUTH_COOKIES_CHUNKING = 'false'; // Disable chunking and force smaller cookies
|
||||
process.env.NEXTAUTH_COOKIES_CHUNKING_SIZE = String(MAX_COOKIE_SIZE);
|
||||
|
||||
// Set secure cookie settings
|
||||
process.env.NEXTAUTH_COOKIES_SECURE = 'true';
|
||||
process.env.NEXTAUTH_COOKIES_SAMESITE = 'none';
|
||||
|
||||
// Disable unnecessary callbacks that might increase cookie size
|
||||
process.env.NEXTAUTH_DISABLE_CALLBACK = 'true';
|
||||
process.env.NEXTAUTH_DISABLE_JWT_CALLBACK = 'true';
|
||||
process.env.NEXTAUTH_JWT_STORE_RAW_TOKEN = 'false';
|
||||
|
||||
// Strongly enforce JWT max age
|
||||
process.env.NEXTAUTH_JWT_MAX_AGE = String(12 * 60 * 60); // 12 hours in seconds
|
||||
};
|
||||
|
||||
// Set environment variables for all routes
|
||||
setNextAuthEnvVars();
|
||||
|
||||
// Detect refresh token errors in cookies and clean them up
|
||||
const checkForErrorsAndCleanup = () => {
|
||||
const cookies = req.cookies;
|
||||
const cookieNames = Object.keys(cookies.getAll());
|
||||
|
||||
// Check for error param in URL that would indicate token refresh errors
|
||||
if (url.pathname.includes('/signin') && url.searchParams.has('error')) {
|
||||
// Clean up all auth cookies to ensure a fresh start
|
||||
const allAuthCookies = cookieNames.filter(name =>
|
||||
name.includes('auth') ||
|
||||
name.includes('keycloak') ||
|
||||
name.includes('session') ||
|
||||
name.includes('KEYCLOAK') ||
|
||||
name.includes('KC_')
|
||||
);
|
||||
|
||||
allAuthCookies.forEach(name => {
|
||||
response.cookies.delete(name);
|
||||
});
|
||||
|
||||
// Special header to indicate a serious error that requires full cleanup
|
||||
response.headers.set('X-Auth-Error-Recovery', 'true');
|
||||
}
|
||||
};
|
||||
|
||||
// Check for and clean up error cookies
|
||||
checkForErrorsAndCleanup();
|
||||
|
||||
// Special handling for loggedout page to clean up cookies
|
||||
if (url.pathname === '/loggedout') {
|
||||
// Check if we're preserving SSO or doing a full logout
|
||||
const preserveSso = url.searchParams.get('preserveSso') === 'true';
|
||||
|
||||
console.log(`Middleware detected logout (preserveSso: ${preserveSso})`);
|
||||
|
||||
if (preserveSso) {
|
||||
// Only clean up NextAuth cookies but preserve Keycloak SSO cookies
|
||||
const nextAuthCookies = [
|
||||
'next-auth.session-token',
|
||||
'next-auth.csrf-token',
|
||||
'next-auth.callback-url',
|
||||
'__Secure-next-auth.session-token',
|
||||
'__Host-next-auth.csrf-token'
|
||||
];
|
||||
|
||||
nextAuthCookies.forEach(name => {
|
||||
response.cookies.delete(name);
|
||||
});
|
||||
|
||||
// Also delete any chunked cookies
|
||||
const cookieNames = Object.keys(req.cookies.getAll());
|
||||
const chunkedCookies = cookieNames.filter(name => /next-auth.*\.\d+$/.test(name));
|
||||
chunkedCookies.forEach(name => {
|
||||
response.cookies.delete(name);
|
||||
});
|
||||
} else {
|
||||
// Full logout - clear all auth-related cookies
|
||||
const authCookies = [
|
||||
'next-auth.session-token',
|
||||
'next-auth.csrf-token',
|
||||
'next-auth.callback-url',
|
||||
'__Secure-next-auth.session-token',
|
||||
'__Host-next-auth.csrf-token',
|
||||
'KEYCLOAK_SESSION',
|
||||
'KEYCLOAK_IDENTITY',
|
||||
'KC_RESTART',
|
||||
'JSESSIONID'
|
||||
];
|
||||
|
||||
authCookies.forEach(name => {
|
||||
response.cookies.delete(name);
|
||||
});
|
||||
|
||||
// Also delete any chunked cookies
|
||||
const cookieNames = Object.keys(req.cookies.getAll());
|
||||
const chunkedCookies = cookieNames.filter(name =>
|
||||
/next-auth.*\.\d+$/.test(name) ||
|
||||
/KEYCLOAK.*/.test(name) ||
|
||||
/KC_.*/.test(name)
|
||||
);
|
||||
chunkedCookies.forEach(name => {
|
||||
response.cookies.delete(name);
|
||||
});
|
||||
}
|
||||
authCookies.forEach(name => {
|
||||
response.cookies.delete(name);
|
||||
});
|
||||
}
|
||||
|
||||
// For sign-in page, add header if fresh login is requested
|
||||
if (url.pathname === '/api/auth/signin' && url.searchParams.get('fresh') === 'true') {
|
||||
response.headers.set('X-Auth-Fresh-Login', 'true');
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user