mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 14:11:13 -05:00
Fix: Prevent AAL1 session on MFA login
This commit is contained in:
@@ -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) => {
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user