/** * Centralized Authentication Service * Handles all authentication flows with consistent AAL checking and MFA verification */ import { supabase } from '@/lib/supabaseClient'; import type { Session } from '@supabase/supabase-js'; import type { AALLevel, MFAFactor, CheckAalResult, AuthServiceResponse, MFAChallengeResult } from '@/types/auth'; import { setStepUpRequired, setAuthMethod, clearAllAuthFlags } from './sessionFlags'; import { logger } from './logger'; import { getErrorMessage, handleNonCriticalError } from './errorHandler'; /** * Extract AAL level from session using Supabase API * Always returns ground truth from server, not cached session data */ export async function getSessionAal(session: Session | null): Promise { if (!session) { logger.log('[AuthService] No session, returning aal1'); return 'aal1'; } try { const { data, error } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel(); logger.log('[AuthService] getSessionAal result', { hasData: !!data, currentLevel: data?.currentLevel, nextLevel: data?.nextLevel, error: error?.message }); if (error) { handleNonCriticalError(error, { action: 'Get session AAL', }); return 'aal1'; } const level = (data.currentLevel as AALLevel) || 'aal1'; logger.log('[AuthService] Returning AAL', { level }); return level; } catch (error: unknown) { handleNonCriticalError(error, { action: 'Get session AAL exception', }); return 'aal1'; } } /** * Get enrolled MFA factors for the current user */ export async function getEnrolledFactors(): Promise { try { const { data, error } = await supabase.auth.mfa.listFactors(); if (error) { handleNonCriticalError(error, { action: 'List MFA factors', }); return []; } return (data?.totp || []) .filter(f => f.status === 'verified') .map(f => ({ id: f.id, factor_type: 'totp' as const, status: 'verified' as const, friendly_name: f.friendly_name, created_at: f.created_at, updated_at: f.updated_at, })); } catch (error: unknown) { handleNonCriticalError(error, { action: 'List MFA factors exception', }); return []; } } /** * Check if user needs AAL step-up * Returns detailed information about enrollment and current AAL level */ export async function checkAalStepUp(session: Session | null): Promise { if (!session?.user) { return { needsStepUp: false, hasMfaEnrolled: false, currentLevel: 'aal1', hasEnrolledFactors: false, }; } const [currentLevel, factors] = await Promise.all([ getSessionAal(session), getEnrolledFactors(), ]); const hasEnrolledFactors = factors.length > 0; const needsStepUp = hasEnrolledFactors && currentLevel === 'aal1'; return { needsStepUp, hasMfaEnrolled: hasEnrolledFactors, currentLevel, hasEnrolledFactors, factorId: factors[0]?.id, }; } /** * Verify MFA is required for a user based on their role */ export async function verifyMfaRequired(userId: string): Promise { try { const { data, error } = await supabase .from('user_roles') .select('role') .eq('user_id', userId) .in('role', ['admin', 'moderator']); if (error) { handleNonCriticalError(error, { action: 'Verify MFA required', userId, }); return false; } return (data?.length || 0) > 0; } catch (error: unknown) { handleNonCriticalError(error, { action: 'Verify MFA required exception', userId, }); return false; } } /** * Handle post-authentication flow for all auth methods * Detects if MFA step-up is needed and redirects accordingly */ export async function handlePostAuthFlow( session: Session, authMethod: 'password' | 'oauth' | 'magiclink' ): Promise> { try { // Store auth method for audit logging setAuthMethod(authMethod); // Check if step-up is needed const aalCheck = await checkAalStepUp(session); if (aalCheck.needsStepUp) { logger.info('[AuthService] MFA step-up required', { authMethod, currentAal: aalCheck.currentLevel }); // Set flag and redirect to step-up page setStepUpRequired(true, window.location.pathname); // Log audit event await logAuthEvent(session.user.id, 'mfa_step_up_required', { auth_method: authMethod, current_aal: aalCheck.currentLevel, }); return { success: true, data: { shouldRedirect: true, redirectTo: '/auth/mfa-step-up', }, }; } // Log successful authentication await logAuthEvent(session.user.id, 'authentication_success', { auth_method: authMethod, aal: aalCheck.currentLevel, }); return { success: true, data: { shouldRedirect: false, }, }; } catch (error: unknown) { handleNonCriticalError(error, { action: 'Handle post-auth flow', metadata: { authMethod }, }); return { success: false, error: getErrorMessage(error), }; } } /** * Verify MFA challenge was successful and session upgraded to AAL2 */ export async function verifyMfaUpgrade(session: Session | null): Promise { if (!session) { return { success: false, error: 'No session found', }; } const currentAal = await getSessionAal(session); if (currentAal !== 'aal2') { handleNonCriticalError(new Error('MFA verification failed'), { action: 'Verify MFA upgrade', metadata: { expectedAal: 'aal2', actualAal: currentAal }, }); await logAuthEvent(session.user.id, 'mfa_verification_failed', { expected_aal: 'aal2', actual_aal: currentAal, }); return { success: false, error: 'Failed to upgrade session to AAL2', newAal: currentAal, }; } // Log successful upgrade await logAuthEvent(session.user.id, 'mfa_verification_success', { new_aal: currentAal, }); // Clear auth flags clearAllAuthFlags(); return { success: true, newAal: currentAal, }; } /** * Log authentication event to audit log */ async function logAuthEvent( userId: string, action: string, details: Record ): Promise { try { const { error } = await supabase.rpc('log_admin_action', { _admin_user_id: userId, _action: action, _target_user_id: userId, _details: details, }); if (error) { handleNonCriticalError(error, { action: 'Log auth event', metadata: { eventAction: action, userId }, }); } } catch (error: unknown) { handleNonCriticalError(error, { action: 'Log auth event exception', metadata: { eventAction: action, userId }, }); } } /** * Handle sign out with proper cleanup */ export async function signOutUser(): Promise { try { const { error } = await supabase.auth.signOut(); if (error) { return { success: false, error: error.message, }; } // Clear all session flags clearAllAuthFlags(); return { success: true }; } catch (error: unknown) { return { success: false, error: error instanceof Error ? error.message : 'Unknown error', }; } }