Problem
The TOTP two-factor authentication check is decoupled from the NextAuth authorize callback. A determined attacker could skip the /two-factor page entirely and call signIn('credentials', ...) directly, bypassing TOTP verification.
Current Flow (Vulnerable)
- Client calls
checkTwoFactorStatus(email, password) — validates credentials, returns twoFactorRequired: boolean
- If 2FA required, client redirects to
/two-factor?email=...
- User enters TOTP code →
verifyTwoFactorLogin() validates it
- On success, calls
signIn('credentials', ...) to complete auth
The vulnerability: Step 4 calls signIn('credentials') which goes through the authorize callback in src/lib/auth.ts:32-57. This callback does NOT check twoFactorEnabled — it just validates email + password and returns the user.
Proposed Fix
Option A: Move TOTP into authorize callback
- Accept a
totpCode parameter in the credentials provider
- In
authorize(), after password validation, check if twoFactorEnabled is true
- If enabled and no
totpCode provided, throw an error
- If enabled and
totpCode provided, validate it via otpauth
- The client sends email + password + TOTP code all in one
signIn() call
Option B: Session-level 2FA state
- Add a
twoFactorVerified boolean to the JWT
- Set it to
false in authorize() when twoFactorEnabled is true
- Middleware checks
twoFactorVerified — if false, redirects to /two-factor
- After TOTP verification, update the JWT to set
twoFactorVerified: true
Option A is simpler and more secure (no intermediate session state). Recommended.
Acceptance Criteria
Files to Modify
apps/web/src/lib/auth.ts — authorize callback
apps/web/src/components/login-form.tsx — pass TOTP in signIn call
apps/web/src/app/two-factor/page.tsx — update flow
apps/web/src/components/settings/two-factor-setup.tsx — verify still works
Problem
The TOTP two-factor authentication check is decoupled from the NextAuth
authorizecallback. A determined attacker could skip the/two-factorpage entirely and callsignIn('credentials', ...)directly, bypassing TOTP verification.Current Flow (Vulnerable)
checkTwoFactorStatus(email, password)— validates credentials, returnstwoFactorRequired: boolean/two-factor?email=...verifyTwoFactorLogin()validates itsignIn('credentials', ...)to complete authThe vulnerability: Step 4 calls
signIn('credentials')which goes through theauthorizecallback insrc/lib/auth.ts:32-57. This callback does NOT checktwoFactorEnabled— it just validates email + password and returns the user.Proposed Fix
Option A: Move TOTP into
authorizecallbacktotpCodeparameter in the credentials providerauthorize(), after password validation, check iftwoFactorEnabledis truetotpCodeprovided, throw an errortotpCodeprovided, validate it viaotpauthsignIn()callOption B: Session-level 2FA state
twoFactorVerifiedboolean to the JWTfalseinauthorize()whentwoFactorEnabledis truetwoFactorVerified— if false, redirects to/two-factortwoFactorVerified: trueOption A is simpler and more secure (no intermediate session state). Recommended.
Acceptance Criteria
signIn('credentials', { email, password })call without TOTP fails when 2FA is enabled/two-factorpage still provides the UX for entering the codeFiles to Modify
apps/web/src/lib/auth.ts— authorize callbackapps/web/src/components/login-form.tsx— pass TOTP in signIn callapps/web/src/app/two-factor/page.tsx— update flowapps/web/src/components/settings/two-factor-setup.tsx— verify still works