/** * MFA Step-Up Context * * Provides global MFA step-up functionality that automatically * prompts users for MFA verification when operations require AAL2 */ import { createContext, useContext, useState, useCallback, ReactNode } from 'react'; import { useAuth } from '@/hooks/useAuth'; import { MFAStepUpModal } from '@/components/auth/MFAStepUpModal'; import { getEnrolledFactors } from '@/lib/authService'; import { isAAL2PolicyError, getAAL2ErrorMessage, MFACancelledError } from '@/lib/aalErrorDetection'; import { logger } from '@/lib/logger'; interface MFAStepUpContextType { /** * Wrap an operation that may require AAL2 verification * If operation fails due to AAL2, prompts user for MFA and retries */ requireAAL2: (operation: () => Promise, errorMessage?: string) => Promise; /** * Check if error is AAL2-related */ isAAL2Error: (error: unknown) => boolean; } const MFAStepUpContext = createContext(undefined); interface MFAStepUpProviderProps { children: ReactNode; } export function MFAStepUpProvider({ children }: MFAStepUpProviderProps) { const { session } = useAuth(); const [modalOpen, setModalOpen] = useState(false); const [factorId, setFactorId] = useState(''); const [pendingOperation, setPendingOperation] = useState<(() => Promise) | null>(null); const [pendingResolve, setPendingResolve] = useState<((value: any) => void) | null>(null); const [pendingReject, setPendingReject] = useState<((error: any) => void) | null>(null); /** * Handle successful MFA verification * Retries the pending operation with upgraded AAL2 session */ const handleMFASuccess = useCallback(async () => { setModalOpen(false); if (!pendingOperation || !pendingResolve || !pendingReject) { // Invalid state - missing pending operations return; } try { logger.log('[MFAStepUp] Retrying operation after AAL2 upgrade'); const result = await pendingOperation(); pendingResolve(result); } catch (error) { // Operation failed after AAL2 - will be handled by caller pendingReject(error); } finally { // Clean up setPendingOperation(null); setPendingResolve(null); setPendingReject(null); } }, [pendingOperation, pendingResolve, pendingReject]); /** * Handle MFA cancellation */ const handleMFACancel = useCallback(() => { setModalOpen(false); if (pendingReject) { pendingReject(new MFACancelledError()); } // Clean up setPendingOperation(null); setPendingResolve(null); setPendingReject(null); }, [pendingReject]); /** * Wrap an operation with AAL2 requirement checking */ const requireAAL2 = useCallback( async (operation: () => Promise, customErrorMessage?: string): Promise => { try { // Try the operation first return await operation(); } catch (error) { // Check if error is AAL2-related if (!isAAL2PolicyError(error)) { // Not an AAL2 error, re-throw throw error; } logger.log('[MFAStepUp] AAL2 error detected, prompting for MFA', { error: error instanceof Error ? error.message : String(error), }); // Get enrolled factors const factors = await getEnrolledFactors(); if (factors.length === 0) { // No MFA set up throw new Error('MFA is not set up for your account'); } const factor = factors[0]; setFactorId(factor.id); // Return a promise that resolves when MFA is verified return new Promise((resolve, reject) => { setPendingOperation(() => operation); setPendingResolve(() => resolve); setPendingReject(() => reject); setModalOpen(true); }); } }, [] ); const value: MFAStepUpContextType = { requireAAL2, isAAL2Error: isAAL2PolicyError, }; return ( {children} ); } /** * Hook to access MFA step-up functionality */ export function useMFAStepUp(): MFAStepUpContextType { const context = useContext(MFAStepUpContext); if (!context) { throw new Error('useMFAStepUp must be used within MFAStepUpProvider'); } return context; }