mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 14:11:13 -05:00
Update authentication flows to enforce AAL2 requirements for MFA operations and identity disconnections, and adjust TOTP verification logic. Replit-Commit-Author: Agent Replit-Commit-Session-Id: da324197-4d44-4e4b-b342-fe8ae33cf0cf Replit-Commit-Checkpoint-Type: intermediate_checkpoint
207 lines
6.5 KiB
TypeScript
207 lines
6.5 KiB
TypeScript
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4';
|
|
import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts';
|
|
|
|
const corsHeaders = {
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
|
};
|
|
|
|
Deno.serve(async (req) => {
|
|
const tracking = startRequest();
|
|
|
|
// Handle CORS preflight requests
|
|
if (req.method === 'OPTIONS') {
|
|
return new Response(null, {
|
|
headers: {
|
|
...corsHeaders,
|
|
'X-Request-ID': tracking.requestId
|
|
}
|
|
});
|
|
}
|
|
|
|
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) {
|
|
const duration = endRequest(tracking);
|
|
edgeLogger.error('Authentication failed', {
|
|
action: 'mfa_unenroll_auth',
|
|
requestId: tracking.requestId,
|
|
duration
|
|
});
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: 'Unauthorized',
|
|
requestId: tracking.requestId
|
|
}),
|
|
{
|
|
status: 401,
|
|
headers: {
|
|
...corsHeaders,
|
|
'Content-Type': 'application/json',
|
|
'X-Request-ID': tracking.requestId
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
edgeLogger.info('Processing MFA unenroll', {
|
|
action: 'mfa_unenroll',
|
|
requestId: tracking.requestId,
|
|
userId: user.id
|
|
});
|
|
|
|
// Phase 1: Check AAL level
|
|
const { data: { session } } = await supabaseClient.auth.getSession();
|
|
const { data: aalData } = await supabaseClient.auth.mfa.getAuthenticatorAssuranceLevel();
|
|
const aal = aalData?.currentLevel || 'aal1';
|
|
|
|
if (aal !== 'aal2') {
|
|
edgeLogger.warn('AAL2 required for MFA removal', { action: 'mfa_unenroll_aal', userId: user.id, 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) {
|
|
edgeLogger.warn('Role requires MFA, blocking removal', { action: 'mfa_unenroll_role', userId: user.id });
|
|
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) {
|
|
edgeLogger.warn('Rate limit exceeded', { action: 'mfa_unenroll_rate', userId: user.id, attempts: recentAttempts.length });
|
|
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) {
|
|
edgeLogger.error('Unenroll failed', { action: 'mfa_unenroll_fail', userId: user.id, error: unenrollError.message });
|
|
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) {
|
|
edgeLogger.error('Audit log failed', { action: 'mfa_unenroll_audit', userId: user.id });
|
|
}
|
|
|
|
// 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) {
|
|
edgeLogger.error('Notification failed', { action: 'mfa_unenroll_notification', userId: user.id });
|
|
}
|
|
|
|
const duration = endRequest(tracking);
|
|
edgeLogger.info('MFA successfully disabled', {
|
|
action: 'mfa_unenroll_success',
|
|
requestId: tracking.requestId,
|
|
userId: user.id,
|
|
duration
|
|
});
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
success: true,
|
|
requestId: tracking.requestId
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: {
|
|
...corsHeaders,
|
|
'Content-Type': 'application/json',
|
|
'X-Request-ID': tracking.requestId
|
|
}
|
|
}
|
|
);
|
|
|
|
} catch (error) {
|
|
const duration = endRequest(tracking);
|
|
edgeLogger.error('Unexpected error', {
|
|
action: 'mfa_unenroll_error',
|
|
requestId: tracking.requestId,
|
|
duration,
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: 'Internal server error',
|
|
requestId: tracking.requestId
|
|
}),
|
|
{
|
|
status: 500,
|
|
headers: {
|
|
...corsHeaders,
|
|
'Content-Type': 'application/json',
|
|
'X-Request-ID': tracking.requestId
|
|
}
|
|
}
|
|
);
|
|
}
|
|
});
|