Add password setup flow with server-side validation
All checks were successful
Build And Push Image / docker (push) Successful in 3m2s
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:
160
server/api/auth/setup-password.post.ts
Normal file
160
server/api/auth/setup-password.post.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user