[SECURITY AUDIT] SSO login bypasses TOTP two-factor for accounts that have 2FA enabled #31

Closed
opened 2026-07-04 22:00:49 +00:00 by wtclaude · 0 comments
Member

Severity: Medium
Confidence: Medium (may be an intentional trust-model decision — flagging for an explicit call)
Scope area: Admin login hardening / TOTP flow (audit area 1), auth transition overlap (area 2)

What

The local password login flow enforces TOTP: login in server/src/router/v1/auth/auth.controller.js:63-67 checks needsTotp(user) and refuses to issue a session until the second factor is verified. The SSO login flow does not perform this check.

In server/src/router/v1/auth/sso.controller.js, finishLogin (lines 141-157) resolves the linked identity and immediately mints a full session:

const { token: sessionToken } = sessionService.createSession(user, authMethod)
token.setAuthCookie(req, res, sessionToken)

There is no if (user.totp_enabled) gate. An admin who has deliberately enabled TOTP, and who also has a Google/Discord/OIDC identity linked, can sign in through the SSO button and receive an identical fully-privileged session without presenting their authenticator code.

Why it matters

Enabling 2FA is an explicit statement that a single factor is insufficient to access the account. SSO silently downgrades that: the effective strength of the account becomes the strength of the external IdP login (which may itself be single-factor). The user never consented to that downgrade and has no visibility into it. It also creates an asymmetry — the local path is hardened, an alternative path to the same session is not.

Where

  • server/src/router/v1/auth/sso.controller.js:141-157 (finishLogin, no TOTP gate)
  • Contrast with server/src/router/v1/auth/auth.controller.js:63-67 (local path gates on needsTotp)

Suggested fix (not implemented)

Decide the intended policy explicitly, then either:

  • Treat a completed SSO authentication as satisfying the second factor (document this — the IdP is the second factor), or
  • After a successful finishLogin for a user with totp_enabled, issue the staged TOTP challenge (sessionService.createPartialSession) instead of a full session and route the browser through the existing /login/totp step before setting the auth cookie.

Given the app already invested in a staged TOTP challenge, wiring SSO through the same gate is the consistent choice.

**Severity:** Medium **Confidence:** Medium (may be an intentional trust-model decision — flagging for an explicit call) **Scope area:** Admin login hardening / TOTP flow (audit area 1), auth transition overlap (area 2) ### What The local password login flow enforces TOTP: `login` in `server/src/router/v1/auth/auth.controller.js:63-67` checks `needsTotp(user)` and refuses to issue a session until the second factor is verified. The SSO login flow does **not** perform this check. In `server/src/router/v1/auth/sso.controller.js`, `finishLogin` (lines 141-157) resolves the linked identity and immediately mints a full session: ```js const { token: sessionToken } = sessionService.createSession(user, authMethod) token.setAuthCookie(req, res, sessionToken) ``` There is no `if (user.totp_enabled)` gate. An admin who has deliberately enabled TOTP, and who also has a Google/Discord/OIDC identity linked, can sign in through the SSO button and receive an identical fully-privileged session **without presenting their authenticator code**. ### Why it matters Enabling 2FA is an explicit statement that a single factor is insufficient to access the account. SSO silently downgrades that: the effective strength of the account becomes the strength of the external IdP login (which may itself be single-factor). The user never consented to that downgrade and has no visibility into it. It also creates an asymmetry — the local path is hardened, an alternative path to the same session is not. ### Where - `server/src/router/v1/auth/sso.controller.js:141-157` (`finishLogin`, no TOTP gate) - Contrast with `server/src/router/v1/auth/auth.controller.js:63-67` (local path gates on `needsTotp`) ### Suggested fix (not implemented) Decide the intended policy explicitly, then either: - Treat a completed SSO authentication as satisfying the second factor (document this — the IdP is the second factor), **or** - After a successful `finishLogin` for a user with `totp_enabled`, issue the staged TOTP challenge (`sessionService.createPartialSession`) instead of a full session and route the browser through the existing `/login/totp` step before setting the auth cookie. Given the app already invested in a staged TOTP challenge, wiring SSO through the same gate is the consistent choice.
wtclaude added the
severity:medium
label 2026-07-04 22:00:49 +00:00
Sign in to join this conversation.
No description provided.