mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-24 04:11:12 -05:00
Fix: Implement MFA removal protection
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -8,8 +8,9 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { handleError, handleSuccess, handleInfo, AppError, getErrorMessage } from '@/lib/errorHandler';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { useRequireMFA } from '@/hooks/useRequireMFA';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { Smartphone, Shield, Copy, Eye, EyeOff, Trash2 } from 'lucide-react';
|
||||
import { Smartphone, Shield, Copy, Eye, EyeOff, Trash2, AlertTriangle } from 'lucide-react';
|
||||
import { MFARemovalDialog } from './MFARemovalDialog';
|
||||
import { setStepUpRequired, getAuthMethod } from '@/lib/sessionFlags';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@@ -17,6 +18,7 @@ import type { MFAFactor } from '@/types/auth';
|
||||
|
||||
export function TOTPSetup() {
|
||||
const { user } = useAuth();
|
||||
const { requiresMFA } = useRequireMFA();
|
||||
const navigate = useNavigate();
|
||||
const [factors, setFactors] = useState<MFAFactor[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -274,6 +276,16 @@ export function TOTPSetup() {
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{/* Phase 2: Warning for role-required users */}
|
||||
{requiresMFA && (
|
||||
<Alert variant="default" className="border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-950">
|
||||
<AlertTriangle className="w-4 h-4 text-amber-600 dark:text-amber-400" />
|
||||
<AlertDescription className="text-amber-800 dark:text-amber-200">
|
||||
Your role requires MFA. You cannot disable two-factor authentication.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-green-50 dark:bg-green-950 rounded-full flex items-center justify-center">
|
||||
|
||||
Reference in New Issue
Block a user