Add password setup flow with server-side validation
All checks were successful
Build And Push Image / docker (push) Successful in 3m2s

- Replace external password setup link with internal navigation
- Add comprehensive password validation utility with strength requirements
- Create dedicated password setup page and API endpoint
- Streamline user flow from email verification to password creation
This commit is contained in:
2025-08-09 19:11:54 +02:00
parent 30b7e23319
commit d14008efd4
4 changed files with 635 additions and 15 deletions

View File

@@ -0,0 +1,160 @@
/**
* Password Setup API Endpoint
* Handles setting passwords for newly registered users
*/
import { createKeycloakAdminClient } from '~/server/utils/keycloak-admin';
import { validatePassword } from '~/server/utils/security';
interface SetupPasswordRequest {
email: string;
password: string;
token?: string;
}
export default defineEventHandler(async (event) => {
console.log('[api/auth/setup-password] =========================');
console.log('[api/auth/setup-password] POST /api/auth/setup-password - Password setup');
try {
const body = await readBody(event) as SetupPasswordRequest;
console.log('[api/auth/setup-password] Setup password attempt for:', body.email);
// 1. Validate request data
if (!body.email?.trim()) {
throw createError({
statusCode: 400,
statusMessage: 'Email address is required'
});
}
if (!body.password?.trim()) {
throw createError({
statusCode: 400,
statusMessage: 'Password is required'
});
}
// 2. Validate password strength
const passwordValidation = validatePassword(body.password);
if (!passwordValidation.isValid) {
throw createError({
statusCode: 422,
statusMessage: `Password validation failed: ${passwordValidation.errors.join(', ')}`
});
}
// 3. Find user in Keycloak
const keycloakAdmin = createKeycloakAdminClient();
const existingUsers = await keycloakAdmin.findUserByEmail(body.email);
if (existingUsers.length === 0) {
throw createError({
statusCode: 404,
statusMessage: 'User not found. Please register first or contact support.'
});
}
const user = existingUsers[0];
// 4. Check if user already has a password set by checking if they have any required actions
console.log('[api/auth/setup-password] User found:', user.id, 'Required actions:', user.requiredActions);
if (user.requiredActions && !user.requiredActions.includes('UPDATE_PASSWORD')) {
console.log('[api/auth/setup-password] User already has password set, allowing password update');
// Allow password updates - this could be a password reset scenario
}
// 5. Set the user's password in Keycloak using direct REST API
console.log('[api/auth/setup-password] Setting password for user:', user.id);
const adminToken = await keycloakAdmin.getAdminToken();
const config = useRuntimeConfig();
const adminBaseUrl = config.keycloak.issuer.replace('/realms/', '/admin/realms/');
// Set password using Keycloak Admin REST API
const setPasswordResponse = await fetch(`${adminBaseUrl}/users/${user.id}/reset-password`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${adminToken}`,
'Content-Type': 'application/json',
'User-Agent': 'MonacoUSA-Portal/1.0'
},
body: JSON.stringify({
type: 'password',
value: body.password,
temporary: false
})
});
if (!setPasswordResponse.ok) {
const errorText = await setPasswordResponse.text().catch(() => 'Unknown error');
throw createError({
statusCode: setPasswordResponse.status,
statusMessage: `Failed to set password: ${errorText}`
});
}
// 6. Update user to ensure they're enabled, email is verified, and remove required actions
const updateUserResponse = await fetch(`${adminBaseUrl}/users/${user.id}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${adminToken}`,
'Content-Type': 'application/json',
'User-Agent': 'MonacoUSA-Portal/1.0'
},
body: JSON.stringify({
...user,
enabled: true,
emailVerified: true,
requiredActions: [], // Remove all required actions including UPDATE_PASSWORD
attributes: {
...user.attributes,
needsPasswordSetup: ['false'],
passwordSetAt: [new Date().toISOString()]
}
})
});
if (!updateUserResponse.ok) {
const errorText = await updateUserResponse.text().catch(() => 'Unknown error');
console.warn('[api/auth/setup-password] Failed to update user profile:', errorText);
// Don't fail the entire operation if this update fails
}
console.log(`[api/auth/setup-password] ✅ Password setup successful for user: ${body.email}`);
return {
success: true,
message: 'Password set successfully! You can now log in to your account.',
data: {
email: body.email,
passwordSet: true,
canLogin: true
}
};
} catch (error: any) {
console.error('[api/auth/setup-password] ❌ Password setup failed:', error);
// Handle Keycloak specific errors
if (error.response?.status === 404) {
throw createError({
statusCode: 404,
statusMessage: 'User not found. Please register first or contact support.'
});
} else if (error.response?.status === 409) {
throw createError({
statusCode: 409,
statusMessage: 'Password has already been set. You can log in with your existing password.'
});
} else if (error.response?.status === 400) {
throw createError({
statusCode: 422,
statusMessage: 'Password does not meet Keycloak security requirements. Please choose a stronger password.'
});
}
throw error;
}
});

View File

@@ -128,6 +128,59 @@ export const cleanupOldEntries = (): void => {
console.log('🧹 Cleaned up old security entries');
};
// Password validation function
export const validatePassword = (password: string): { isValid: boolean; errors: string[] } => {
const errors: string[] = [];
if (!password || typeof password !== 'string') {
errors.push('Password is required');
return { isValid: false, errors };
}
if (password.length < 8) {
errors.push('Password must be at least 8 characters long');
}
if (password.length > 128) {
errors.push('Password must not exceed 128 characters');
}
if (!/[A-Z]/.test(password)) {
errors.push('Password must contain at least one uppercase letter');
}
if (!/[a-z]/.test(password)) {
errors.push('Password must contain at least one lowercase letter');
}
if (!/[0-9]/.test(password)) {
errors.push('Password must contain at least one number');
}
// Optional: require special characters
// if (!/[^A-Za-z0-9]/.test(password)) {
// errors.push('Password must contain at least one special character');
// }
// Check for common weak patterns
const commonPatterns = [
/(.)\1{2,}/i, // Three or more consecutive identical characters
/123456|654321|abcdef|qwerty|password|admin|login/i, // Common weak passwords
];
for (const pattern of commonPatterns) {
if (pattern.test(password)) {
errors.push('Password contains common patterns that make it weak');
break;
}
}
return {
isValid: errors.length === 0,
errors
};
};
// Initialize cleanup interval (runs every 5 minutes)
if (typeof setInterval !== 'undefined') {
setInterval(cleanupOldEntries, 5 * 60 * 1000);