From 4583f183c4b329ebf11ad1626f433caba64b8652 Mon Sep 17 00:00:00 2001 From: alma Date: Sat, 3 May 2025 17:27:45 +0200 Subject: [PATCH] equipes keycloak flow --- app/api/auth/debug-keycloak/route.ts | 104 +++++++++++++++++++++ lib/keycloak.ts | 133 +++++++++++---------------- 2 files changed, 158 insertions(+), 79 deletions(-) create mode 100644 app/api/auth/debug-keycloak/route.ts diff --git a/app/api/auth/debug-keycloak/route.ts b/app/api/auth/debug-keycloak/route.ts new file mode 100644 index 00000000..31030b56 --- /dev/null +++ b/app/api/auth/debug-keycloak/route.ts @@ -0,0 +1,104 @@ +import { NextResponse } from 'next/server'; + +export async function GET() { + try { + // Get Keycloak URL + 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 realm = process.env.KEYCLOAK_REALM; + + // Log all relevant environment variables (without exposing secrets) + const envVars = { + hasKeycloakUrl: !!keycloakUrl, + keycloakUrl, + hasClientId: !!clientId, + clientId, + hasClientSecret: !!clientSecret, + hasAdminUsername: !!adminUsername, + hasAdminPassword: !!adminPassword, + realm, + }; + + if (!keycloakUrl) { + return NextResponse.json({ + error: 'Missing Keycloak URL', + message: 'KEYCLOAK_BASE_URL, KEYCLOAK_ISSUER, or NEXT_PUBLIC_KEYCLOAK_ISSUER is required' + }, { status: 400 }); + } + + if (!clientId) { + return NextResponse.json({ + error: 'Missing Client ID', + message: 'KEYCLOAK_CLIENT_ID is required' + }, { status: 400 }); + } + + console.log('Environment variables check:', envVars); + + // Try direct authentication + const url = `${keycloakUrl}/realms/master/protocol/openid-connect/token`; + const formData = new URLSearchParams(); + + // Try client credentials if available + if (clientSecret) { + formData.append('client_id', clientId); + formData.append('client_secret', clientSecret); + formData.append('grant_type', 'client_credentials'); + } + // Fall back to password grant + else if (adminUsername && adminPassword) { + formData.append('client_id', clientId); + formData.append('username', adminUsername); + formData.append('password', adminPassword); + formData.append('grant_type', 'password'); + } else { + return NextResponse.json({ + error: 'Missing authentication credentials', + message: 'Either client_secret or admin username/password is required' + }, { status: 400 }); + } + + console.log(`Testing authentication to: ${url}`); + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: formData, + }); + + console.log(`Response status: ${response.status}`); + + const data = await response.json(); + + if (!response.ok) { + console.error('Authentication error:', data); + return NextResponse.json({ + error: 'Authentication failed', + details: data, + requestedUrl: url, + clientId: clientId, + grantType: formData.get('grant_type') + }, { status: response.status }); + } + + // Success! Return sanitized token info (not the actual tokens) + return NextResponse.json({ + success: true, + tokenType: data.token_type, + expiresIn: data.expires_in, + hasAccessToken: !!data.access_token, + hasRefreshToken: !!data.refresh_token, + }); + } catch (error) { + console.error('Error testing Keycloak connection:', error); + return NextResponse.json({ + error: 'Failed to test Keycloak connection', + message: error instanceof Error ? error.message : String(error) + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/lib/keycloak.ts b/lib/keycloak.ts index 0281cb53..ee5e65f4 100644 --- a/lib/keycloak.ts +++ b/lib/keycloak.ts @@ -3,138 +3,113 @@ 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 { - if (adminClient) { + // Check if we have a recently authenticated client + const now = Date.now(); + if (adminClient && (now - lastAuthTime < AUTH_CACHE_DURATION)) { try { - // Check if the token is still valid by making a simple request + // Validate token is still working with 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'); + console.log('Cached Keycloak token invalid or expired, creating new admin client'); adminClient = null; } } - // Only use environment variables - no hardcoded defaults + // Get configuration from environment variables 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 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 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'); + throw new Error('Missing Keycloak URL. Set KEYCLOAK_BASE_URL, KEYCLOAK_ISSUER, or NEXT_PUBLIC_KEYCLOAK_ISSUER'); } - if (!adminClientId || !realmName) { + if (!clientId || !realmName) { const missing = []; - if (!adminClientId) missing.push('KEYCLOAK_CLIENT_ID'); + if (!clientId) 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'); + throw new Error(`Missing Keycloak configuration: ${missing.join(', ')}`); } - // We'll try various authentication methods depending on what credentials we have + // Need either client credentials or username/password 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'); + 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: ${adminClientId}`); + console.log(`Connecting to Keycloak at ${keycloakUrl}, realm: ${realmName}, client: ${clientId}`); 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 + // Create and configure the admin client const kcAdminClient = new KcAdminClient({ baseUrl: keycloakUrl, - realmName: 'master', + realmName: 'master', // Start with master realm for auth }); - - // 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}`); + // 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: realmName, + realmName, }); - // Cache the admin client + // 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 connecting to Keycloak:', error); + console.error('Error authenticating with Keycloak:', error); - // Add more detailed error information + // Add more error details if (error instanceof Error) { console.error(`Error message: ${error.message}`); - console.error(`Error cause: ${error.cause}`); - // Try to extract more information from the error + // 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 headers:', anyError.response.headers); - - // Try to extract response data if available - if (anyError.responseData) { - console.error('Response data:', JSON.stringify(anyError.responseData, null, 2)); - } + console.error('Response data:', anyError.response.data); } } - // 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}`); + // 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)}`); }