Fix: Prevent AAL1 session on MFA login

This commit is contained in:
gpt-engineer-app[bot]
2025-10-31 16:51:25 +00:00
parent f36d6266be
commit a9d4ee44e5
3 changed files with 124 additions and 37 deletions

View File

@@ -131,13 +131,18 @@ export function AuthModal({ open, onOpenChange, defaultTab = 'signin' }: AuthMod
const totpFactor = factors?.totp?.find(f => f.status === 'verified'); const totpFactor = factors?.totp?.find(f => f.status === 'verified');
if (totpFactor) { if (totpFactor) {
// Keep AAL1 session active for MFA verification // DESTROY the AAL1 session - user should NOT be logged in before MFA
// RLS policies will block sensitive operations until AAL2 console.log('[AuthModal] MFA required - destroying AAL1 session and storing credentials');
console.log('[AuthModal] MFA required - keeping AAL1 session for verification'); 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); setMfaFactorId(totpFactor.id);
setLoading(false); setLoading(false);
return; // MFA modal will show, session-based MFA flow will work return; // User has NO session - MFA modal will show
} }
} }
@@ -166,30 +171,71 @@ export function AuthModal({ open, onOpenChange, defaultTab = 'signin' }: AuthMod
}; };
const handleMfaSuccess = async () => { const handleMfaSuccess = async () => {
// Verify AAL upgrade was successful console.log('[AuthModal] MFA verification succeeded');
const { data: { session } } = await supabase.auth.getSession();
const verification = await verifyMfaUpgrade(session);
if (!verification.success) { // 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({ toast({
title: "Authentication error",
description: "Please sign in again.",
variant: "destructive", variant: "destructive",
title: "MFA Verification Failed", });
description: verification.error || "Failed to upgrade session. Please try again." 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,
}); });
// Force sign out on verification failure if (reAuthError) {
await supabase.auth.signOut(); console.error('[AuthModal] Re-authentication failed:', reAuthError);
toast({
title: "Authentication error",
description: "Please sign in again.",
variant: "destructive",
});
setMfaFactorId(null); setMfaFactorId(null);
return; return;
} }
setMfaFactorId(null); setMfaFactorId(null);
toast({
title: "Authentication complete",
description: "You've been signed in successfully.",
});
onOpenChange(false); onOpenChange(false);
}; };
const handleMfaCancel = () => { 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');
setMfaFactorId(null); setMfaFactorId(null);
setSignInCaptchaKey(prev => prev + 1); setSignInCaptchaKey(prev => prev + 1);
toast({
title: "Authentication cancelled",
description: "Please sign in again when you're ready to complete two-factor authentication.",
});
}; };
const handleSignUp = async (e: React.FormEvent) => { const handleSignUp = async (e: React.FormEvent) => {

View File

@@ -155,14 +155,19 @@ export default function Auth() {
const totpFactor = factors?.totp?.find(f => f.status === 'verified'); const totpFactor = factors?.totp?.find(f => f.status === 'verified');
if (totpFactor) { if (totpFactor) {
// Keep AAL1 session active for MFA verification // DESTROY the AAL1 session - user should NOT be logged in before MFA
// RLS policies will block sensitive operations until AAL2 console.log('[Auth] MFA required - destroying AAL1 session and storing credentials');
console.log('[Auth] MFA required - keeping AAL1 session for verification'); await supabase.auth.signOut();
// Store credentials in memory for re-authentication after TOTP
sessionStorage.setItem('mfa_pending_email', formData.email);
sessionStorage.setItem('mfa_pending_password', formData.password);
sessionStorage.setItem('mfa_factor_id', totpFactor.id);
setMfaPendingEmail(formData.email); setMfaPendingEmail(formData.email);
setMfaFactorId(totpFactor.id); setMfaFactorId(totpFactor.id);
setLoading(false); setLoading(false);
return; // MFA modal will show, session-based MFA flow will work return; // User has NO session - MFA modal will show
} else { } else {
// MFA is required but no factor found - FORCE SIGN OUT for security // MFA is required but no factor found - FORCE SIGN OUT for security
console.error('[Auth] SECURITY: MFA required but no verified factor found'); console.error('[Auth] SECURITY: MFA required but no verified factor found');
@@ -225,40 +230,76 @@ export default function Auth() {
}; };
const handleMfaSuccess = async () => { const handleMfaSuccess = async () => {
// Verify AAL upgrade was successful console.log('[Auth] MFA verification succeeded');
const { data: { session } } = await supabase.auth.getSession();
const verification = await verifyMfaUpgrade(session);
if (!verification.success) { // Retrieve stored credentials
const email = sessionStorage.getItem('mfa_pending_email');
const password = sessionStorage.getItem('mfa_pending_password');
if (!email || !password) {
console.error('[Auth] Missing stored credentials for re-authentication');
toast({ toast({
title: "Authentication error",
description: "Please sign in again.",
variant: "destructive", variant: "destructive",
title: "MFA Verification Failed", });
description: verification.error || "Failed to upgrade session. Please try again." setMfaFactorId(null);
setMfaPendingEmail(null);
return;
}
// Clear stored credentials
sessionStorage.removeItem('mfa_pending_email');
sessionStorage.removeItem('mfa_pending_password');
sessionStorage.removeItem('mfa_factor_id');
// Re-authenticate with stored credentials - this should create AAL2 session
console.log('[Auth] Re-authenticating with verified credentials');
const { error: reAuthError } = await supabase.auth.signInWithPassword({
email,
password,
}); });
// Force sign out on verification failure if (reAuthError) {
await supabase.auth.signOut(); console.error('[Auth] Re-authentication failed:', reAuthError);
toast({
title: "Authentication error",
description: "Please sign in again.",
variant: "destructive",
});
setMfaFactorId(null); setMfaFactorId(null);
setMfaPendingEmail(null);
return; return;
} }
setMfaFactorId(null); setMfaFactorId(null);
setMfaPendingEmail(null);
toast({ toast({
title: "Welcome back!", title: "Authentication complete",
description: "You've been signed in successfully." description: "You've been signed in successfully.",
}); });
setTimeout(() => {
navigate('/');
}, 500);
}; };
const handleMfaCancel = async () => { const handleMfaCancel = async () => {
// Clear state variables console.log('[Auth] User cancelled MFA verification');
// Clear stored credentials
sessionStorage.removeItem('mfa_pending_email');
sessionStorage.removeItem('mfa_pending_password');
sessionStorage.removeItem('mfa_factor_id');
setMfaFactorId(null); setMfaFactorId(null);
setMfaPendingEmail(null); setMfaPendingEmail(null);
setSignInCaptchaKey(prev => prev + 1); setSignInCaptchaKey(prev => prev + 1);
toast({ toast({
title: "Sign in cancelled", title: "Authentication cancelled",
description: "Two-factor authentication is required for your account. Please sign in again and complete MFA verification.", description: "Please sign in again when you're ready to complete two-factor authentication.",
variant: "destructive"
}); });
}; };
const handleSignUp = async (e: React.FormEvent) => { const handleSignUp = async (e: React.FormEvent) => {

View File

@@ -119,13 +119,13 @@ export default function AuthCallback() {
const totpFactor = factors?.totp?.find(f => f.status === 'verified'); const totpFactor = factors?.totp?.find(f => f.status === 'verified');
if (totpFactor) { if (totpFactor) {
// Keep AAL1 session active for MFA verification // OAuth flow: We can't store the OAuth token, so we keep the AAL1 session
// RLS policies will block sensitive operations until AAL2 // This is unavoidable for OAuth flows - but RLS blocks sensitive operations
console.log('[AuthCallback] MFA required - keeping AAL1 session for verification'); console.log('[AuthCallback] OAuth MFA required - keeping AAL1 session (OAuth limitation)');
setMfaFactorId(totpFactor.id); setMfaFactorId(totpFactor.id);
setStatus('mfa_required'); setStatus('mfa_required');
return; // MFA modal will show, session-based MFA flow will work return;
} }
} }