Module: utils/mfaEnforcement

Per-workspace MFA enforcement helpers (SEC-004).

Workspaces with mfaRequired = 1 (migration 028) block login for any member whose user account has mfaEnabled = 0 once they are past the grace window. The grace window starts at the later of:

  • the workspace's mfaPolicyUpdatedAt (when the policy flipped on)
  • the membership's joinedAt (so a new hire still gets a fair window even if the policy has been in place for months) and lasts mfaGracePeriodDays calendar days.

A user who belongs to multiple workspaces is enforced by the strictest one: if ANY workspace they belong to is past-grace and requires MFA, login is blocked.

Exports

  • evaluateMfaEnforcement — Decide enforce / grace / allow for a user.
Source:

Methods

(static) evaluateMfaEnforcement(user, optsopt) → {MfaEnforcementDecision}

Evaluate whether MFA enforcement blocks, grace-warns, or allows a login for a user. Pure function — no side effects, safe to call from any auth surface (/login, /refresh, OAuth callbacks).

Users are "MFA-enabled" if EITHER mfaEnabled === 1 (TOTP) OR they have at least one registered WebAuthn credential. Either factor satisfies the workspace requirement — otherwise an OAuth-only user with a passkey but no TOTP would be falsely blocked at OAuth callback time.

Parameters:
Name Type Attributes Description
user Object

User row (must include id, mfaEnabled, createdAt).

opts Object <optional>
Properties
Name Type Attributes Default Description
skipWebauthnCheck boolean <optional>
false

— If true, do NOT count the user's registered passkeys as a satisfying factor. Used by the self-lockout guard in DELETE /webauthn/credentials/:id, which needs to ask "would this user be blocked if the passkey they're trying to remove were their only factor?". The caller has already verified TOTP is off and that this is their last passkey, so the live webauthnRepo.countByUser read (which still sees the passkey since it hasn't been deleted yet) would falsely return "allow" and let the user lock themselves out.

Self-lockout guard contract

Callers that use this to decide whether to allow a factor-removal action (e.g. /mfa/disable, DELETE /webauthn/credentials/:id) MUST refuse the action for BOTH "block" and "grace" outcomes, not just "block". Permitting removal during grace defers the lockout by N days but does not prevent it — the user signs out and is blocked at next login. Only the "allow" state (no workspace requires MFA) permits factor removal.

Source:
Returns:
Type
MfaEnforcementDecision

Type Definitions

MfaEnforcementDecision

Type:
  • Object
Properties:
Name Type Attributes Description
state "allow" | "grace" | "block"

Final decision.

workspaceId string <optional>

Workspace that produced the decision (block / grace only).

workspaceName string <optional>

Display name for the same workspace.

gracePeriodDaysRemaining number <optional>

Ceil-rounded days left when state="grace".

graceEndsAt string <optional>

ISO timestamp when the user becomes blocked.

Source: