diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts
index bf076e42..fd7ca927 100644
--- a/app/api/auth/[...nextauth]/route.ts
+++ b/app/api/auth/[...nextauth]/route.ts
@@ -112,24 +112,17 @@ export const authOptions: NextAuthOptions = {
}
},
profile(profile) {
- console.log('Keycloak profile callback:', {
- rawProfile: profile,
- rawRoles: profile.roles,
- realmAccess: profile.realm_access,
- groups: profile.groups
- });
+ // Simplified profile logging to reduce console noise
+ console.log('Keycloak profile received');
// Get roles from realm_access
const roles = profile.realm_access?.roles || [];
- console.log('Profile callback raw roles:', roles);
-
+
// Clean up roles by removing ROLE_ prefix and converting to lowercase
const cleanRoles = roles.map((role: string) =>
role.replace(/^ROLE_/, '').toLowerCase()
);
- console.log('Profile callback cleaned roles:', cleanRoles);
-
return {
id: profile.sub,
name: profile.name ?? profile.preferred_username,
@@ -144,7 +137,7 @@ export const authOptions: NextAuthOptions = {
],
session: {
strategy: "jwt",
- maxAge: 30 * 24 * 60 * 60, // 30 days
+ maxAge: 12 * 60 * 60, // Reduce to 12 hours to help with token size
},
cookies: {
sessionToken: {
@@ -155,12 +148,13 @@ export const authOptions: NextAuthOptions = {
path: '/',
secure: true,
domain: process.env.NEXTAUTH_COOKIE_DOMAIN || undefined,
+ maxAge: 12 * 60 * 60, // Match session maxAge
},
},
},
jwt: {
- // Add explicit max size to prevent chunking
- maxAge: 30 * 24 * 60 * 60, // 30 days
+ // Maximum JWT size to prevent chunking
+ maxAge: 12 * 60 * 60, // Reduce to 12 hours
},
callbacks: {
async jwt({ token, account, profile }) {
@@ -168,26 +162,27 @@ export const authOptions: NextAuthOptions = {
if (account && profile) {
const keycloakProfile = profile as KeycloakProfile;
const roles = keycloakProfile.realm_access?.roles || [];
- const cleanRoles = roles.map((role: string) =>
- role.replace(/^ROLE_/, '').toLowerCase()
- );
+
+ // 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 minimal data in the token
- token.accessToken = account.access_token ?? '';
- token.refreshToken = account.refresh_token ?? '';
+ // 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 = cleanRoles;
- token.username = keycloakProfile.preferred_username ?? '';
- token.error = undefined; // Clear any errors
+ token.role = criticalRoles.length > 0 ? criticalRoles : ['user']; // Only critical roles
+ token.username = keycloakProfile.preferred_username?.substring(0, 30) ?? '';
+ token.error = undefined;
- // Only store these if they're short
- if (keycloakProfile.given_name && keycloakProfile.given_name.length < 30) {
- token.first_name = keycloakProfile.given_name;
- }
- if (keycloakProfile.family_name && keycloakProfile.family_name.length < 30) {
- token.last_name = keycloakProfile.family_name;
- }
+ // Don't store first/last name in the token to save space
+ // Applications can get these from the userinfo endpoint if needed
return token;
}
@@ -235,15 +230,22 @@ export const authOptions: NextAuthOptions = {
...session,
error: "RefreshTokenError",
user: {
- ...session.user,
- id: token.sub ?? ''
+ 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 a minimal user object
+ // Create an extremely minimal user object
session.user = {
id: token.sub ?? '',
email: token.email ?? null,
@@ -253,7 +255,7 @@ export const authOptions: NextAuthOptions = {
first_name: token.first_name ?? '',
last_name: token.last_name ?? '',
role: userRoles,
- nextcloudInitialized: false,
+ // Don't include nextcloudInitialized or other non-essential fields
};
// Only store access token, not the entire token
@@ -267,8 +269,15 @@ export const authOptions: NextAuthOptions = {
...session,
error: "SessionError",
user: {
- ...session.user,
- id: token.sub ?? ''
+ id: token.sub ?? '',
+ role: ['user'],
+ username: '',
+ first_name: '',
+ last_name: '',
+ name: null,
+ email: null,
+ image: null,
+ nextcloudInitialized: false,
}
};
}
@@ -278,7 +287,7 @@ export const authOptions: NextAuthOptions = {
signIn: '/signin',
error: '/signin',
},
- debug: process.env.NODE_ENV === 'development',
+ debug: false, // Disable debug to reduce cookie size from logging
};
const handler = NextAuth(authOptions);
diff --git a/app/layout.tsx b/app/layout.tsx
index c726c206..dee70fe9 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -18,18 +18,32 @@ export default async function RootLayout({
}: {
children: React.ReactNode;
}) {
- const session = await getServerSession(authOptions);
+ // Try to get the session, but handle potential errors gracefully
+ let session = null;
+ let sessionError = false;
+
+ try {
+ session = await getServerSession(authOptions);
+ } catch (error) {
+ console.error("Error getting server session:", error);
+ sessionError = true;
+ }
+
const headersList = await headers();
const pathname = headersList.get("x-pathname") || "";
const isSignInPage = pathname === "/signin";
+
+ // If we're on the signin page and there was a session error,
+ // don't pass the session to avoid refresh attempts
+ const safeSession = isSignInPage && sessionError ? null : session;
return (
-
+
{children}
diff --git a/components/auth/auth-check.tsx b/components/auth/auth-check.tsx
index eaaa04bb..d33844b7 100644
--- a/components/auth/auth-check.tsx
+++ b/components/auth/auth-check.tsx
@@ -2,30 +2,37 @@
import { useSession, signOut } from "next-auth/react";
import { usePathname, useRouter } from "next/navigation";
-import { useEffect } from "react";
+import { useEffect, useCallback } from "react";
export function AuthCheck({ children }: { children: React.ReactNode }) {
const { data: session, 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 authentication status changes
+ // Handle expired sessions immediately
+ if (session?.error) {
+ handleSessionError(session.error);
+ return;
+ }
+
+ // Handle unauthenticated status (after checking for errors)
if (status === "unauthenticated" && !pathname.includes("/signin")) {
console.log("User is not authenticated, redirecting to signin page");
router.push("/signin");
}
-
- // Handle session errors (like refresh token failures)
- if (session?.error) {
- console.log(`Session error detected: ${session.error}, signing out`);
- // Force a clean sign out
- signOut({
- callbackUrl: `/signin?error=${encodeURIComponent(session.error)}`,
- redirect: true
- });
- }
- }, [status, session, router, pathname]);
+ }, [status, session, router, pathname, handleSessionError]);
// Show loading state
if (status === "loading") {
@@ -39,13 +46,13 @@ export function AuthCheck({ children }: { children: React.ReactNode }) {
);
}
- // If not authenticated and not on signin page, don't render children
- if (status === "unauthenticated" && !pathname.includes("/signin")) {
+ // Do not render with session errors
+ if (session?.error) {
return null;
}
- // Session has error, don't render children
- if (session?.error) {
+ // Do not render if not authenticated and not on signin page
+ if (status === "unauthenticated" && !pathname.includes("/signin")) {
return null;
}
diff --git a/lib/session.ts b/lib/session.ts
index e31785aa..7ff2cbbe 100644
--- a/lib/session.ts
+++ b/lib/session.ts
@@ -119,7 +119,10 @@ export function clearAuthCookies() {
'KEYCLOAK_REMEMBER_ME',
'KC_RESTART',
'KEYCLOAK_SESSION_LEGACY',
- 'KEYCLOAK_IDENTITY_LEGACY'
+ 'KEYCLOAK_IDENTITY_LEGACY',
+ 'AUTH_SESSION_ID',
+ 'AUTH_SESSION_ID_LEGACY',
+ 'JSESSIONID'
];
console.log(`Processing ${cookies.length} cookies`);
@@ -127,18 +130,7 @@ export function clearAuthCookies() {
// 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);
-
+ // First attempt: Forcefully delete all auth and session cookies by name
for (const cookie of cookies) {
const [name] = cookie.split('=');
const trimmedName = name.trim();
@@ -156,16 +148,17 @@ export function clearAuthCookies() {
trimmedName.toLowerCase().includes('login') ||
trimmedName.toLowerCase().includes('id');
- if (isAuthCookie || containsAuthTerm) {
+ 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'];
+ 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
+ `.${window.location.hostname.split('.').slice(-2).join('.')}`, // Root domain with leading dot
+ "" // No domain
];
// Try each combination of path and domain
@@ -175,19 +168,43 @@ export function clearAuthCookies() {
// 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) {
- 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;`;
+ 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;`;
+ }
}
}
}
}
+ // 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'];
+ 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++) {
@@ -232,4 +249,20 @@ export function clearAuthCookies() {
} 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};`;
+ }
+ }
}
\ No newline at end of file
diff --git a/middleware.ts b/middleware.ts
index ff5d8cd5..9b444660 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -17,24 +17,62 @@ export default async function middleware(req: NextRequest) {
const url = req.nextUrl;
const response = NextResponse.next();
- // Maximum size to prevent cookie chunking
- const MAX_COOKIE_SIZE = 3500; // conservative limit in bytes
+ // Maximum size to prevent cookie chunking - make it even more conservative
+ const MAX_COOKIE_SIZE = 3000; // even more conservative limit in bytes
// Function to set all required nextAuth environment variables
const setNextAuthEnvVars = () => {
- // Disable callbacks that could increase cookie size
- process.env.NEXTAUTH_DISABLE_CALLBACK = 'true';
- process.env.NEXTAUTH_DISABLE_JWT_CALLBACK = 'true';
+ // Set strict cookie size limits
process.env.NEXTAUTH_COOKIE_SIZE_LIMIT = String(MAX_COOKIE_SIZE);
- process.env.NEXTAUTH_COOKIES_CHUNKING = 'true';
+
+ // 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
diff --git a/types/next-auth.d.ts b/types/next-auth.d.ts
index b660b33d..df632783 100644
--- a/types/next-auth.d.ts
+++ b/types/next-auth.d.ts
@@ -1,5 +1,13 @@
import NextAuth, { DefaultSession, DefaultUser } from "next-auth";
+// Define possible error types for better type checking
+type AuthErrorType =
+ | "RefreshTokenError"
+ | "SessionError"
+ | "TokenError"
+ | "ConfigError"
+ | string;
+
declare module "next-auth" {
interface Session {
user: {
@@ -14,7 +22,8 @@ declare module "next-auth" {
refreshToken?: string;
rocketChatToken?: string | null;
rocketChatUserId?: string | null;
- error?: string;
+ error?: AuthErrorType;
+ errorDescription?: string;
}
interface JWT {
@@ -27,7 +36,8 @@ declare module "next-auth" {
role?: string[];
rocketChatToken?: string | null;
rocketChatUserId?: string | null;
- error?: string;
+ error?: AuthErrorType;
+ errorDescription?: string;
}
interface User extends DefaultUser {
@@ -62,6 +72,7 @@ declare module "next-auth/jwt" {
role?: string[];
rocketChatToken?: string;
rocketChatUserId?: string;
- error?: string;
+ error?: AuthErrorType;
+ errorDescription?: string;
}
}