Fix: Implement RLS policies for MFA

This commit is contained in:
gpt-engineer-app[bot]
2025-10-31 17:12:59 +00:00
parent a9d4ee44e5
commit cdb1d0f762
10 changed files with 527 additions and 250 deletions

View File

@@ -35,6 +35,8 @@ export function AuthModal({ open, onOpenChange, defaultTab = 'signin' }: AuthMod
const [signInCaptchaToken, setSignInCaptchaToken] = useState<string | null>(null);
const [signInCaptchaKey, setSignInCaptchaKey] = useState(0);
const [mfaFactorId, setMfaFactorId] = useState<string | null>(null);
const [mfaChallengeId, setMfaChallengeId] = useState<string | null>(null);
const [mfaPendingUserId, setMfaPendingUserId] = useState<string | null>(null);
const [formData, setFormData] = useState({
email: '',
password: '',
@@ -70,82 +72,57 @@ export function AuthModal({ open, onOpenChange, defaultTab = 'signin' }: AuthMod
setSignInCaptchaToken(null);
try {
const signInOptions: any = {
email: formData.email,
password: formData.password,
};
if (tokenToUse) {
signInOptions.options = { captchaToken: tokenToUse };
// Call server-side auth check with MFA detection
const { data: authResult, error: authError } = await supabase.functions.invoke(
'auth-with-mfa-check',
{
body: {
email: formData.email,
password: formData.password,
captchaToken: tokenToUse,
},
}
);
if (authError || authResult.error) {
throw new Error(authResult?.error || authError?.message || 'Authentication failed');
}
const { data, error } = await supabase.auth.signInWithPassword(signInOptions);
if (error) throw error;
// CRITICAL: Check ban status immediately after successful authentication
const { data: profile } = await supabase
.from('profiles')
.select('banned, ban_reason')
.eq('user_id', data.user.id)
.single();
if (profile?.banned) {
// Sign out immediately
await supabase.auth.signOut();
const reason = profile.ban_reason
? `Reason: ${profile.ban_reason}`
// Check if user is banned
if (authResult.banned) {
const reason = authResult.banReason
? `Reason: ${authResult.banReason}`
: 'Contact support for assistance.';
toast({
variant: "destructive",
title: "Account Suspended",
description: `Your account has been suspended. ${reason}`,
duration: 10000
duration: 10000,
});
setLoading(false);
return; // Stop authentication flow
return;
}
// Check if MFA is required (user exists but no session)
if (data.user && !data.session) {
const totpFactor = data.user.factors?.find(f => f.factor_type === 'totp' && f.status === 'verified');
if (totpFactor) {
setMfaFactorId(totpFactor.id);
setLoading(false);
return;
}
// Check if MFA is required
if (authResult.mfaRequired) {
// NO SESSION EXISTS YET - show MFA challenge
console.log('[AuthModal] MFA required - no session created yet');
setMfaFactorId(authResult.factorId);
setMfaChallengeId(authResult.challengeId);
setMfaPendingUserId(authResult.userId);
setLoading(false);
return; // User has NO session - MFA modal will show
}
// No MFA required - user has session
console.log('[AuthModal] No MFA required - user authenticated');
// Track auth method for audit logging
setAuthMethod('password');
// Check if MFA step-up is required
const { handlePostAuthFlow } = await import('@/lib/authService');
const postAuthResult = await handlePostAuthFlow(data.session, 'password');
if (postAuthResult.success && postAuthResult.data.shouldRedirect) {
// CRITICAL SECURITY FIX: Get factor BEFORE destroying session
const { data: factors } = await supabase.auth.mfa.listFactors();
const totpFactor = factors?.totp?.find(f => f.status === 'verified');
if (totpFactor) {
// DESTROY the AAL1 session - user should NOT be logged in before MFA
console.log('[AuthModal] MFA required - destroying AAL1 session and storing credentials');
await supabase.auth.signOut();
// Store credentials in memory for re-authentication after TOTP
sessionStorage.setItem('mfa_pending_email_modal', formData.email);
sessionStorage.setItem('mfa_pending_password_modal', formData.password);
sessionStorage.setItem('mfa_factor_id_modal', totpFactor.id);
setMfaFactorId(totpFactor.id);
setLoading(false);
return; // User has NO session - MFA modal will show
}
// Set the session in Supabase client
if (authResult.session) {
await supabase.auth.setSession(authResult.session);
}
toast({
title: "Welcome back!",
description: "You've been signed in successfully."
@@ -171,47 +148,12 @@ export function AuthModal({ open, onOpenChange, defaultTab = 'signin' }: AuthMod
};
const handleMfaSuccess = async () => {
console.log('[AuthModal] MFA verification succeeded');
// Retrieve stored credentials
const email = sessionStorage.getItem('mfa_pending_email_modal');
const password = sessionStorage.getItem('mfa_pending_password_modal');
if (!email || !password) {
console.error('[AuthModal] Missing stored credentials for re-authentication');
toast({
title: "Authentication error",
description: "Please sign in again.",
variant: "destructive",
});
setMfaFactorId(null);
return;
}
// Clear stored credentials
sessionStorage.removeItem('mfa_pending_email_modal');
sessionStorage.removeItem('mfa_pending_password_modal');
sessionStorage.removeItem('mfa_factor_id_modal');
// Re-authenticate with stored credentials - this should create AAL2 session
console.log('[AuthModal] Re-authenticating with verified credentials');
const { error: reAuthError } = await supabase.auth.signInWithPassword({
email,
password,
});
if (reAuthError) {
console.error('[AuthModal] Re-authentication failed:', reAuthError);
toast({
title: "Authentication error",
description: "Please sign in again.",
variant: "destructive",
});
setMfaFactorId(null);
return;
}
console.log('[AuthModal] MFA verification succeeded - no further action needed');
// Clear state
setMfaFactorId(null);
setMfaChallengeId(null);
setMfaPendingUserId(null);
toast({
title: "Authentication complete",
@@ -224,12 +166,10 @@ export function AuthModal({ open, onOpenChange, defaultTab = 'signin' }: AuthMod
const handleMfaCancel = async () => {
console.log('[AuthModal] User cancelled MFA verification');
// Clear stored credentials
sessionStorage.removeItem('mfa_pending_email_modal');
sessionStorage.removeItem('mfa_pending_password_modal');
sessionStorage.removeItem('mfa_factor_id_modal');
// Clear state
setMfaFactorId(null);
setMfaChallengeId(null);
setMfaPendingUserId(null);
setSignInCaptchaKey(prev => prev + 1);
toast({
@@ -429,6 +369,8 @@ export function AuthModal({ open, onOpenChange, defaultTab = 'signin' }: AuthMod
{mfaFactorId ? (
<MFAChallenge
factorId={mfaFactorId}
challengeId={mfaChallengeId}
userId={mfaPendingUserId}
onSuccess={handleMfaSuccess}
onCancel={handleMfaCancel}
/>

View File

@@ -10,11 +10,13 @@ import { Shield, AlertCircle } from 'lucide-react';
interface MFAChallengeProps {
factorId: string;
challengeId?: string | null;
userId?: string | null;
onSuccess: () => void;
onCancel: () => void;
}
export function MFAChallenge({ factorId, onSuccess, onCancel }: MFAChallengeProps) {
export function MFAChallenge({ factorId, challengeId, userId, onSuccess, onCancel }: MFAChallengeProps) {
const { toast } = useToast();
const [code, setCode] = useState('');
const [loading, setLoading] = useState(false);
@@ -24,6 +26,38 @@ export function MFAChallenge({ factorId, onSuccess, onCancel }: MFAChallengeProp
setLoading(true);
try {
// NEW SERVER-SIDE FLOW: If we have challengeId and userId, use edge function
if (challengeId && userId) {
const { data: result, error: verifyError } = await supabase.functions.invoke(
'verify-mfa-and-login',
{
body: {
challengeId,
factorId,
code: code.trim(),
userId,
},
}
);
if (verifyError || result.error) {
throw new Error(result?.error || verifyError?.message || 'Verification failed');
}
// Set the session in Supabase client
if (result.session) {
await supabase.auth.setSession(result.session);
}
toast({
title: "Welcome back!",
description: "MFA verification successful."
});
onSuccess();
return;
}
// OLD FLOW: For OAuth/Magic Link step-up (existing session)
// Create fresh challenge for each verification attempt
const { data: challengeData, error: challengeError } =
await supabase.auth.mfa.challenge({ factorId });