Skip to content

feat: move MFA enrolment onto BetterAuth's twoFactor plugin (D1) - #87

Open
LucaGerlich wants to merge 2 commits into
developmentfrom
feat/mfa-betterauth
Open

feat: move MFA enrolment onto BetterAuth's twoFactor plugin (D1)#87
LucaGerlich wants to merge 2 commits into
developmentfrom
feat/mfa-betterauth

Conversation

@LucaGerlich

Copy link
Copy Markdown
Owner

Summary

Resolves TECHNICAL_DEBT.md decision D1 (MFA bypassable). The login gate already used BetterAuth's twoFactor plugin; enrolment wrote custom columns the gate never read. Enrolment now runs on the plugin end to end and the custom implementation is deleted (−461 lines, otplib removed).

  • Settings UI rewritten on authClient.twoFactor (enable → QR → verify → backup codes; disable; regenerate backup codes). LDAP/SSO accounts enrol without a password (allowPasswordless), local accounts confirm with theirs.
  • Migration 20260908_betterauth_two_factor: drops user.mfaEnabled/mfaSecret/mfaBackupCodes, adds the plugin's verified, failedVerificationCount, lockedUntil columns to twoFactor.
  • Audit log: enrolment, removal, regeneration and TOTP/backup-code logins via the BetterAuth after hook (lib/auth-two-factor-audit.ts); the credentials login audit waits for the second factor.
  • Compliance dashboard: real MFA coverage; all counts scoped to the organization (they were global across tenants).

Breaking: users who had MFA "enabled" must enrol again. The old flag was never enforced, so no protection is lost, but notify them before deploying.

Verification

  • tsc --noEmit 0 errors, ESLint clean, next build
  • 478 unit tests pass, including the new auth-two-factor-audit suite
  • Fresh Postgres: all 29 migrations apply

Manual test plan

  • Local user: Enable MFA → password → scan QR → code → backup codes shown once; audit log has "MFA enabled"
  • Sign out, sign in → redirected to /mfa-verify → TOTP accepted → audit log has LOGIN method totp
  • Sign in with a backup code; the code is single-use
  • "New backup codes" → old codes rejected
  • Disable MFA → next sign-in has no second step
  • LDAP/SSO user: Enable MFA without a password prompt
  • Compliance dashboard shows MFA coverage for the org only

🤖 Generated with Claude Code

https://claude.ai/code/session_01GcY31e5LXkP6Vtfqi5FvPt

The login gate already used the plugin, but enrolment wrote custom
user.mfaEnabled/mfaSecret/mfaBackupCodes columns the gate never read, so
enabling MFA had no effect. The settings UI now enrols through
authClient.twoFactor (enable, verifyTotp, disable, generateBackupCodes),
renders the TOTP URI as a QR code client-side and shows backup codes once.

- twoFactor plugin: allowPasswordless for LDAP/SSO accounts, encrypted
  backup codes; the twoFactor table gains the plugin's verified,
  failedVerificationCount and lockedUntil columns; the mfa* user columns
  are dropped (existing enrolments must re-enrol)
- Audit: enrolment, removal, backup-code regeneration and TOTP/backup-code
  logins are recorded from the BetterAuth after hook; the credentials
  login audit no longer fires on the password step of a 2FA login
- Compliance dashboard: MFA coverage is computed from twoFactorEnabled,
  and all counts are scoped to the admin's organization (they were global)
- Removed: /api/auth/mfa routes, lib/mfa.ts, otplib, encryptArray helpers
Copilot AI lite review requested due to automatic review settings September 7, 2026 22:33
@vercel

vercel Bot commented Sep 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
assett-tracker Ready Ready Preview Sep 7, 2026 10:44pm UTC

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new twoFactor.verified column defaults to true, which conflicts with its intended “enrolment confirmation” purpose and could undermine the verification step unless corrected.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR resolves the long-standing mismatch where MFA enrolment was implemented outside BetterAuth while login enforcement relied on BetterAuth’s twoFactor plugin. It rewires enrolment end-to-end through the plugin, removes the legacy MFA implementation (code + DB columns + dependency), adds audit logging for 2FA events via an auth hook, and fixes cross-tenant compliance dashboard counts by scoping them to the admin’s organization.

