This skill provides guidance and enforcement rules for implementing secure two-factor authentication (2FA) using Better Auth's twoFactor plugin.
Setting Up Two-Factor Authentication
When adding 2FA to your application, configure the twoFactor plugin with your app name as the issuer. This name appears in authenticator apps when users scan the QR code.
import { betterAuth } from"better-auth";
import { twoFactor } from"better-auth/plugins";
exportconst auth = betterAuth({
appName: "My App", // Used as the default issuer for TOTPplugins: [
twoFactor({
issuer: "My App", // Optional: override the app name for 2FA specifically
}),
],
});
Note: After adding the plugin, run npx @better-auth/cli migrate to add the required database fields and tables.
Client-Side Setup
Add the client plugin and configure the redirect behavior for 2FA verification:
When a user enables 2FA, require their password for verification. The enable endpoint returns a TOTP URI for QR code generation and backup codes for account recovery.
constenable2FA = async (password: string) => {
const { data, error } = await authClient.twoFactor.enable({
password,
});
if (data) {
// data.totpURI - Use this to generate a QR code// data.backupCodes - Display these to the user for safekeeping
}
};
Important: The twoFactorEnabled flag on the user is not set to true until the user successfully verifies their first TOTP code. This ensures users have properly configured their authenticator app before 2FA is fully active.
Skipping Initial Verification
If you want to enable 2FA immediately without requiring verification, set skipVerificationOnEnable:
twoFactor({
skipVerificationOnEnable: true, // Not recommended for most use cases
});
Note: This is generally not recommended as it doesn't confirm the user has successfully set up their authenticator app.
TOTP (Authenticator App)
TOTP generates time-based codes using an authenticator app (Google Authenticator, Authy, etc.). Codes are valid for 30 seconds by default.
Displaying the QR Code
Use the TOTP URI to generate a QR code for users to scan:
Backup codes provide account recovery when users lose access to their authenticator app or phone. They are generated automatically when 2FA is enabled.
Displaying Backup Codes
Always show backup codes to users when they enable 2FA:
constBackupCodes = ({ codes }: { codes: string[] }) => {
return (
<div><p>Save these codes in a secure location:</p><ul>
{codes.map((code, i) => (
<likey={i}>{code}</li>
))}
</ul></div>
);
};
Regenerating Backup Codes
When users need new codes, regenerate them (this invalidates all previous codes):
twoFactor({
trustDeviceMaxAge: 30 * 24 * 60 * 60, // 30 days in seconds (default)
});
Note: The trust period refreshes on each successful sign-in within the trust window.
Security Considerations
Session Management
During the 2FA flow:
User signs in with credentials
Session cookie is removed (not yet authenticated)
A temporary two-factor cookie is set (default: 10-minute expiration)
User verifies via TOTP, OTP, or backup code
Session cookie is created upon successful verification
Configure the two-factor cookie expiration:
twoFactor({
twoFactorCookieMaxAge: 600, // 10 minutes in seconds (default)
});
Rate Limiting
Better Auth applies built-in rate limiting to all 2FA endpoints (3 requests per 10 seconds). For OTP verification, additional attempt limiting is applied:
twoFactor({
otpOptions: {
allowedAttempts: 5, // Max attempts per OTP code (default: 5)
},
});
Encryption at Rest
TOTP secrets are encrypted using symmetric encryption with your auth secret
Backup codes are stored encrypted by default
OTP codes can be configured for plain, encrypted, or hashed storage
Constant-Time Comparison
Better Auth uses constant-time comparison for OTP verification to prevent timing attacks.
Credential Account Requirement
Two-factor authentication can only be enabled for credential (email/password) accounts. For social accounts, it's assumed the provider already handles 2FA.
Disabling 2FA
Allow users to disable 2FA with password confirmation: