190 lines
6.5 KiB
TypeScript
190 lines
6.5 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 - 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;
|
|
}
|
|
}
|