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
- Grace Period: Give users time to set up MFA before enforcing
- Backup Codes: Always provide backup codes for account recovery
- Clear Instructions: Guide users through setup with clear steps
- Session Management: Consider MFA verification timeout
- 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.
