Refactor password reset to use dedicated Keycloak admin client
All checks were successful
Build And Push Image / docker (push) Successful in 2m55s

- Add Keycloak admin credentials configuration to environment variables
- Extract Keycloak admin operations into reusable utility module
- Refactor forgot-password endpoint to use new admin client utility
- Add documentation for Keycloak custom login implementation
- Add password reset fix summary documentation

This improves code organization by separating admin operations from
business logic and provides proper admin credentials for Keycloak
API operations instead of using regular client credentials.
This commit is contained in:
2025-08-07 17:50:09 +02:00
parent c6a57c7922
commit c84442433f
7 changed files with 1746 additions and 102 deletions

View File

@@ -0,0 +1,110 @@
import type { KeycloakAdminConfig } from '~/utils/types';
export class KeycloakAdminClient {
private config: KeycloakAdminConfig;
constructor(config: KeycloakAdminConfig) {
this.config = config;
}
/**
* Get an admin access token using client credentials grant
*/
async getAdminToken(): Promise<string> {
const response = await fetch(`${this.config.issuer}/protocol/openid-connect/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'MonacoUSA-Portal/1.0'
},
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: this.config.clientId,
client_secret: this.config.clientSecret
})
});
if (!response.ok) {
const errorText = await response.text().catch(() => 'Unknown error');
throw new Error(`Failed to get admin token: ${response.status} - ${errorText}`);
}
const tokenData = await response.json();
return tokenData.access_token;
}
/**
* Find a user by email address
*/
async findUserByEmail(email: string, adminToken: string): Promise<any[]> {
const adminBaseUrl = this.config.issuer.replace('/realms/', '/admin/realms/');
const response = await fetch(`${adminBaseUrl}/users?email=${encodeURIComponent(email)}&exact=true`, {
headers: {
'Authorization': `Bearer ${adminToken}`,
'User-Agent': 'MonacoUSA-Portal/1.0'
}
});
if (!response.ok) {
const errorText = await response.text().catch(() => 'Unknown error');
throw new Error(`Failed to search users: ${response.status} - ${errorText}`);
}
return response.json();
}
/**
* Send password reset email to a user
*/
async sendPasswordResetEmail(userId: string, adminToken: string, portalClientId: string, callbackUrl: string): Promise<void> {
const adminBaseUrl = this.config.issuer.replace('/realms/', '/admin/realms/');
const resetUrl = new URL(`${adminBaseUrl}/users/${userId}/execute-actions-email`);
// Add query parameters for better email template rendering
resetUrl.searchParams.set('clientId', portalClientId);
resetUrl.searchParams.set('redirectUri', callbackUrl.replace('/auth/callback', '/login'));
resetUrl.searchParams.set('lifespan', '43200'); // 12 hours
// Create AbortController for timeout handling
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout
try {
const response = await fetch(resetUrl.toString(), {
method: 'PUT',
headers: {
'Authorization': `Bearer ${adminToken}`,
'Content-Type': 'application/json',
'User-Agent': 'MonacoUSA-Portal/1.0'
},
body: JSON.stringify(['UPDATE_PASSWORD']),
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
const errorText = await response.text().catch(() => 'Unknown error');
throw new Error(`Failed to send reset email: ${response.status} - ${errorText}`);
}
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
}
export function createKeycloakAdminClient(): KeycloakAdminClient {
const config = useRuntimeConfig() as any;
if (!config.keycloakAdmin?.clientId || !config.keycloakAdmin?.clientSecret || !config.keycloak?.issuer) {
throw new Error('Missing Keycloak admin configuration');
}
return new KeycloakAdminClient({
issuer: config.keycloak.issuer,
clientId: config.keycloakAdmin.clientId,
clientSecret: config.keycloakAdmin.clientSecret
});
}