Skip to content

setup-mfa

Access: /template setup-mfa or /t setup-mfa

Configures Multi-Factor Authentication (MFA) in your MakerKit project using Time-based One-Time Passwords (TOTP) with authenticator apps.

What It Does

The setup-mfa command helps you:

  • Enable MFA/2FA in Supabase Auth
  • Create MFA enrollment UI components
  • Implement MFA verification flows
  • Add backup codes functionality
  • Create MFA management settings

Usage

bash
/template setup-mfa "Description"
# or
/t setup-mfa "Description"

When prompted, specify:

  • MFA requirements (optional or mandatory)
  • Backup codes configuration
  • Grace period for mandatory MFA
  • UI placement (onboarding or settings)

Prerequisites

  • A MakerKit project with authentication
  • Supabase Auth configured
  • Commercial MakerKit license from MakerKit

What Gets Created

1. MFA Configuration

Database migrations for MFA settings:

sql
-- Enable MFA in auth config
alter table auth.users 
add column if not exists mfa_enabled boolean default false;

-- Store user MFA preferences
create table public.user_mfa_settings (
  user_id uuid primary key references auth.users(id) on delete cascade,
  mfa_required boolean default false,
  mfa_enabled_at timestamptz,
  backup_codes_generated_at timestamptz,
  created_at timestamptz default now(),
  updated_at timestamptz default now()
);

-- RLS policies
alter table public.user_mfa_settings enable row level security;

create policy "Users can view own MFA settings"
  on public.user_mfa_settings for select
  to authenticated
  using (user_id = auth.uid());

create policy "Users can update own MFA settings"
  on public.user_mfa_settings for update
  to authenticated
  using (user_id = auth.uid());

2. MFA Enrollment Component

UI for setting up MFA:

typescript
// components/mfa/mfa-enrollment.tsx
'use client';

import { useState } from 'react';
import { QRCodeSVG } from 'qrcode.react';
import { Button } from '@kit/ui/button';
import { Input } from '@kit/ui/input';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@kit/ui/card';
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
import { toast } from 'sonner';

export function MFAEnrollment() {
  const supabase = useSupabase();
  const [qrCode, setQrCode] = useState<string>('');
  const [secret, setSecret] = useState<string>('');
  const [verificationCode, setVerificationCode] = useState('');
  const [isEnrolling, setIsEnrolling] = useState(false);

  const startEnrollment = async () => {
    setIsEnrolling(true);
    
    const { data, error } = await supabase.auth.mfa.enroll({
      factorType: 'totp',
    });

    if (error) {
      toast.error('Failed to start MFA enrollment');
      setIsEnrolling(false);
      return;
    }

    setQrCode(data.totp.qr_code);
    setSecret(data.totp.secret);
  };

  const verifyAndEnable = async () => {
    const { data, error } = await supabase.auth.mfa.verify({
      factorId: data.id,
      challengeId: data.id,
      code: verificationCode,
    });

    if (error) {
      toast.error('Invalid verification code');
      return;
    }

    // Update user settings
    await updateMFASettings(true);
    
    toast.success('MFA enabled successfully');
    onComplete();
  };

  return (
    <Card>
      <CardHeader>
        <CardTitle>Enable Two-Factor Authentication</CardTitle>
        <CardDescription>
          Enhance your account security with 2FA
        </CardDescription>
      </CardHeader>
      <CardContent className="space-y-4">
        {!qrCode ? (
          <Button onClick={startEnrollment} disabled={isEnrolling}>
            {isEnrolling ? 'Starting...' : 'Start Setup'}
          </Button>
        ) : (
          <>
            <div className="space-y-4">
              <div className="text-center">
                <p className="text-sm text-muted-foreground mb-4">
                  Scan this QR code with your authenticator app
                </p>
                <div className="inline-block p-4 bg-white rounded-lg">
                  <QRCodeSVG value={qrCode} size={200} />
                </div>
              </div>
              
              <div className="space-y-2">
                <p className="text-sm text-muted-foreground">
                  Or enter this code manually:
                </p>
                <code className="block p-2 bg-muted rounded text-xs">
                  {secret}
                </code>
              </div>
              
              <div className="space-y-2">
                <label className="text-sm font-medium">
                  Enter verification code
                </label>
                <Input
                  type="text"
                  inputMode="numeric"
                  pattern="[0-9]*"
                  maxLength={6}
                  value={verificationCode}
                  onChange={(e) => setVerificationCode(e.target.value)}
                  placeholder="000000"
                />
              </div>
              
              <Button
                onClick={verifyAndEnable}
                disabled={verificationCode.length !== 6}
                className="w-full"
              >
                Verify and Enable 2FA
              </Button>
            </div>
          </>
        )}
      </CardContent>
    </Card>
  );
}

