redesign fully scaffolded and web login works
This commit is contained in:
331
.agents/skills/two-factor-authentication-best-practices/SKILL.md
Normal file
331
.agents/skills/two-factor-authentication-best-practices/SKILL.md
Normal file
@@ -0,0 +1,331 @@
|
||||
---
|
||||
name: two-factor-authentication-best-practices
|
||||
description: Configure TOTP authenticator apps, send OTP codes via email/SMS, manage backup codes, handle trusted devices, and implement 2FA sign-in flows using Better Auth's twoFactor plugin. Use when users need MFA, multi-factor authentication, authenticator setup, or login security with Better Auth.
|
||||
---
|
||||
|
||||
## Setup
|
||||
|
||||
1. Add `twoFactor()` plugin to server config with `issuer`
|
||||
2. Add `twoFactorClient()` plugin to client config
|
||||
3. Run `npx @better-auth/cli migrate`
|
||||
4. Verify: check that `twoFactorSecret` column exists on user table
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { twoFactor } from "better-auth/plugins";
|
||||
|
||||
export const auth = betterAuth({
|
||||
appName: "My App",
|
||||
plugins: [
|
||||
twoFactor({
|
||||
issuer: "My App",
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Client-Side Setup
|
||||
|
||||
```ts
|
||||
import { createAuthClient } from "better-auth/client";
|
||||
import { twoFactorClient } from "better-auth/client/plugins";
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
plugins: [
|
||||
twoFactorClient({
|
||||
onTwoFactorRedirect() {
|
||||
window.location.href = "/2fa";
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Enabling 2FA for Users
|
||||
|
||||
Requires password verification. Returns TOTP URI (for QR code) and backup codes.
|
||||
|
||||
```ts
|
||||
const enable2FA = async (password: string) => {
|
||||
const { data, error } = await authClient.twoFactor.enable({
|
||||
password,
|
||||
});
|
||||
|
||||
if (data) {
|
||||
// data.totpURI — generate a QR code from this
|
||||
// data.backupCodes — display to user
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
`twoFactorEnabled` is not set to `true` until first TOTP verification succeeds. Override with `skipVerificationOnEnable: true` (not recommended).
|
||||
|
||||
## TOTP (Authenticator App)
|
||||
|
||||
### Displaying the QR Code
|
||||
|
||||
```tsx
|
||||
import QRCode from "react-qr-code";
|
||||
|
||||
const TotpSetup = ({ totpURI }: { totpURI: string }) => {
|
||||
return <QRCode value={totpURI} />;
|
||||
};
|
||||
```
|
||||
|
||||
### Verifying TOTP Codes
|
||||
|
||||
Accepts codes from one period before/after current time:
|
||||
|
||||
```ts
|
||||
const verifyTotp = async (code: string) => {
|
||||
const { data, error } = await authClient.twoFactor.verifyTotp({
|
||||
code,
|
||||
trustDevice: true,
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### TOTP Configuration Options
|
||||
|
||||
```ts
|
||||
twoFactor({
|
||||
totpOptions: {
|
||||
digits: 6, // 6 or 8 digits (default: 6)
|
||||
period: 30, // Code validity period in seconds (default: 30)
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## OTP (Email/SMS)
|
||||
|
||||
### Configuring OTP Delivery
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { twoFactor } from "better-auth/plugins";
|
||||
import { sendEmail } from "./email";
|
||||
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
twoFactor({
|
||||
otpOptions: {
|
||||
sendOTP: async ({ user, otp }, ctx) => {
|
||||
await sendEmail({
|
||||
to: user.email,
|
||||
subject: "Your verification code",
|
||||
text: `Your code is: ${otp}`,
|
||||
});
|
||||
},
|
||||
period: 5, // Code validity in minutes (default: 3)
|
||||
digits: 6, // Number of digits (default: 6)
|
||||
allowedAttempts: 5, // Max verification attempts (default: 5)
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Sending and Verifying OTP
|
||||
|
||||
Send: `authClient.twoFactor.sendOtp()`. Verify: `authClient.twoFactor.verifyOtp({ code, trustDevice: true })`.
|
||||
|
||||
### OTP Storage Security
|
||||
|
||||
Configure how OTP codes are stored in the database:
|
||||
|
||||
```ts
|
||||
twoFactor({
|
||||
otpOptions: {
|
||||
storeOTP: "encrypted", // Options: "plain", "encrypted", "hashed"
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
For custom encryption:
|
||||
|
||||
```ts
|
||||
twoFactor({
|
||||
otpOptions: {
|
||||
storeOTP: {
|
||||
encrypt: async (token) => myEncrypt(token),
|
||||
decrypt: async (token) => myDecrypt(token),
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Backup Codes
|
||||
|
||||
Generated automatically when 2FA is enabled. Each code is single-use.
|
||||
|
||||
### Displaying Backup Codes
|
||||
|
||||
```tsx
|
||||
const BackupCodes = ({ codes }: { codes: string[] }) => {
|
||||
return (
|
||||
<div>
|
||||
<p>Save these codes in a secure location:</p>
|
||||
<ul>
|
||||
{codes.map((code, i) => (
|
||||
<li key={i}>{code}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Regenerating Backup Codes
|
||||
|
||||
Invalidates all previous codes:
|
||||
|
||||
```ts
|
||||
const regenerateBackupCodes = async (password: string) => {
|
||||
const { data, error } = await authClient.twoFactor.generateBackupCodes({
|
||||
password,
|
||||
});
|
||||
// data.backupCodes contains the new codes
|
||||
};
|
||||
```
|
||||
|
||||
### Using Backup Codes for Recovery
|
||||
|
||||
```ts
|
||||
const verifyBackupCode = async (code: string) => {
|
||||
const { data, error } = await authClient.twoFactor.verifyBackupCode({
|
||||
code,
|
||||
trustDevice: true,
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### Backup Code Configuration
|
||||
|
||||
```ts
|
||||
twoFactor({
|
||||
backupCodeOptions: {
|
||||
amount: 10, // Number of codes to generate (default: 10)
|
||||
length: 10, // Length of each code (default: 10)
|
||||
storeBackupCodes: "encrypted", // Options: "plain", "encrypted"
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Handling 2FA During Sign-In
|
||||
|
||||
Response includes `twoFactorRedirect: true` when 2FA is required:
|
||||
|
||||
### Sign-In Flow
|
||||
|
||||
1. Call `signIn.email({ email, password })`
|
||||
2. Check `context.data.twoFactorRedirect` in `onSuccess`
|
||||
3. If `true`, redirect to `/2fa` verification page
|
||||
4. Verify via TOTP, OTP, or backup code
|
||||
5. Session cookie is created on successful verification
|
||||
|
||||
```ts
|
||||
const signIn = async (email: string, password: string) => {
|
||||
const { data, error } = await authClient.signIn.email(
|
||||
{ email, password },
|
||||
{
|
||||
onSuccess(context) {
|
||||
if (context.data.twoFactorRedirect) {
|
||||
window.location.href = "/2fa";
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
Server-side: check `"twoFactorRedirect" in response` when using `auth.api.signInEmail`.
|
||||
|
||||
## Trusted Devices
|
||||
|
||||
Pass `trustDevice: true` when verifying. Default trust duration: 30 days (`trustDeviceMaxAge`). Refreshes on each sign-in.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Session Management
|
||||
|
||||
Flow: credentials → session removed → temporary 2FA cookie (10 min default) → verify → session created.
|
||||
|
||||
```ts
|
||||
twoFactor({
|
||||
twoFactorCookieMaxAge: 600, // 10 minutes in seconds (default)
|
||||
});
|
||||
```
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
Built-in: 3 requests per 10 seconds for all 2FA endpoints. OTP has additional attempt limiting:
|
||||
|
||||
```ts
|
||||
twoFactor({
|
||||
otpOptions: {
|
||||
allowedAttempts: 5, // Max attempts per OTP code (default: 5)
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Encryption at Rest
|
||||
|
||||
TOTP secrets: encrypted with auth secret. Backup codes: encrypted by default. OTP: configurable (`"plain"`, `"encrypted"`, `"hashed"`). Uses constant-time comparison for verification.
|
||||
|
||||
2FA can only be enabled for credential (email/password) accounts.
|
||||
|
||||
## Disabling 2FA
|
||||
|
||||
Requires password confirmation. Revokes trusted device records:
|
||||
|
||||
```ts
|
||||
const disable2FA = async (password: string) => {
|
||||
const { data, error } = await authClient.twoFactor.disable({
|
||||
password,
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
## Complete Configuration Example
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { twoFactor } from "better-auth/plugins";
|
||||
import { sendEmail } from "./email";
|
||||
|
||||
export const auth = betterAuth({
|
||||
appName: "My App",
|
||||
plugins: [
|
||||
twoFactor({
|
||||
// TOTP settings
|
||||
issuer: "My App",
|
||||
totpOptions: {
|
||||
digits: 6,
|
||||
period: 30,
|
||||
},
|
||||
// OTP settings
|
||||
otpOptions: {
|
||||
sendOTP: async ({ user, otp }) => {
|
||||
await sendEmail({
|
||||
to: user.email,
|
||||
subject: "Your verification code",
|
||||
text: `Your code is: ${otp}`,
|
||||
});
|
||||
},
|
||||
period: 5,
|
||||
allowedAttempts: 5,
|
||||
storeOTP: "encrypted",
|
||||
},
|
||||
// Backup code settings
|
||||
backupCodeOptions: {
|
||||
amount: 10,
|
||||
length: 10,
|
||||
storeBackupCodes: "encrypted",
|
||||
},
|
||||
// Session settings
|
||||
twoFactorCookieMaxAge: 600, // 10 minutes
|
||||
trustDeviceMaxAge: 30 * 24 * 60 * 60, // 30 days
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
Reference in New Issue
Block a user