Neah/app/api/auth/[...nextauth]/route.ts
2025-05-02 16:41:42 +02:00

257 lines
8.3 KiB
TypeScript

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<string, string[]> = {
// 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 };