Changes:

  • Replaced the custom MFA enrolment/verify/disable flow with authClient.twoFactor.* in the user settings UI; removed legacy MFA routes and helpers (and otplib).
  • Added auth-two-factor-audit + an after hook in auth-server.ts to audit 2FA enable/disable/regen and 2FA-completed logins.
  • Scoped compliance dashboard counts (including MFA coverage) to the caller’s organization.
File summaries
File Description
TECHNICAL_DEBT.md Marks D1 as resolved and documents the BetterAuth-based MFA approach.
src/lib/mfa.ts Removes legacy MFA secret/URI/token/backup-code implementation.
src/lib/encryption.ts Removes array encryption helpers previously used for MFA backup codes.
src/lib/auth-two-factor-audit.ts Adds 2FA endpoint classification + audit-log writing helpers.
src/lib/auth-server.ts Integrates BetterAuth twoFactor options and adds 2FA after-hook auditing + session enrichment.
src/lib/auth-client.ts Removes legacy mfaEnabled user field from client typing.
src/lib/tests/mfa.test.ts Removes tests for deleted legacy MFA helpers.
src/lib/tests/encryption.test.ts Removes tests for deleted encrypt/decrypt array helpers.
src/lib/tests/auth-two-factor-audit.test.ts Adds unit tests for new 2FA auditing helpers.
src/app/user/[id]/settings/ui/MfaSettings.tsx Rewrites MFA settings UI to use BetterAuth twoFactor enable/verify/disable/generate codes.
src/app/user/[id]/settings/page.tsx Switches settings page to use twoFactorEnabled + authProvider.
src/app/api/user/route.ts Updates user sanitization to drop removed MFA columns.
src/app/api/auth/mfa/verify/route.ts Deletes legacy MFA verify endpoint.
src/app/api/auth/mfa/setup/route.ts Deletes legacy MFA setup endpoint.
src/app/api/auth/mfa/disable/route.ts Deletes legacy MFA disable endpoint.
src/app/api/admin/compliance/route.ts Scopes compliance counts to the admin’s organization and adds MFA enabled user count.
src/app/admin/compliance/ui/ComplianceDashboard.tsx Uses real MFA coverage to compute compliance status.
prisma/schema.prisma Drops legacy MFA user fields; adds BetterAuth-managed 2FA lockout/verification fields.
prisma/migrations/20260908_betterauth_two_factor/migration.sql Drops legacy MFA user columns and adds BetterAuth twoFactor columns.
package.json Bumps version to 0.10.0 and removes otplib.
docs/DEVELOPMENT_NOTES.md Updates encryption notes to reflect BetterAuth-managed 2FA secret/codes encryption.
docs/DEPLOYMENT.md Updates env-var descriptions and deployment notes for BetterAuth secret impact on MFA.
docs/DATABASE_MIGRATION_GUIDE.md Updates encryption/migration notes to include BetterAuth-managed 2FA encryption.
CHANGELOG.md Adds 0.10.0 release notes for BetterAuth MFA + compliance scoping.
bun.lock Removes otplib and related dependencies from lockfile.
Review details

Suppressed comments (1)

src/app/user/[id]/settings/ui/MfaSettings.tsx:161

  • After regenerating backup codes, the password remains in component state even though it’s no longer needed for the backup-code display step. Clearing it reduces exposure of sensitive data in memory.
    setBackupCodes(result.data.backupCodes);
    setStep("backup");
  • Files reviewed: 24/25 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +10 to +13
ALTER TABLE "public"."twoFactor"
ADD COLUMN IF NOT EXISTS "verified" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN IF NOT EXISTS "failedVerificationCount" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS "lockedUntil" TIMESTAMPTZ(6);
Comment thread prisma/schema.prisma
Comment on lines +1255 to +1258
// Plugin-managed: enrolment confirmation and verification lockout
verified Boolean @default(true)
failedVerificationCount Int @default(0)
lockedUntil DateTime? @db.Timestamptz(6)
Comment on lines +117 to +119
setTotpUri(result.data.totpURI);
setBackupCodes(result.data.backupCodes);
setStep("qr");
Preview builds executed the migration against whatever DATABASE_URL the
Preview environment holds. On the personal deployment that is the
production database, so the preview build of this branch applied the
column-dropping MFA migration to production before release. The build
command now migrates only when VERCEL_ENV is production; previews compile
against the existing schema.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants