Refactor: Implement full authentication overhaul

This commit is contained in:
gpt-engineer-app[bot]
2025-10-14 14:01:17 +00:00
parent ccfa83faee
commit 23f7cbb9de
8 changed files with 525 additions and 122 deletions

267
src/lib/authService.ts Normal file
View File

@@ -0,0 +1,267 @@
/**
* Centralized Authentication Service
* Handles all authentication flows with consistent AAL checking and MFA verification
*/
import { supabase } from '@/integrations/supabase/client';
import type { Session } from '@supabase/supabase-js';
import type {
AALLevel,
MFAFactor,
CheckAalResult,
AuthServiceResponse,
MFAChallengeResult
} from '@/types/auth';
import { setStepUpRequired, setAuthMethod, clearAllAuthFlags } from './sessionFlags';
/**
* 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<AALLevel> {
if (!session) return 'aal1';
try {
const { data, error } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel();
if (error) {
console.error('[AuthService] Error getting AAL:', error);
return 'aal1';
}
return (data.currentLevel as AALLevel) || 'aal1';
} catch (error) {
console.error('[AuthService] Exception getting AAL:', error);
return 'aal1';
}
}
/**
* Get enrolled MFA factors for the current user
*/
export async function getEnrolledFactors(): Promise<MFAFactor[]> {
try {
const { data, error } = await supabase.auth.mfa.listFactors();
if (error) {
console.error('[AuthService] Error listing factors:', error);
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) {
console.error('[AuthService] Exception listing factors:', error);
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<CheckAalResult> {
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<boolean> {
try {
const { data, error } = await supabase
.from('user_roles')
.select('role')
.eq('user_id', userId)
.in('role', ['admin', 'moderator']);
if (error) {
console.error('[AuthService] Error checking roles:', error);
return false;
}
return (data?.length || 0) > 0;
} catch (error) {
console.error('[AuthService] Exception checking roles:', error);
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<AuthServiceResponse<{ shouldRedirect: boolean; redirectTo?: string }>> {
try {
// Store auth method for audit logging
setAuthMethod(authMethod);
// Check if step-up is needed
const aalCheck = await checkAalStepUp(session);
if (aalCheck.needsStepUp) {
console.log(`[AuthService] ${authMethod} sign-in requires MFA step-up`);
// 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) {
console.error('[AuthService] Error in post-auth flow:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
/**
* Verify MFA challenge was successful and session upgraded to AAL2
*/
export async function verifyMfaUpgrade(session: Session | null): Promise<MFAChallengeResult> {
if (!session) {
return {
success: false,
error: 'No session found',
};
}
const currentAal = await getSessionAal(session);
if (currentAal !== 'aal2') {
console.error('[AuthService] MFA verification failed - still at AAL1');
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<string, any>
): Promise<void> {
try {
const { error } = await supabase.rpc('log_admin_action', {
_admin_user_id: userId,
_action: action,
_target_user_id: userId,
_details: details,
});
if (error) {
console.error('[AuthService] Error logging auth event:', error);
}
} catch (error) {
console.error('[AuthService] Exception logging auth event:', error);
}
}
/**
* Handle sign out with proper cleanup
*/
export async function signOutUser(): Promise<AuthServiceResponse> {
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) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}