3. MFA Challenge Component

For login verification:

typescript
// components/mfa/mfa-challenge.tsx
'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { Button } from '@kit/ui/button';
import { Input } from '@kit/ui/input';
import { Card } from '@kit/ui/card';
import { useSupabase } from '@kit/supabase/hooks/use-supabase';

export function MFAChallenge({ 
  onSuccess,
  redirectTo = '/dashboard' 
}: {
  onSuccess?: () => void;
  redirectTo?: string;
}) {
  const supabase = useSupabase();
  const router = useRouter();
  const [code, setCode] = useState('');
  const [isVerifying, setIsVerifying] = useState(false);
  const [error, setError] = useState('');

  const verifyMFA = async (e: React.FormEvent) => {
    e.preventDefault();
    setIsVerifying(true);
    setError('');

    const { error } = await supabase.auth.mfa.verify({
      factorId: factors[0].id,
      challengeId: challenge.id,
      code,
    });

    if (error) {
      setError('Invalid code. Please try again.');
      setIsVerifying(false);
      return;
    }

    if (onSuccess) {
      onSuccess();
    } else {
      router.push(redirectTo);
    }
  };

  return (
    <Card className="w-full max-w-md">
      <CardHeader>
        <CardTitle>Two-Factor Authentication</CardTitle>
        <CardDescription>
          Enter the 6-digit code from your authenticator app
        </CardDescription>
      </CardHeader>
      <CardContent>
        <form onSubmit={verifyMFA} className="space-y-4">
          <div className="space-y-2">
            <Input
              type="text"
              inputMode="numeric"
              pattern="[0-9]*"
              maxLength={6}
              value={code}
              onChange={(e) => setCode(e.target.value.replace(/\D/g, ''))}
              placeholder="000000"
              className="text-center text-2xl tracking-widest"
              autoFocus
            />
            {error && (
              <p className="text-sm text-destructive">{error}</p>
            )}
          </div>
          
          <Button
            type="submit"
            disabled={code.length !== 6 || isVerifying}
            className="w-full"
          >
            {isVerifying ? 'Verifying...' : 'Verify'}
          </Button>
          
          <Button
            type="button"
            variant="link"
            className="w-full"
            onClick={() => setShowBackupCode(true)}
          >
            Use backup code instead
          </Button>
        </form>
      </CardContent>
    </Card>
  );
}

4. Backup Codes Management

Generate and display backup codes:

