import NextAuth, { NextAuthOptions } from "next-auth"; import KeycloakProvider from "next-auth/providers/keycloak"; // Define Keycloak profile type interface KeycloakProfile { sub: string; email?: string; name?: string; preferred_username?: string; given_name?: string; family_name?: string; realm_access?: { roles: string[]; }; } // Define custom profile type interface CustomProfile { id: string; name?: string | null; email?: string | null; username: string; first_name: string; last_name: string; role: string[]; } // Declare module augmentation for NextAuth types 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[]; }; accessToken?: string; } interface JWT { sub?: string; accessToken?: string; refreshToken?: string; role?: string[]; username?: string; first_name?: string; last_name?: string; } } // Simple, minimal implementation - NO REFRESH TOKEN LOGIC export const authOptions: NextAuthOptions = { providers: [ KeycloakProvider({ clientId: process.env.KEYCLOAK_CLIENT_ID || "", clientSecret: process.env.KEYCLOAK_CLIENT_SECRET || "", issuer: process.env.KEYCLOAK_ISSUER || "", profile(profile: any) { // Just return a simple profile with required fields return { id: profile.sub, name: profile.name || profile.preferred_username, email: profile.email, image: null, username: profile.preferred_username || profile.email?.split('@')[0] || '', first_name: profile.given_name || '', last_name: profile.family_name || '', role: ['user'], // Store raw profile data for later processing raw_profile: profile }; } }), ], session: { strategy: "jwt", maxAge: 8 * 60 * 60, // 8 hours }, callbacks: { async jwt({ token, account, profile, user }: any) { // Debug input parameters to understand what's available console.log('JWT callback - input parameters:', { hasAccount: !!account, hasProfile: !!profile, hasUser: !!user, hasToken: !!token, tokenSub: token?.sub, tokenAccessToken: token?.accessToken ? '[exists]' : undefined }); // Initial sign in if (account && account.access_token) { console.log('JWT callback - NEW SIGN IN with access token detected'); token.accessToken = account.access_token; token.refreshToken = account.refresh_token; // Process the raw profile data if available if (user && user.raw_profile) { console.log('JWT callback - Raw profile data found, extracting roles'); const rawProfile = user.raw_profile; // Extract roles from all possible sources let roles: string[] = []; // Get roles from realm_access if (rawProfile.realm_access && Array.isArray(rawProfile.realm_access.roles)) { console.log('Found realm_access roles:', rawProfile.realm_access.roles); roles = roles.concat(rawProfile.realm_access.roles); } // Get roles from resource_access if (rawProfile.resource_access) { const clientId = process.env.KEYCLOAK_CLIENT_ID; if (clientId && rawProfile.resource_access[clientId] && Array.isArray(rawProfile.resource_access[clientId].roles)) { console.log('Found client-specific roles:', rawProfile.resource_access[clientId].roles); roles = roles.concat(rawProfile.resource_access[clientId].roles); } // Also check resource_access roles under 'account' if (rawProfile.resource_access.account && Array.isArray(rawProfile.resource_access.account.roles)) { console.log('Found account roles:', rawProfile.resource_access.account.roles); roles = roles.concat(rawProfile.resource_access.account.roles); } } // Clean up roles and convert to lowercase const cleanedRoles = roles .filter(Boolean) .map(role => role.toLowerCase()); // Always ensure user has basic user role const finalRoles = [...new Set([...cleanedRoles, 'user'])]; // Add the application-specific roles for testing finalRoles.push('admin', 'dataintelligence', 'coding', 'expression', 'mediation'); // Store roles in token token.role = [...new Set(finalRoles)]; // Ensure uniqueness console.log('JWT callback - Extracted roles:', token.role); } else if (user && user.role) { console.log('JWT callback - Using roles from user object:', user.role); token.role = Array.isArray(user.role) ? user.role : [user.role]; } else { console.log('JWT callback - No profile data, setting default roles'); token.role = ['user', 'admin', 'dataintelligence', 'coding', 'expression', 'mediation']; } // Store user information if (user) { token.username = user.username || user.name || ''; token.first_name = user.first_name || ''; token.last_name = user.last_name || ''; } } // Token exists but no roles, add default roles for testing else if (token && !token.role) { console.log('JWT callback - Token exists but no roles, adding defaults'); // For testing purposes, add all roles token.role = ['user', 'admin', 'dataintelligence', 'coding', 'expression', 'mediation']; } // Log the token roles console.log('JWT token structure:', JSON.stringify({ sub: token.sub, role: token.role, username: token.username }, null, 2)); console.log('JWT token roles:', token.role); return token; }, async session({ session, token }: any) { console.log('Session callback - input parameters:', { hasSession: !!session, hasToken: !!token, tokenRole: token?.role, tokenSub: token?.sub }); // Pass necessary info to the session session.accessToken = token.accessToken; if (session.user) { session.user.id = token.sub || ""; // Ensure roles are passed to the session if (token.role && Array.isArray(token.role)) { session.user.role = token.role; session.user.username = token.username || ''; session.user.first_name = token.first_name || ''; session.user.last_name = token.last_name || ''; console.log('Session callback - using token roles:', token.role); } else { // Fallback roles - ENSURE ALL REQUIRED ROLES ARE INCLUDED session.user.role = ["user", "admin", "dataintelligence", "coding", "expression", "mediation"]; session.user.username = ''; session.user.first_name = ''; session.user.last_name = ''; console.log('Session callback - no token roles, using fallback'); } // Log the session user roles console.log('Session user roles:', session.user.role); } return session; } }, pages: { signIn: '/signin', error: '/signin', }, cookies: { sessionToken: { name: 'next-auth.session-token', options: { httpOnly: true, sameSite: 'none', path: '/', secure: true, }, }, }, debug: true, // Enable debug logs to help with troubleshooting }; /** * Maps Keycloak roles to application-specific roles */ function mapToApplicationRoles(keycloakRoles: string[]): string[] { const mappings: Record = { // Map Keycloak roles to your application's role names 'admin': ['admin', 'dataintelligence', 'coding', 'expression', 'mediation'], 'owner': ['admin', 'dataintelligence', 'coding', 'expression', 'mediation'], 'cercle-admin': ['admin', 'dataintelligence', 'coding', 'expression', 'mediation'], 'manager': ['dataintelligence', 'coding', 'expression', 'mediation'], 'developer': ['coding', 'dataintelligence'], 'data-scientist': ['dataintelligence'], 'designer': ['expression'], 'writer': ['expression'], 'mediator': ['mediation'], // Default access roles from Keycloak 'default-roles-cercle': ['user'], 'uma_authorization': ['user'], 'offline_access': ['user'], // Add more mappings as needed }; // Convert all keycloak roles to lowercase for case-insensitive matching const lowerKeycloakRoles = keycloakRoles.map(role => role.toLowerCase()); // Map roles based on the defined mappings let applicationRoles: string[] = []; // Check all Keycloak roles for matches in our mapping for (const role of lowerKeycloakRoles) { if (mappings[role]) { applicationRoles = applicationRoles.concat(mappings[role]); } // Handle any role that contains certain keywords if (role.includes('admin')) { applicationRoles.push('admin', 'dataintelligence', 'coding', 'expression', 'mediation'); } else if (role.includes('developer') || role.includes('dev')) { applicationRoles.push('coding', 'dataintelligence'); } else if (role.includes('design')) { applicationRoles.push('expression'); } else if (role.includes('data')) { applicationRoles.push('dataintelligence'); } else if (role.includes('mediat')) { applicationRoles.push('mediation'); } } // Ensure user always has basic access applicationRoles.push('user'); // Return unique application roles return [...new Set(applicationRoles)]; } const handler = NextAuth(authOptions); export { handler as GET, handler as POST };