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) { // Initial sign in if (account && account.access_token) { token.accessToken = account.access_token; token.refreshToken = account.refresh_token; // Process the raw profile data if available if (user && user.raw_profile) { 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)) { 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)) { 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)) { 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'])]; // Map Keycloak roles to application roles token.role = mapToApplicationRoles(finalRoles); } else if (user && user.role) { token.role = Array.isArray(user.role) ? user.role : [user.role]; } else { // Default roles if no profile data available token.role = ['user']; } // 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 user role else if (token && !token.role) { token.role = ['user']; } 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 || ''; } else { // Fallback roles session.user.role = ["user"]; session.user.username = ''; session.user.first_name = ''; session.user.last_name = ''; } } return session; } }, pages: { signIn: '/signin', error: '/signin', }, cookies: { sessionToken: { name: 'next-auth.session-token', options: { httpOnly: true, sameSite: 'none', path: '/', secure: true, }, }, }, debug: process.env.NODE_ENV === 'development', }; /** * 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 }; // Map each role and flatten the result let appRoles: string[] = ['user']; // Always include 'user' role for (const role of keycloakRoles) { const mappedRoles = mappings[role.toLowerCase()]; if (mappedRoles) { appRoles = [...appRoles, ...mappedRoles]; } } // Remove duplicates and return return [...new Set(appRoles)]; } const handler = NextAuth(authOptions); export { handler as GET, handler as POST };