NeahNew/lib/keycloak.ts
2025-05-03 17:25:46 +02:00

201 lines
6.9 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;
/**
* Get a Keycloak admin client instance
* @returns KcAdminClient instance
*/
export async function getKeycloakAdminClient(): Promise<KcAdminClient> {
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 {
// Try a direct authentication approach first to debug the issue
console.log('Trying direct authentication to debug...');
// Create form data for authentication
const formData = new URLSearchParams();
formData.append('client_id', adminClientId);
if (clientSecret) {
formData.append('client_secret', clientSecret);
formData.append('grant_type', 'client_credentials');
} else {
formData.append('username', adminUsername);
formData.append('password', adminPassword);
formData.append('grant_type', 'password');
}
// Determine the token URL
const tokenUrl = `${keycloakUrl}/realms/master/protocol/openid-connect/token`;
console.log(`Authenticating to: ${tokenUrl}`);
// Make the request directly
const response = await fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formData,
});
// Log detailed response information
console.log(`Authentication response status: ${response.status}`);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
console.log('Authentication error:', errorData);
throw new Error(`Direct authentication failed: ${errorData.error || response.statusText}`);
}
const tokenData = await response.json();
console.log('Authentication successful, received token data');
// Now proceed with the Keycloak admin client
const kcAdminClient = new KcAdminClient({
baseUrl: keycloakUrl,
realmName: 'master',
});
// Set the token manually since we already authenticated
kcAdminClient.setAccessToken(tokenData.access_token);
// 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}`);
// Try to extract more information from the error
const anyError = error as any;
if (anyError.response) {
console.error('Response status:', anyError.response.status);
console.error('Response headers:', anyError.response.headers);
// Try to extract response data if available
if (anyError.responseData) {
console.error('Response data:', JSON.stringify(anyError.responseData, null, 2));
}
}
}
// 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;
}
}