From 1a31f72dd83984e82778d7de9d55579fb789c365 Mon Sep 17 00:00:00 2001 From: Oleg Date: Thu, 20 Aug 2026 08:53:51 +0000 Subject: [PATCH] Extend TOTP 2FA to LDAP accounts; disable now requires a code, not a password MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2FA was previously local-accounts-only, gated on the assumption that LDAP already has its own MFA story — that's not actually guaranteed (depends on what's behind the directory), so app-level TOTP is now available for LDAP accounts too, as a second factor independent of whatever the directory does or doesn't enforce. The login route's LDAP branch now checks user.totpEnabled the same way the local branch already did, routing through the same login-challenge flow before minting a session. This forced a change to disabling 2FA: it used to require the current password, but an LDAP-provisioned user has passwordHash: null — there's no password to check. Disable now requires a live TOTP code or a recovery code instead (same "prove you still hold the factor" idea, just checking the right thing), which works identically for local and LDAP accounts. Verified: re-ran the full local-account Playwright flow with the new code-based disable (still passes end-to-end). For the LDAP path — no real LDAP server available to log in through — flipped a throwaway test account to authSource="ldap"/passwordHash=null directly in the DB and exercised the setup/confirm/disable logic functions directly (same technique used earlier in this session for testing mail ingestion without a live IMAP server): setup no longer rejects it, confirm and disable both work correctly with no password present. Test account fully deleted after. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01GteWhnWKTmnXcsd5jx6H7u --- src/app/(admin)/settings/account/page.tsx | 6 +--- .../settings/account/two-factor-settings.tsx | 13 ++++--- src/app/api/auth/login/route.ts | 11 ++++-- src/app/api/auth/totp/disable/route.ts | 35 ++++++++++++++----- src/app/api/auth/totp/setup/route.ts | 6 ---- 5 files changed, 42 insertions(+), 29 deletions(-) diff --git a/src/app/(admin)/settings/account/page.tsx b/src/app/(admin)/settings/account/page.tsx index ef3ae98..068c58e 100644 --- a/src/app/(admin)/settings/account/page.tsx +++ b/src/app/(admin)/settings/account/page.tsx @@ -10,14 +10,10 @@ export default async function AccountSettingsPage() {

Аккаунт

{session?.user.email}

- {session?.user.authSource === "local" ? ( + {session && (
- ) : ( -

- Двухфакторная аутентификация недоступна для LDAP-аккаунтов — вход защищён на уровне домена. -

)} ); diff --git a/src/app/(admin)/settings/account/two-factor-settings.tsx b/src/app/(admin)/settings/account/two-factor-settings.tsx index bd38e31..f090ba1 100644 --- a/src/app/(admin)/settings/account/two-factor-settings.tsx +++ b/src/app/(admin)/settings/account/two-factor-settings.tsx @@ -13,7 +13,7 @@ export function TwoFactorSettings({ initialEnabled }: { initialEnabled: boolean const [code, setCode] = useState(""); const [recoveryCodes, setRecoveryCodes] = useState(null); const [copied, setCopied] = useState(false); - const [password, setPassword] = useState(""); + const [disableCode, setDisableCode] = useState(""); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); @@ -22,7 +22,7 @@ export function TwoFactorSettings({ initialEnabled }: { initialEnabled: boolean setQrDataUrl(null); setSecret(null); setCode(""); - setPassword(""); + setDisableCode(""); setError(null); } @@ -70,7 +70,7 @@ export function TwoFactorSettings({ initialEnabled }: { initialEnabled: boolean const res = await fetch("/api/auth/totp/disable", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ password }), + body: JSON.stringify({ code: disableCode }), }); setLoading(false); if (!res.ok) { @@ -164,12 +164,11 @@ export function TwoFactorSettings({ initialEnabled }: { initialEnabled: boolean

Отключить 2FA