Add an extra layer of security with Time-based One-Time Passwords (TOTP).
BetterAuth supports TOTP-based 2FA compatible with:
# config/packages/better_auth.yaml
better_auth:
two_factor:
enabled: true
issuer: 'MyApp' # Name shown in authenticator apps
backup_codes_count: 10 # Number of recovery codes
POST /auth/2fa/setup
Initialize 2FA for the authenticated user.
curl -X POST http://localhost:8000/auth/2fa/setup \
-H "Authorization: Bearer {access_token}"
Response:
{
"secret": "JBSWY3DPEHPK3PXP",
"qrCode": "data:image/png;base64,...",
"manualEntryKey": "JBSWY3DPEHPK3PXP",
"backupCodes": [
"12345678",
"87654321",
"..."
]
}
POST /auth/2fa/validate
Confirm 2FA setup with a code from the authenticator app.
curl -X POST http://localhost:8000/auth/2fa/validate \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{"code": "123456"}'
Response:
{
"message": "2FA successfully enabled",
"enabled": true
}
POST /auth/login/2fa
Complete login when 2FA is required.
curl -X POST http://localhost:8000/auth/login/2fa \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "password123",
"code": "123456"
}'
Response:
{
"access_token": "v4.local.eyJ...",
"refresh_token": "rt_abc123...",
"user": { ... }
}
GET /auth/2fa/status
Check if 2FA is enabled for the user.
curl -X GET http://localhost:8000/auth/2fa/status \
-H "Authorization: Bearer {access_token}"
Response:
{
"enabled": true,
"backupCodesRemaining": 8
}
POST /auth/2fa/disable
Disable 2FA (requires current TOTP code).
curl -X POST http://localhost:8000/auth/2fa/disable \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{"code": "123456"}'
Response:
{
"message": "2FA disabled",
"enabled": false
}
POST /auth/2fa/backup-codes/regenerate
Generate new backup codes (invalidates old ones).
curl -X POST http://localhost:8000/auth/2fa/backup-codes/regenerate \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{"code": "123456"}'
Response:
{
"message": "Backup codes regenerated",
"backupCodes": [
"11111111",
"22222222",
"..."
]
}
┌──────────┐ ┌──────────┐
│ User │ │ Server │
└────┬─────┘ └────┬─────┘
│ │
│ POST /auth/2fa/setup │
├──────────────────────────────────────────────►
│ │
│ {secret, qrCode, backupCodes} │
◄──────────────────────────────────────────────┤
│ │
│ [User scans QR code] │
│ │
│ POST /auth/2fa/validate │
│ {code: "123456"} │
├──────────────────────────────────────────────►
│ │
│ {enabled: true} │
◄──────────────────────────────────────────────┤
┌──────────┐ ┌──────────┐
│ User │ │ Server │
└────┬─────┘ └────┬─────┘
│ │
│ POST /auth/login │
│ {email, password} │
├──────────────────────────────────────────────►
│ │
│ {requires2fa: true} │
◄──────────────────────────────────────────────┤
│ │
│ [User enters code from app] │
│ │
│ POST /auth/login/2fa │
│ {email, password, code} │
├──────────────────────────────────────────────►
│ │
│ {access_token, refresh_token, user} │
◄──────────────────────────────────────────────┤
// components/TwoFactorSetup.tsx
import { useState } from 'react';
import { twoFactorApi } from '../lib/api';
export function TwoFactorSetup() {
const [setup, setSetup] = useState<{
qrCode: string;
backupCodes: string[];
} | null>(null);
const [code, setCode] = useState('');
const handleSetup = async () => {
const data = await twoFactorApi.setup();
setSetup(data);
};
const handleValidate = async () => {
await twoFactorApi.validate(code);
alert('2FA enabled successfully!');
};
if (!setup) {
return (
<button onClick={handleSetup}>
Enable Two-Factor Authentication
</button>
);
}
return (
<div>
<h3>Scan this QR code with your authenticator app:</h3>
<img src={setup.qrCode} alt="2FA QR Code" />
<h3>Or enter this key manually:</h3>
<code>{setup.manualEntryKey}</code>
<h3>Save these backup codes:</h3>
<ul>
{setup.backupCodes.map((code, i) => (
<li key={i}><code>{code}</code></li>
))}
</ul>
<h3>Enter code from app to confirm:</h3>
<input
type="text"
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="123456"
maxLength={6}
/>
<button onClick={handleValidate}>Verify & Enable</button>
</div>
);
}
// components/Login.tsx
import { useState } from 'react';
import { authApi } from '../lib/api';
export function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [code, setCode] = useState('');
const [requires2fa, setRequires2fa] = useState(false);
const handleLogin = async () => {
try {
const response = await authApi.login(email, password);
if (response.requires2fa) {
setRequires2fa(true);
return;
}
// Login successful
handleSuccess(response);
} catch (error) {
console.error('Login failed:', error);
}
};
const handleLogin2fa = async () => {
try {
const response = await authApi.login2fa(email, password, code);
handleSuccess(response);
} catch (error) {
console.error('2FA verification failed:', error);
}
};
if (requires2fa) {
return (
<div>
<h3>Enter 2FA Code</h3>
<input
type="text"
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="Enter code from authenticator"
maxLength={6}
/>
<button onClick={handleLogin2fa}>Verify</button>
</div>
);
}
return (
<form onSubmit={(e) => { e.preventDefault(); handleLogin(); }}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<button type="submit">Login</button>
</form>
);
}
Backup codes allow login when the authenticator app is unavailable.
Backup codes are:
| Option | Default | Description |
|---|---|---|
enabled |
true | Enable/disable 2FA globally |
issuer |
BetterAuth | Name in authenticator apps |
backup_codes_count |
10 | Number of backup codes |
better_auth:
two_factor:
enabled: false
2FA routes will return an error when disabled.
How can I help you explore Laravel packages today?