import KcAdminClient from '@keycloak/keycloak-admin-client'; import { Credentials } from '@keycloak/keycloak-admin-client/lib/utils/auth'; // Cache the admin client to avoid creating a new one for each request let adminClient: KcAdminClient | null = null; /** * Get a Keycloak admin client instance * @returns KcAdminClient instance */ export async function getKeycloakAdminClient(): Promise { if (adminClient) { try { // Check if the token is still valid by making a simple request await adminClient.users.find({ max: 1 }); return adminClient; } catch (error) { // Token expired, create a new client console.log('Keycloak token expired, creating new admin client'); adminClient = null; } } // Only use environment variables - no hardcoded defaults const keycloakUrl = process.env.KEYCLOAK_BASE_URL || process.env.KEYCLOAK_ISSUER || process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER; const adminClientId = process.env.KEYCLOAK_CLIENT_ID; const adminUsername = process.env.KEYCLOAK_ADMIN_USERNAME; const adminPassword = process.env.KEYCLOAK_ADMIN_PASSWORD; const clientSecret = process.env.KEYCLOAK_CLIENT_SECRET; const realmName = process.env.KEYCLOAK_REALM; // Validate required environment variables if (!keycloakUrl) { console.error('Missing Keycloak URL. Please add one of these to your .env file: KEYCLOAK_BASE_URL, KEYCLOAK_ISSUER, or NEXT_PUBLIC_KEYCLOAK_ISSUER'); throw new Error('Missing Keycloak URL configuration'); } if (!adminClientId || !realmName) { const missing = []; if (!adminClientId) missing.push('KEYCLOAK_CLIENT_ID'); if (!realmName) missing.push('KEYCLOAK_REALM'); console.error(`Missing Keycloak client credentials in .env: ${missing.join(', ')}`); throw new Error('Missing Keycloak client credentials'); } // We'll try various authentication methods depending on what credentials we have if (!clientSecret && (!adminUsername || !adminPassword)) { console.error('Missing credentials for Keycloak authentication. Need either a client secret or username/password.'); throw new Error('Missing Keycloak authentication credentials'); } console.log(`Connecting to Keycloak at ${keycloakUrl}, realm: ${realmName}, client: ${adminClientId}`); try { const kcAdminClient = new KcAdminClient({ baseUrl: keycloakUrl, realmName: 'master', // Use master realm for admin operations }); // Log auth configuration (don't log the actual secret or password) console.log('Auth configuration:', { clientId: adminClientId, hasClientSecret: !!clientSecret, hasUsername: !!adminUsername, hasPassword: !!adminPassword, authUrl: `${keycloakUrl}/realms/master/protocol/openid-connect/token` }); // Authenticate admin client let authParams: Credentials; // If we have a client secret, try to use client credentials grant if (clientSecret) { console.log('Using client credentials grant with client secret'); authParams = { clientId: adminClientId, clientSecret: clientSecret, grantType: 'client_credentials' }; } else { // Fall back to password grant console.log('Using password grant without client secret'); authParams = { clientId: adminClientId, username: adminUsername, password: adminPassword, grantType: 'password' }; } await kcAdminClient.auth(authParams); console.log('Successfully authenticated with Keycloak admin client'); // Now that we're authenticated, we can specify the realm we want to work with // This could be different from the authentication realm (master) console.log(`Setting target realm to: ${realmName}`); kcAdminClient.setConfig({ realmName: realmName, }); // Cache the admin client adminClient = kcAdminClient; return kcAdminClient; } catch (error) { console.error('Error connecting to Keycloak:', error); // Add more detailed error information if (error instanceof Error) { console.error(`Error message: ${error.message}`); console.error(`Error cause: ${error.cause}`); } // For debugging - show what values we're trying to use (without exposing the password) console.error(`Debug info - URL: ${keycloakUrl}, Client ID: ${adminClientId}, Username: ${adminUsername}, Realm: ${realmName}`); throw new Error(`Failed to connect to Keycloak: ${error instanceof Error ? error.message : String(error)}`); } } /** * Get a user by ID * @param userId - Keycloak user ID * @returns User representation or null if not found */ export async function getUserById(userId: string) { try { const kcAdminClient = await getKeycloakAdminClient(); return await kcAdminClient.users.findOne({ id: userId }); } catch (error) { console.error('Error getting user by ID:', error); return null; } } /** * Get a user by email * @param email - User email * @returns User representation or null if not found */ export async function getUserByEmail(email: string) { try { const kcAdminClient = await getKeycloakAdminClient(); const users = await kcAdminClient.users.find({ email: email }); return users?.[0] || null; } catch (error) { console.error('Error getting user by email:', error); return null; } } /** * Get all available roles in the realm * @returns Array of role representations */ export async function getAllRoles() { try { const kcAdminClient = await getKeycloakAdminClient(); return await kcAdminClient.roles.find(); } catch (error) { console.error('Error getting roles:', error); return []; } } /** * Get user roles for a specific user * @param userId - Keycloak user ID * @returns User role mappings */ export async function getUserRoles(userId: string) { try { const kcAdminClient = await getKeycloakAdminClient(); return await kcAdminClient.users.listRoleMappings({ id: userId }); } catch (error) { console.error('Error getting user roles:', error); return null; } }