mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 17:51:12 -05:00
Fix: Implement MFA security fix
This commit is contained in:
@@ -5,7 +5,8 @@ import { getErrorMessage } from '@/lib/errorHandler';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp';
|
import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp';
|
||||||
import { Shield } from 'lucide-react';
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||||
|
import { Shield, AlertCircle } from 'lucide-react';
|
||||||
|
|
||||||
interface MFAChallengeProps {
|
interface MFAChallengeProps {
|
||||||
factorId: string;
|
factorId: string;
|
||||||
@@ -59,6 +60,14 @@ export function MFAChallenge({ factorId, onSuccess, onCancel }: MFAChallengeProp
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
<Alert className="border-destructive/50 text-destructive dark:border-destructive dark:text-destructive">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertTitle>Security Verification Required</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Cancelling will sign you out completely. Two-factor authentication must be completed to access your account.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 text-primary">
|
<div className="flex items-center gap-2 text-primary">
|
||||||
<Shield className="w-5 h-5" />
|
<Shield className="w-5 h-5" />
|
||||||
<h3 className="font-semibold">Two-Factor Authentication</h3>
|
<h3 className="font-semibold">Two-Factor Authentication</h3>
|
||||||
@@ -96,7 +105,7 @@ export function MFAChallenge({ factorId, onSuccess, onCancel }: MFAChallengeProp
|
|||||||
className="flex-1"
|
className="flex-1"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel & Sign Out
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleVerify}
|
onClick={handleVerify}
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ interface MFAStepUpModalProps {
|
|||||||
export function MFAStepUpModal({ open, factorId, onSuccess, onCancel }: MFAStepUpModalProps) {
|
export function MFAStepUpModal({ open, factorId, onSuccess, onCancel }: MFAStepUpModalProps) {
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onCancel()}>
|
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onCancel()}>
|
||||||
<DialogContent className="sm:max-w-md" onInteractOutside={(e) => e.preventDefault()}>
|
<DialogContent
|
||||||
|
className="sm:max-w-md"
|
||||||
|
onInteractOutside={(e) => e.preventDefault()}
|
||||||
|
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<div className="flex items-center gap-2 justify-center mb-2">
|
<div className="flex items-center gap-2 justify-center mb-2">
|
||||||
<Shield className="h-6 w-6 text-primary" />
|
<Shield className="h-6 w-6 text-primary" />
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import { Label } from '@/components/ui/label';
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Zap, Mail, Lock, User, AlertCircle, Eye, EyeOff } from 'lucide-react';
|
import { Zap, Mail, Lock, User, AlertCircle, Eye, EyeOff, Shield } from 'lucide-react';
|
||||||
import { supabase } from '@/integrations/supabase/client';
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import { getErrorMessage } from '@/lib/errorHandler';
|
import { getErrorMessage } from '@/lib/errorHandler';
|
||||||
@@ -228,9 +229,43 @@ export default function Auth() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMfaCancel = () => {
|
const handleMfaCancel = async () => {
|
||||||
|
try {
|
||||||
|
// CRITICAL SECURITY: Log cancellation attempt
|
||||||
|
const { data: { session } } = await supabase.auth.getSession();
|
||||||
|
if (session) {
|
||||||
|
try {
|
||||||
|
const { data: aalData } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel();
|
||||||
|
await supabase.rpc('log_admin_action', {
|
||||||
|
_admin_user_id: session.user.id,
|
||||||
|
_action: 'mfa_verification_cancelled',
|
||||||
|
_target_user_id: session.user.id,
|
||||||
|
_details: {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
reason: 'user_cancelled_mfa_prompt',
|
||||||
|
aal_before_cancel: aalData?.currentLevel || 'aal1'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (logError) {
|
||||||
|
console.error('Failed to log MFA cancellation:', logError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during MFA cancellation:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRITICAL SECURITY: User cannot bypass MFA if enrolled
|
||||||
|
// Cancelling MFA prompt MUST sign the user out
|
||||||
|
await supabase.auth.signOut();
|
||||||
|
|
||||||
setMfaFactorId(null);
|
setMfaFactorId(null);
|
||||||
setSignInCaptchaKey(prev => prev + 1);
|
setSignInCaptchaKey(prev => prev + 1);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Sign in cancelled",
|
||||||
|
description: "Two-factor authentication is required for your account. Please sign in again and complete MFA verification.",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
};
|
};
|
||||||
const handleSignUp = async (e: React.FormEvent) => {
|
const handleSignUp = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -427,11 +462,33 @@ export default function Auth() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{mfaFactorId ? (
|
{mfaFactorId ? (
|
||||||
<MFAChallenge
|
<Dialog open={!!mfaFactorId} onOpenChange={(isOpen) => {
|
||||||
factorId={mfaFactorId}
|
if (!isOpen) {
|
||||||
onSuccess={handleMfaSuccess}
|
handleMfaCancel();
|
||||||
onCancel={handleMfaCancel}
|
}
|
||||||
/>
|
}}>
|
||||||
|
<DialogContent
|
||||||
|
className="sm:max-w-md"
|
||||||
|
onInteractOutside={(e) => e.preventDefault()}
|
||||||
|
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<div className="flex items-center gap-2 justify-center mb-2">
|
||||||
|
<Shield className="h-6 w-6 text-primary" />
|
||||||
|
<DialogTitle>Two-Factor Authentication Required</DialogTitle>
|
||||||
|
</div>
|
||||||
|
<DialogDescription className="text-center">
|
||||||
|
Your account security settings require MFA verification to continue.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<MFAChallenge
|
||||||
|
factorId={mfaFactorId}
|
||||||
|
onSuccess={handleMfaSuccess}
|
||||||
|
onCancel={handleMfaCancel}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<form onSubmit={handleSignIn} className="space-y-4">
|
<form onSubmit={handleSignIn} className="space-y-4">
|
||||||
|
|||||||
Reference in New Issue
Block a user