import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4'; const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', }; Deno.serve(async (req) => { // Handle CORS preflight requests if (req.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }); } try { // Create Supabase client with user's auth token const supabaseClient = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_ANON_KEY')!, { global: { headers: { Authorization: req.headers.get('Authorization')! } } } ); // Get authenticated user const { data: { user }, error: userError } = await supabaseClient.auth.getUser(); if (userError || !user) { console.error('Authentication failed:', userError); return new Response( JSON.stringify({ error: 'Unauthorized' }), { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } console.log('[mfa-unenroll] Processing request for user:', user.id); // Phase 1: Check AAL level const { data: { session } } = await supabaseClient.auth.getSession(); const aal = session?.aal || 'aal1'; if (aal !== 'aal2') { console.warn('[mfa-unenroll] AAL2 required, current:', aal); return new Response( JSON.stringify({ error: 'AAL2 required to remove MFA' }), { status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } // Phase 2: Check if user's role requires MFA const { data: roles } = await supabaseClient .from('user_roles') .select('role') .eq('user_id', user.id); const requiresMFA = roles?.some(r => ['admin', 'moderator', 'superuser'].includes(r.role)); if (requiresMFA) { console.warn('[mfa-unenroll] Role requires MFA, blocking removal'); return new Response( JSON.stringify({ error: 'Your role requires MFA and it cannot be disabled' }), { status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } // Phase 4: Check rate limit (2 attempts per 24 hours) const { data: recentAttempts } = await supabaseClient .from('admin_audit_log') .select('created_at') .eq('admin_user_id', user.id) .eq('action', 'mfa_disabled') .gte('created_at', new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString()); if (recentAttempts && recentAttempts.length >= 2) { console.warn('[mfa-unenroll] Rate limit exceeded:', recentAttempts.length, 'attempts'); return new Response( JSON.stringify({ error: 'Rate limit exceeded. Try again in 24 hours.' }), { status: 429, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } // Get factor ID from request const { factorId } = await req.json(); if (!factorId) { return new Response( JSON.stringify({ error: 'Factor ID required' }), { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } // Phase 3: Proceed with unenrollment const { error: unenrollError } = await supabaseClient.auth.mfa.unenroll({ factorId }); if (unenrollError) { console.error('[mfa-unenroll] Unenroll failed:', unenrollError); return new Response( JSON.stringify({ error: unenrollError.message }), { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } // Audit log the action const { error: auditError } = await supabaseClient.from('admin_audit_log').insert({ admin_user_id: user.id, target_user_id: user.id, action: 'mfa_disabled', details: { factorId, timestamp: new Date().toISOString(), user_agent: req.headers.get('user-agent') || 'unknown', aal_level: aal } }); if (auditError) { console.error('[mfa-unenroll] Audit log failed:', auditError); } // Send security notification try { await supabaseClient.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 (notifError) { console.error('[mfa-unenroll] Notification failed:', notifError); } console.log('[mfa-unenroll] MFA successfully disabled for user:', user.id); return new Response( JSON.stringify({ success: true }), { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } catch (error) { console.error('[mfa-unenroll] Unexpected error:', error); return new Response( JSON.stringify({ error: 'Internal server error' }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } });