feat: two-factor authentication (TOTP + backup codes)
Uses better-auth's built-in twoFactor plugin rather than hand-rolling TOTP — adds the two_factors table and users.twoFactorEnabled, wires the server/client plugins (allowPasswordless: true so OAuth-only accounts aren't locked out of managing 2FA), and adds a custom rate limit for the verify endpoints (5/min — far stricter than the default 100/10s, since a 6-digit code has a much smaller keyspace than a password). New Settings → Security section walks through enable (password confirm -> QR + backup codes -> confirm a live code before it's actually turned on, per the plugin's default flow) and disable. New /verify-2fa page handles the post-password mid-login step, with a backup-code fallback. Verified live end-to-end: enable, confirm, forced re-auth on next sign-in, TOTP accepted, backup code accepted (single-use), disable. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { createAuthClient } from "better-auth/react";
|
||||
import { genericOAuthClient } from "better-auth/client/plugins";
|
||||
import { genericOAuthClient, twoFactorClient } from "better-auth/client/plugins";
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
plugins: [genericOAuthClient()],
|
||||
plugins: [
|
||||
genericOAuthClient(),
|
||||
twoFactorClient({ twoFactorPage: "/verify-2fa" }),
|
||||
],
|
||||
});
|
||||
|
||||
export const {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { betterAuth } from "better-auth";
|
||||
import { genericOAuth } from "better-auth/plugins";
|
||||
import { genericOAuth, twoFactor } from "better-auth/plugins";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { db, users, sessions, accounts, verifications, eq, count } from "@epicure/db";
|
||||
import { db, users, sessions, accounts, verifications, twoFactors, eq, count } from "@epicure/db";
|
||||
import { sendEmail, verifyEmailHtml, resetPasswordHtml, welcomeHtml } from "@/lib/email";
|
||||
import { isSignupsDisabled } from "@/lib/site-settings";
|
||||
import { findValidInvite, consumeInvite, INVITE_COOKIE } from "@/lib/invites";
|
||||
@@ -17,11 +17,19 @@ export const auth = betterAuth({
|
||||
// rules) — everything else on /api/auth falls back to 100/10s.
|
||||
rateLimit: {
|
||||
enabled: true,
|
||||
// A 6-digit TOTP/backup code has far fewer combinations than a password —
|
||||
// the generic 100-req/10s default is too loose to meaningfully slow down
|
||||
// guessing it.
|
||||
customRules: {
|
||||
"/two-factor/verify-totp": { window: 60, max: 5 },
|
||||
"/two-factor/verify-otp": { window: 60, max: 5 },
|
||||
"/two-factor/verify-backup-code": { window: 60, max: 5 },
|
||||
},
|
||||
},
|
||||
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "pg",
|
||||
schema: { user: users, session: sessions, account: accounts, verification: verifications },
|
||||
schema: { user: users, session: sessions, account: accounts, verification: verifications, twoFactor: twoFactors },
|
||||
}),
|
||||
|
||||
emailAndPassword: {
|
||||
@@ -68,6 +76,10 @@ export const auth = betterAuth({
|
||||
},
|
||||
|
||||
plugins: [
|
||||
// allowPasswordless: OAuth-only accounts (no credential/password account)
|
||||
// would otherwise never be able to enable or manage 2FA, since the
|
||||
// endpoint requires a password to confirm identity by default.
|
||||
twoFactor({ issuer: "Epicure", allowPasswordless: true }),
|
||||
...(process.env["AUTHENTIK_CLIENT_ID"] && process.env["AUTHENTIK_BASE_URL"] ? [
|
||||
genericOAuth({
|
||||
config: [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Mirrors CHANGELOG.md at the repo root — update both together.
|
||||
export const APP_VERSION = "0.22.0";
|
||||
export const APP_VERSION = "0.23.0";
|
||||
|
||||
export type ChangelogEntry = {
|
||||
version: string;
|
||||
@@ -11,6 +11,13 @@ export type ChangelogEntry = {
|
||||
};
|
||||
|
||||
export const CHANGELOG: ChangelogEntry[] = [
|
||||
{
|
||||
version: "0.23.0",
|
||||
date: "2026-07-13 23:06",
|
||||
added: [
|
||||
"**Two-factor authentication**: turn it on in Settings → Security to require a code from an authenticator app when signing in, with backup codes in case you lose access to it.",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "0.22.0",
|
||||
date: "2026-07-13 22:49",
|
||||
|
||||
Reference in New Issue
Block a user