typescript
// components/mfa/backup-codes.tsx
export function BackupCodes() {
  const [codes, setCodes] = useState<string[]>([]);
  const [isGenerating, setIsGenerating] = useState(false);

  const generateBackupCodes = async () => {
    setIsGenerating(true);
    
    // Generate 10 backup codes
    const backupCodes = Array.from({ length: 10 }, () => 
      generateSecureCode(8)
    );
    
    // Store hashed versions in database
    await storeBackupCodes(backupCodes);
    
    setCodes(backupCodes);
    setIsGenerating(false);
  };

  const downloadCodes = () => {
    const content = codes.join('\n');
    const blob = new Blob([content], { type: 'text/plain' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'backup-codes.txt';
    a.click();
  };

  return (
    <Card>
      <CardHeader>
        <CardTitle>Backup Codes</CardTitle>
        <CardDescription>
          Use these codes if you lose access to your authenticator
        </CardDescription>
      </CardHeader>
      <CardContent>
        {codes.length === 0 ? (
          <Button onClick={generateBackupCodes} disabled={isGenerating}>
            {isGenerating ? 'Generating...' : 'Generate Backup Codes'}
          </Button>
        ) : (
          <div className="space-y-4">
            <Alert>
              <AlertCircle className="h-4 w-4" />
              <AlertTitle>Important</AlertTitle>
              <AlertDescription>
                Save these codes in a secure location. Each code can only be used once.
              </AlertDescription>
            </Alert>
            
            <div className="grid grid-cols-2 gap-2 p-4 bg-muted rounded-lg font-mono text-sm">
              {codes.map((code, index) => (
                <div key={index}>{code}</div>
              ))}
            </div>
            
            <div className="flex gap-2">
              <Button onClick={downloadCodes} variant="outline">
                Download Codes
              </Button>
              <Button onClick={() => setCodes([])} variant="ghost">
                Done
              </Button>
            </div>
          </div>
        )}
      </CardContent>
    </Card>
  );
}

5. MFA Settings Page

User settings for managing MFA:

typescript
// app/settings/security/page.tsx
export default function SecuritySettings() {
  const { user } = useUser();
  const { data: mfaFactors } = useMFAFactors();
  
  return (
    <div className="space-y-6">
      <div>
        <h3 className="text-lg font-medium">Security Settings</h3>
        <p className="text-sm text-muted-foreground">
          Manage your account security preferences
        </p>
      </div>
      
      <Card>
        <CardHeader>
          <CardTitle>Two-Factor Authentication</CardTitle>
          <CardDescription>
            Add an extra layer of security to your account
          </CardDescription>
        </CardHeader>
        <CardContent>
          {mfaFactors?.totp ? (
            <div className="space-y-4">
              <div className="flex items-center justify-between">
                <div className="flex items-center gap-2">
                  <Shield className="h-5 w-5 text-green-500" />
                  <span>2FA is enabled</span>
                </div>
                <Button variant="destructive" size="sm">
                  Disable 2FA
                </Button>
              </div>
              <BackupCodes />
            </div>
          ) : (
            <MFAEnrollment />
          )}
        </CardContent>
      </Card>
    </div>
  );
}

Authentication Flow Integration

Login with MFA

typescript
// Enhanced login flow
export async function signIn(email: string, password: string) {
  const { data, error } = await supabase.auth.signInWithPassword({
    email,
    password,
  });

  if (error) {
    return { error };
  }

  // Check if MFA is required
  const { data: factors } = await supabase.auth.mfa.listFactors();
  
  if (factors?.totp && factors.totp.length > 0) {
    // Redirect to MFA challenge
    return { requiresMFA: true };
  }

  return { success: true };
}

Enforce MFA for Sensitive Operations

typescript
// Require MFA for sensitive actions
export const deleteSensitiveDataAction = enhanceAction(
  async (data, user) => {
    // Check if user has verified MFA recently
    const mfaVerified = await checkRecentMFAVerification(user.id);
    
    if (!mfaVerified) {
      throw new Error('MFA verification required');
    }
    
    // Proceed with sensitive operation
    await performSensitiveOperation(data);
  },
  {
    auth: true,
    schema: DeleteSensitiveDataSchema,
  }
);

Organization-Level MFA

Enforce MFA for entire teams:

sql
-- Add MFA requirement to accounts
alter table public.accounts
add column mfa_required boolean default false;

-- Check in middleware
export async function enforceOrgMFA(accountId: string, userId: string) {
  const { data: account } = await supabase
    .from('accounts')
    .select('mfa_required')
    .eq('id', accountId)
    .single();
  
  if (account?.mfa_required) {
    const { data: factors } = await supabase.auth.mfa.listFactors();
    
    if (!factors?.totp || factors.totp.length === 0) {
      // Redirect to MFA setup
      redirect('/settings/security/mfa/setup');
    }
  }
}

Testing MFA

Unit Tests

typescript
describe('MFA Enrollment', () => {
  it('generates QR code for enrollment', async () => {
    const { result } = renderHook(() => useMFAEnrollment());
    
    await act(async () => {
      await result.current.startEnrollment();
    });
    
    expect(result.current.qrCode).toBeDefined();
    expect(result.current.secret).toHaveLength(32);
  });
  
  it('verifies TOTP code correctly', async () => {
    const code = generateTOTPCode(secret);
    const isValid = await verifyTOTPCode(secret, code);
    
    expect(isValid).toBe(true);
  });
});

E2E Tests

typescript
test('complete MFA setup flow', async ({ page }) => {
  // Navigate to security settings
  await page.goto('/settings/security');
  
  // Start MFA enrollment
  await page.click('text=Enable 2FA');
  
  // Wait for QR code
  await page.waitForSelector('[data-testid="mfa-qr-code"]');
  
  // Enter verification code
  await page.fill('[data-testid="mfa-code-input"]', '123456');
  await page.click('text=Verify and Enable');
  
  // Verify success
  await expect(page.locator('text=2FA is enabled')).toBeVisible();
});

Best Practices

  1. Grace Period: Give users time to set up MFA before enforcing
  2. Backup Codes: Always provide backup codes for account recovery
  3. Clear Instructions: Guide users through setup with clear steps
  4. Session Management: Consider MFA verification timeout
  5. Audit Logging: Log all MFA-related events

License Requirement

Important: This command requires a commercial MakerKit license from https://makerkit.dev?atp=MqaGgc MakerKit is a premium SaaS starter kit and requires proper licensing for commercial use.

Built with ❤️ for the AI Coding community, by Praney Behl