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) { // Debug the raw profile from Keycloak to understand its structure console.log('Raw Keycloak profile:', JSON.stringify(profile, null, 2)); // Extract roles from realm_access.roles and resource_access let roles: string[] = []; // Get roles from realm_access if (profile.realm_access && Array.isArray(profile.realm_access.roles)) { roles = roles.concat(profile.realm_access.roles); } // Get roles from resource_access for the client if (profile.resource_access) { const clientId = process.env.KEYCLOAK_CLIENT_ID; if (clientId && profile.resource_access[clientId] && Array.isArray(profile.resource_access[clientId].roles)) { roles = roles.concat(profile.resource_access[clientId].roles); } // Also check resource_access roles under 'account' if (profile.resource_access.account && Array.isArray(profile.resource_access.account.roles)) { roles = roles.concat(profile.resource_access.account.roles); } } // Extract groups if available if (profile.groups && Array.isArray(profile.groups)) { // Remove any path prefixes (like "/") and add as roles const groupRoles = profile.groups.map((group: string) => group.replace(/^\//, '').toLowerCase() ); roles = roles.concat(groupRoles); } // Clean up roles and convert to lowercase const cleanedRoles = roles .filter(Boolean) // Remove empty roles .map((role: string) => role.replace(/^ROLE_/, '').toLowerCase() ); // Add some common application-specific role mappings const applicationRoles = mapToApplicationRoles(cleanedRoles); const allRoles = [...new Set([...cleanedRoles, ...applicationRoles, 'user'])]; console.log('Extracted roles:', allRoles); return { id: profile.sub, name: profile.name || profile.preferred_username, email: profile.email, image: null, role: allRoles, first_name: profile.given_name || '', last_name: profile.family_name || '', username: profile.preferred_username || profile.email?.split('@')[0] || '', }; } }), ], session: { strategy: "jwt", maxAge: 8 * 60 * 60, // 8 hours }, callbacks: { async jwt({ token, account, profile }: any) { if (account && profile) { // Store access token token.accessToken = account.access_token; token.refreshToken = account.refresh_token; // Use the roles from the profile function if (profile.role && Array.isArray(profile.role)) { token.role = profile.role; console.log('JWT callback - roles from profile:', profile.role); } else { // Fallback for missing roles token.role = ['user']; console.log('JWT callback - no roles in profile, using fallback'); } // Store user information token.username = profile.username || ''; token.first_name = profile.first_name || ''; token.last_name = profile.last_name || ''; } // Log the token roles console.log('JWT token roles:', token.role); return token; }, async session({ session, token }: any) { // 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 session.user.role = ["user"]; 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 };