Fix: Implement MFA removal protection

This commit is contained in:
gpt-engineer-app[bot]
2025-10-17 19:50:29 +00:00
parent 8a36c71edb
commit a0d341c4e0
4 changed files with 201 additions and 33 deletions

View File

@@ -1,7 +1,8 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
import { getErrorMessage } from '@/lib/errorHandler';
import { useRequireMFA } from '@/hooks/useRequireMFA';
import {
AlertDialog,
AlertDialogAction,
@@ -26,11 +27,29 @@ interface MFARemovalDialogProps {
}
export function MFARemovalDialog({ open, onOpenChange, factorId, onSuccess }: MFARemovalDialogProps) {
const { requiresMFA } = useRequireMFA();
const [step, setStep] = useState<'password' | 'totp' | 'confirm'>('password');
const [password, setPassword] = useState('');
const [totpCode, setTotpCode] = useState('');
const [loading, setLoading] = useState(false);
// Phase 1: Check AAL2 requirement on dialog open
useEffect(() => {
if (open) {
const checkAalLevel = async () => {
const { data: { session } } = await supabase.auth.getSession();
const currentAal = (session as any)?.aal || 'aal1';
if (currentAal !== 'aal2') {
toast.error('Please verify your identity with MFA before making security changes');
onOpenChange(false);
}
};
checkAalLevel();
}
}, [open, onOpenChange]);
const handleClose = () => {
setStep('password');
setPassword('');
@@ -93,6 +112,14 @@ export function MFARemovalDialog({ open, onOpenChange, factorId, onSuccess }: MF
if (verifyError) throw verifyError;
// Phase 1: Verify session is at AAL2 after TOTP verification
const { data: { session } } = await supabase.auth.getSession();
const currentAal = (session as any)?.aal || 'aal1';
if (currentAal !== 'aal2') {
throw new Error('Session must be at AAL2 to remove MFA');
}
toast.success('TOTP code verified');
setStep('confirm');
} catch (error) {
@@ -104,39 +131,22 @@ export function MFARemovalDialog({ open, onOpenChange, factorId, onSuccess }: MF
};
const handleMFARemoval = async () => {
// Phase 2: Check if user's role requires MFA
if (requiresMFA) {
toast.error('Your role requires two-factor authentication and it cannot be disabled');
handleClose();
return;
}
setLoading(true);
try {
// Unenroll the factor
const { error } = await supabase.auth.mfa.unenroll({ factorId });
// Phase 3: Call edge function instead of direct unenroll
const { data, error } = await supabase.functions.invoke('mfa-unenroll', {
body: { factorId }
});
if (error) throw error;
// Log the action
const { data: { user } } = await supabase.auth.getUser();
if (user) {
await supabase.from('admin_audit_log').insert({
admin_user_id: user.id,
target_user_id: user.id,
action: 'mfa_disabled',
details: {
factor_id: factorId,
timestamp: new Date().toISOString(),
user_agent: navigator.userAgent
}
});
// Trigger email notification
await supabase.functions.invoke('trigger-notification', {
body: {
userId: user.id,
workflowId: 'security-alert',
payload: {
action: 'MFA Disabled',
message: 'Two-factor authentication has been disabled on your account.',
timestamp: new Date().toISOString()
}
}
}).catch(err => console.error('Notification error:', err));
}
if (data?.error) throw new Error(data.error);
toast.success('Two-factor authentication has been disabled');
handleClose();