mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 12:11:17 -05:00
Fix: Implement RLS policies for MFA
This commit is contained in:
4
supabase/functions/_shared/cors.ts
Normal file
4
supabase/functions/_shared/cors.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
};
|
||||
128
supabase/functions/auth-with-mfa-check/index.ts
Normal file
128
supabase/functions/auth-with-mfa-check/index.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
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',
|
||||
};
|
||||
|
||||
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
|
||||
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
}
|
||||
|
||||
try {
|
||||
const { email, password } = await req.json();
|
||||
|
||||
if (!email || !password) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Email and password are required' }),
|
||||
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
// Create admin client
|
||||
const supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey, {
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Verify credentials using signInWithPassword (doesn't create session with admin client)
|
||||
const { data: authData, error: authError } = await supabaseAdmin.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (authError || !authData.user) {
|
||||
console.error('Auth error:', authError);
|
||||
return new Response(
|
||||
JSON.stringify({ error: authError?.message || 'Invalid credentials' }),
|
||||
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
const userId = authData.user.id;
|
||||
|
||||
// Check if user is banned
|
||||
const { data: profile, error: profileError } = await supabaseAdmin
|
||||
.from('profiles')
|
||||
.select('banned, ban_reason')
|
||||
.eq('user_id', userId)
|
||||
.single();
|
||||
|
||||
if (profileError) {
|
||||
console.error('Profile check error:', profileError);
|
||||
}
|
||||
|
||||
if (profile?.banned) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Account suspended',
|
||||
banned: true,
|
||||
banReason: profile.ban_reason
|
||||
}),
|
||||
{ status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
// Check for MFA enrollment
|
||||
const { data: factors, error: factorsError } = await supabaseAdmin.auth.admin.mfa.listFactors({
|
||||
userId,
|
||||
});
|
||||
|
||||
if (factorsError) {
|
||||
console.error('MFA factors check error:', factorsError);
|
||||
// Continue - assume no MFA if check fails
|
||||
}
|
||||
|
||||
const verifiedFactors = factors?.totp?.filter((f) => f.status === 'verified') || [];
|
||||
|
||||
if (verifiedFactors.length > 0) {
|
||||
// User has MFA - create a challenge but don't create session
|
||||
const factorId = verifiedFactors[0].id;
|
||||
|
||||
// Create MFA challenge
|
||||
const { data: challengeData, error: challengeError } = await supabaseAdmin.auth.mfa.challenge({
|
||||
factorId,
|
||||
});
|
||||
|
||||
if (challengeError) {
|
||||
console.error('Challenge creation error:', challengeError);
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Failed to create MFA challenge' }),
|
||||
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
mfaRequired: true,
|
||||
factorId,
|
||||
challengeId: challengeData.id,
|
||||
userId, // Needed for verification step
|
||||
}),
|
||||
{ status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
// No MFA required - return session
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
mfaRequired: false,
|
||||
session: authData.session,
|
||||
user: authData.user,
|
||||
}),
|
||||
{ status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Unexpected error:', error);
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Internal server error' }),
|
||||
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
});
|
||||
84
supabase/functions/check-mfa-enrollment/index.ts
Normal file
84
supabase/functions/check-mfa-enrollment/index.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
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',
|
||||
};
|
||||
|
||||
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
|
||||
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
}
|
||||
|
||||
try {
|
||||
const authHeader = req.headers.get('Authorization');
|
||||
if (!authHeader) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Missing authorization header' }),
|
||||
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
// Create client with user's token to verify session
|
||||
const supabase = createClient(supabaseUrl, supabaseServiceKey, {
|
||||
global: {
|
||||
headers: { Authorization: authHeader },
|
||||
},
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { data: { user }, error: userError } = await supabase.auth.getUser();
|
||||
|
||||
if (userError || !user) {
|
||||
console.error('User verification error:', userError);
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Unauthorized' }),
|
||||
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
// Create admin client to check MFA factors
|
||||
const supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey, {
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { data: factors, error: factorsError } = await supabaseAdmin.auth.admin.mfa.listFactors({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (factorsError) {
|
||||
console.error('MFA factors check error:', factorsError);
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Failed to check MFA enrollment' }),
|
||||
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
const verifiedFactors = factors?.totp?.filter((f) => f.status === 'verified') || [];
|
||||
const hasEnrolled = verifiedFactors.length > 0;
|
||||
const factorId = verifiedFactors.length > 0 ? verifiedFactors[0].id : undefined;
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
hasEnrolled,
|
||||
factorId,
|
||||
}),
|
||||
{ status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Unexpected error:', error);
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Internal server error' }),
|
||||
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
});
|
||||
91
supabase/functions/verify-mfa-and-login/index.ts
Normal file
91
supabase/functions/verify-mfa-and-login/index.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
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',
|
||||
};
|
||||
|
||||
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
|
||||
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
}
|
||||
|
||||
try {
|
||||
const { challengeId, factorId, code, userId } = await req.json();
|
||||
|
||||
if (!challengeId || !factorId || !code || !userId) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Missing required fields' }),
|
||||
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
// Create admin client
|
||||
const supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey, {
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Verify TOTP code
|
||||
const { data: verifyData, error: verifyError } = await supabaseAdmin.auth.mfa.verify({
|
||||
factorId,
|
||||
challengeId,
|
||||
code,
|
||||
});
|
||||
|
||||
if (verifyError || !verifyData) {
|
||||
console.error('MFA verification error:', verifyError);
|
||||
return new Response(
|
||||
JSON.stringify({ error: verifyError?.message || 'Invalid verification code' }),
|
||||
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
// Verification successful - create AAL2 session using admin API
|
||||
const { data: sessionData, error: sessionError } = await supabaseAdmin.auth.admin.createSession({
|
||||
userId,
|
||||
// This creates a session with AAL2
|
||||
});
|
||||
|
||||
if (sessionError || !sessionData) {
|
||||
console.error('Session creation error:', sessionError);
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Failed to create session' }),
|
||||
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
// Log successful MFA authentication
|
||||
try {
|
||||
await supabaseAdmin.rpc('log_admin_action', {
|
||||
_admin_user_id: userId,
|
||||
_target_user_id: userId,
|
||||
_action: 'mfa_login_success',
|
||||
_details: { timestamp: new Date().toISOString(), aal: 'aal2' },
|
||||
});
|
||||
} catch (logError) {
|
||||
console.error('Audit log error:', logError);
|
||||
// Don't fail the login if audit logging fails
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
session: sessionData.session,
|
||||
user: sessionData.user,
|
||||
}),
|
||||
{ status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Unexpected error:', error);
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Internal server error' }),
|
||||
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user