NeahNew/lib/keycloak.ts
2025-05-03 17:27:45 +02:00

176 lines
5.8 KiB
TypeScript

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<KcAdminClient> {
// 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
const kcAdminClient = new KcAdminClient({
baseUrl: keycloakUrl,
realmName: 'master', // Start with master realm for auth
});
// Try client credentials first if available (preferred method)
if (clientSecret) {
console.log('Authenticating with client credentials');
await kcAdminClient.auth({
clientId,
clientSecret,
grantType: 'client_credentials',
});
}
// Fall back to password grant
else if (adminUsername && adminPassword) {
console.log('Authenticating with password grant');
await kcAdminClient.auth({
clientId,
username: adminUsername,
password: adminPassword,
grantType: 'password',
});
}
// Now that we're authenticated, set the target realm
kcAdminClient.setConfig({
realmName,
});
// 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;
}
}