From 0eac7f3d7dcc0094cfe78b2410e04e5f77fbc1eb Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 19:17:46 +0000 Subject: [PATCH] feat: Implement modal-based MFA step-up --- src/App.tsx | 2 - src/components/auth/AuthModal.tsx | 18 ++++ src/components/auth/MFAStepUpModal.tsx | 34 ++++++++ src/pages/Auth.tsx | 18 ++++ src/pages/AuthCallback.tsx | 58 +++++++++++-- src/pages/MFAStepUp.tsx | 109 ------------------------- 6 files changed, 123 insertions(+), 116 deletions(-) create mode 100644 src/components/auth/MFAStepUpModal.tsx delete mode 100644 src/pages/MFAStepUp.tsx diff --git a/src/App.tsx b/src/App.tsx index 75c1cab7..964bdb6f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -50,7 +50,6 @@ import BlogPost from "./pages/BlogPost"; import AdminBlog from "./pages/AdminBlog"; import ForceLogout from "./pages/ForceLogout"; import AuthCallback from "./pages/AuthCallback"; -import MFAStepUp from "./pages/MFAStepUp"; const queryClient = new QueryClient({ defaultOptions: { @@ -101,7 +100,6 @@ function AppContent() { } /> } /> } /> - } /> } /> } /> } /> diff --git a/src/components/auth/AuthModal.tsx b/src/components/auth/AuthModal.tsx index 111efa1f..7d05bf07 100644 --- a/src/components/auth/AuthModal.tsx +++ b/src/components/auth/AuthModal.tsx @@ -93,6 +93,24 @@ export function AuthModal({ open, onOpenChange, defaultTab = 'signin' }: AuthMod // 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) { + console.log('[AuthModal] MFA step-up required'); + + // Get the TOTP factor ID + const { data: factors } = await supabase.auth.mfa.listFactors(); + const totpFactor = factors?.totp?.find(f => f.status === 'verified'); + + if (totpFactor) { + setMfaFactorId(totpFactor.id); + setLoading(false); + return; // Stay in modal, show MFA challenge + } + } toast({ title: "Welcome back!", diff --git a/src/components/auth/MFAStepUpModal.tsx b/src/components/auth/MFAStepUpModal.tsx new file mode 100644 index 00000000..a8133b58 --- /dev/null +++ b/src/components/auth/MFAStepUpModal.tsx @@ -0,0 +1,34 @@ +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { MFAChallenge } from './MFAChallenge'; +import { Shield } from 'lucide-react'; + +interface MFAStepUpModalProps { + open: boolean; + factorId: string; + onSuccess: () => void; + onCancel: () => void; +} + +export function MFAStepUpModal({ open, factorId, onSuccess, onCancel }: MFAStepUpModalProps) { + return ( + !isOpen && onCancel()}> + e.preventDefault()}> + +
+ + Additional Verification Required +
+ + Your role requires two-factor authentication. Please verify your identity to continue. + +
+ + +
+
+ ); +} diff --git a/src/pages/Auth.tsx b/src/pages/Auth.tsx index 3f1c9ba5..f682c073 100644 --- a/src/pages/Auth.tsx +++ b/src/pages/Auth.tsx @@ -121,6 +121,24 @@ export default function Auth() { // 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) { + console.log('[Auth] MFA step-up required'); + + // Get the TOTP factor ID + const { data: factors } = await supabase.auth.mfa.listFactors(); + const totpFactor = factors?.totp?.find(f => f.status === 'verified'); + + if (totpFactor) { + setMfaFactorId(totpFactor.id); + setLoading(false); + return; // Stay on page, show MFA modal + } + } console.log('[Auth] Sign in successful', { user: data.user?.email, diff --git a/src/pages/AuthCallback.tsx b/src/pages/AuthCallback.tsx index 1ebbf5cd..053993da 100644 --- a/src/pages/AuthCallback.tsx +++ b/src/pages/AuthCallback.tsx @@ -4,18 +4,20 @@ import { supabase } from '@/integrations/supabase/client'; import { useToast } from '@/hooks/use-toast'; import { Loader2, CheckCircle2 } from 'lucide-react'; import { Header } from '@/components/layout/Header'; -import { handlePostAuthFlow } from '@/lib/authService'; +import { handlePostAuthFlow, verifyMfaUpgrade } from '@/lib/authService'; import type { AuthMethod } from '@/types/auth'; import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { getErrorMessage } from '@/lib/errorHandler'; +import { MFAStepUpModal } from '@/components/auth/MFAStepUpModal'; export default function AuthCallback() { const navigate = useNavigate(); const { toast } = useToast(); - const [status, setStatus] = useState<'processing' | 'success' | 'error'>('processing'); + const [status, setStatus] = useState<'processing' | 'success' | 'error' | 'mfa_required'>('processing'); + const [mfaFactorId, setMfaFactorId] = useState(null); const [isRecoveryMode, setIsRecoveryMode] = useState(false); const [newPassword, setNewPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); @@ -105,9 +107,17 @@ export default function AuthCallback() { const result = await handlePostAuthFlow(session, authMethod); if (result.success && result.data?.shouldRedirect) { - console.log('[AuthCallback] Redirecting to:', result.data.redirectTo); - navigate(result.data.redirectTo); - return; + console.log('[AuthCallback] MFA step-up required - showing modal'); + + // Get factor ID and show modal instead of redirecting + const { data: factors } = await supabase.auth.mfa.listFactors(); + const totpFactor = factors?.totp?.find(f => f.status === 'verified'); + + if (totpFactor) { + setMfaFactorId(totpFactor.id); + setStatus('mfa_required'); + return; + } } setStatus('success'); @@ -146,6 +156,32 @@ export default function AuthCallback() { processOAuthCallback(); }, [navigate, toast]); + const handleMfaSuccess = async () => { + const { data: { session } } = await supabase.auth.getSession(); + const verification = await verifyMfaUpgrade(session); + + if (!verification.success) { + toast({ + variant: "destructive", + title: "MFA Verification Failed", + description: verification.error || "Failed to upgrade session." + }); + await supabase.auth.signOut(); + navigate('/auth'); + return; + } + + setMfaFactorId(null); + setStatus('success'); + + toast({ + title: 'Welcome to ThrillWiki!', + description: 'You have been signed in successfully.', + }); + + setTimeout(() => navigate('/'), 500); + }; + const handlePasswordReset = async (e: React.FormEvent) => { e.preventDefault(); @@ -288,6 +324,18 @@ export default function AuthCallback() { )} + + {status === 'mfa_required' && mfaFactorId && ( + { + setMfaFactorId(null); + navigate('/auth'); + }} + /> + )} ); } diff --git a/src/pages/MFAStepUp.tsx b/src/pages/MFAStepUp.tsx deleted file mode 100644 index aff0ee7b..00000000 --- a/src/pages/MFAStepUp.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { useEffect, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { supabase } from '@/integrations/supabase/client'; -import { useToast } from '@/hooks/use-toast'; -import { Header } from '@/components/layout/Header'; -import { MFAChallenge } from '@/components/auth/MFAChallenge'; -import { Shield } from 'lucide-react'; -import { getStepUpRequired, getIntendedPath, clearStepUpFlags } from '@/lib/sessionFlags'; -import { getEnrolledFactors } from '@/lib/authService'; - -export default function MFAStepUp() { - const navigate = useNavigate(); - const { toast } = useToast(); - const [factorId, setFactorId] = useState(null); - - useEffect(() => { - const checkStepUpRequired = async () => { - // Check if this page was accessed via proper flow - if (!getStepUpRequired()) { - console.log('[MFAStepUp] No step-up flag found, redirecting to auth'); - navigate('/auth'); - return; - } - - // Get enrolled MFA factors - const factors = await getEnrolledFactors(); - - if (factors.length === 0) { - console.log('[MFAStepUp] No verified TOTP factor found'); - toast({ - variant: 'destructive', - title: 'MFA not enrolled', - description: 'Please enroll in two-factor authentication first.', - }); - clearStepUpFlags(); - navigate('/settings?tab=security'); - return; - } - - setFactorId(factors[0].id); - }; - - checkStepUpRequired(); - }, [navigate, toast]); - - const handleSuccess = async () => { - console.log('[MFAStepUp] MFA verification successful'); - - toast({ - title: 'Verification successful', - description: 'You now have full access to all features.', - }); - - // Redirect to home or intended destination - const intendedPath = getIntendedPath(); - clearStepUpFlags(); - navigate(intendedPath); - }; - - const handleCancel = async () => { - console.log('[MFAStepUp] MFA verification cancelled'); - - // Clear flags and redirect to sign-in (less harsh than forcing sign-out) - clearStepUpFlags(); - - toast({ - title: 'Verification cancelled', - description: 'Please sign in again to continue.', - }); - - navigate('/auth'); - }; - - return ( -
-
- -
-
-
-
-
-
- -
-
-

Additional Verification Required

-

- Your role requires two-factor authentication. Please verify your identity to continue. -

-
- - {factorId ? ( - - ) : ( -
-
-
- )} -
-
-
-
- ); -}