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; let lastAuthTime = 0; const AUTH_CACHE_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds /** * Get a Keycloak admin client instance * @returns KcAdminClient instance */ export async function getKeycloakAdminClient(): Promise { // Check if we have a recently authenticated client const now = Date.now(); if (adminClient && (now - lastAuthTime < AUTH_CACHE_DURATION)) { try { // Validate token is still working with a simple request await adminClient.users.find({ max: 1 }); return adminClient; } catch (error) { console.log('Cached Keycloak token invalid or expired, creating new admin client'); adminClient = null; } } // Get configuration from environment variables const keycloakUrl = process.env.KEYCLOAK_BASE_URL || process.env.KEYCLOAK_ISSUER || process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER; const clientId = process.env.KEYCLOAK_CLIENT_ID; const clientSecret = process.env.KEYCLOAK_CLIENT_SECRET; const adminUsername = process.env.KEYCLOAK_ADMIN_USERNAME; const adminPassword = process.env.KEYCLOAK_ADMIN_PASSWORD; const realmName = process.env.KEYCLOAK_REALM; // Validate required environment variables if (!keycloakUrl) { throw new Error('Missing Keycloak URL. Set KEYCLOAK_BASE_URL, KEYCLOAK_ISSUER, or NEXT_PUBLIC_KEYCLOAK_ISSUER'); } if (!clientId || !realmName) { const missing = []; if (!clientId) missing.push('KEYCLOAK_CLIENT_ID'); if (!realmName) missing.push('KEYCLOAK_REALM'); throw new Error(`Missing Keycloak configuration: ${missing.join(', ')}`); } // Need either client credentials or username/password if (!clientSecret && (!adminUsername || !adminPassword)) { throw new Error('Missing Keycloak authentication credentials. Set either KEYCLOAK_CLIENT_SECRET or both KEYCLOAK_ADMIN_USERNAME and KEYCLOAK_ADMIN_PASSWORD'); } console.log(`Connecting to Keycloak at ${keycloakUrl}, realm: ${realmName}, client: ${clientId}`); try { // Create and configure the admin client - we have already validated realmName is defined above const kcAdminClient = new KcAdminClient({ baseUrl: keycloakUrl, realmName: realmName!, // Non-null assertion since we validated above }); // Try to authenticate directly with a token from the token endpoint console.log('Authenticating with direct token fetch'); const tokenUrl = `${keycloakUrl}/realms/${realmName}/protocol/openid-connect/token`; const formData = new URLSearchParams(); // clientId is validated above, so it's safe to use non-null assertion formData.append('client_id', clientId!); if (clientSecret) { formData.append('client_secret', clientSecret); formData.append('grant_type', 'client_credentials'); } else if (adminUsername && adminPassword) { formData.append('username', adminUsername); formData.append('password', adminPassword); formData.append('grant_type', 'password'); } else { // This should never happen due to validation above throw new Error('No valid authentication method available'); } const response = await fetch(tokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: formData, }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(`Authentication failed: ${errorData.error || response.statusText}`); } const tokenData = await response.json(); // Set the token manually kcAdminClient.setAccessToken(tokenData.access_token); // Test that authentication worked with a simple request await kcAdminClient.users.find({ max: 1 }); // Cache the successful client and auth time adminClient = kcAdminClient; lastAuthTime = Date.now(); console.log('Successfully authenticated with Keycloak'); return kcAdminClient; } catch (error) { console.error('Error authenticating with Keycloak:', error); // Add more error details if (error instanceof Error) { console.error(`Error message: ${error.message}`); // Try to extract more information if available const anyError = error as any; if (anyError.response) { console.error('Response status:', anyError.response.status); console.error('Response data:', anyError.response.data); } } // For debugging - show what values we're trying to use (without exposing secrets) console.error(`Debug info - URL: ${keycloakUrl}, Client ID: ${clientId}, Has Secret: ${!!clientSecret}, Has 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; } }