Tenant two-factor authentication
Tenant TOTP enrollment belongs to an organization's member and applies across that organization's applications. It is separate from consumer two-factor settings, even for a consumer-linked tenant member. A consumer factor does not substitute for the tenant factor, and changing a tenant factor does not reset the consumer one.
When a factor is required
| Authentication path | Local tenant factor |
|---|---|
| Hosted tenant password sign-in | Required when main org allowance and member enrollment are enabled |
| Browser-direct password sign-in | Same, including consumer-linked password authentication in tenant mode |
| Password-based tenant Device Code approval | Uses tenant password policy; no separate public tenant Device Code preference |
| Browser-direct email code / magic link | Main allowance, email allowance, enabled factor and saved email_logins must all be true |
| Fresh brokered/federated authentication | Never requires local tenant 2FA. The broker owns that responsibility. |
| Existing authenticated session, refresh or remembered-account switch | No new local tenant-factor prompt; normal session/member/app admission still applies |
The exemption follows verified brokered authentication, not a caller's skip flag, and does not claim the broker performed MFA. Password setup/change/removal and forgot-password recovery do not erase factors or saved email preferences. A new password sign-in afterward still follows the current matrix. This tenant matrix does not import consumer Device Code or account-switch interval settings.
Availability and retained enrollment
The org-only tenant_policy_allow_two_factor and
tenant_policy_allow_email_two_factor default off without consumer inheritance
or app overrides. Main off prevents local enrollment, use and member factor
changes while retaining enrolled authenticators, backup codes and preferences.
Turning it back on restores applicable use. Email allowance off preserves the
saved email preference and makes its effective use unavailable. Managers can
still reset retained factors. There is no require-enrollment mode.
Policy is rechecked during pending operations and before session creation. An unavailable policy store is an error, not permission to treat factors as off. See org policy and configuration APIs.
Browser-direct endpoint reference
All are SSO POSTs below the browser-direct base, JSON with fresh endpoint-bound DPoP. Management calls require selected session handles, current activated-token/member admission and no extra OAuth scope. Setup, begin and confirm require an actual primary sign-in within five minutes; status does not. Proof is bound to org/member, app, origin, key, carrier, selected session, purpose, exact factor and policy revision. A different selected session cannot finish the challenge. Use a fresh same-carrier sign-in when required.
| Relative path | Exact additional input | Success |
|---|---|---|
/two-factor/status |
None | Status projection below |
/two-factor/setup |
Optional name, default Authenticator |
One-time totp_secret and challenge |
/two-factor/begin |
purpose: policy, recovery or unlink; policy only for policy purpose |
required: true, challenge, recovery_codes_remaining |
/two-factor/confirm |
purpose: setup, policy, recovery or unlink; copied challenge, code, optional boolean recovery (default false); exact policy again for policy purpose |
verified: true; setup/recovery adds ten new recovery_codes; unlink adds removed: true |
/two-factor/login |
No session handles: continuation, code, optional boolean recovery (default false) |
New one-use authorization code, state, expires_in: 120, two_factor_disabled |
name is trimmed, nonempty, at most 191 bytes and contains no controls.
code is a current six-digit TOTP string, or an unused backup code when
recovery: true (input at most 64 bytes). Setup requires TOTP, not recovery.
policy must be exactly {"email_logins":true} or
{"email_logins":false}; other tenant policy fields are not public settings.
Policy cannot be submitted for other purposes. A confirmation must repeat the
same desired boolean used at begin.
Status has available, email_available, configured_enabled, email_enabled,
enrolled, enabled, authenticator (null or {id,name,type,verified_at}),
policy: {email_logins}, and recovery_codes_remaining. configured_enabled
and saved policy describe retained configuration; enabled and email_enabled
describe effective enforcement under current org allowances. Never infer that
an unavailable factor or saved preference has been deleted. Ordinary status
contains no seed, challenge binding, backup plaintext or private proof.
{
"available":true,"email_available":true,"configured_enabled":true,
"enrolled":true,"enabled":true,"email_enabled":false,
"authenticator":{"id":"ffffffff-ffff-4fff-8fff-ffffffffffff","name":"Authenticator","type":"totp","verified_at":"2026-01-01 00:00:00"},
"policy":{"email_logins":false},"recovery_codes_remaining":10
}
Enrollment, QR and backup codes
Call setup and retain its returned challenge object (id, binding,
authenticator_id, expires, a UTC YYYY-MM-DD HH:MM:SS string). Show the
Base32 totp_secret for manual entry, or generate an otpauth://totp/ QR locally
using SHA1, six digits, 30-second period, a clear account label and matching issuer
prefix/parameter. Never send the seed to an external QR service. Scanning is
not enrollment: confirm with purpose: setup and a current code.
{
"bd_carrier":"<carrier>","bd_session":"<selected session>","bd_key":"<carrier secret>",
"purpose":"setup",
"challenge":{"id":"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa","binding":"<one-use binding>","authenticator_id":"ffffffff-ffff-4fff-8fff-ffffffffffff","expires":"2026-01-01 00:10:00"},
"code":"123456"
}
Generate dates and codes at runtime; the example is illustrative. Successful tenant enrollment immediately enables its applicable password checks and shows ten backup codes once. Save them privately, then clear setup/QR/backup material from the UI. There is one linked factor per member. New setup replaces abandoned pending enrollment, not a linked factor. Setup and its challenge expire after ten minutes. There is no remote cancel endpoint; cancel clears local material, and pending setup expires or is replaced by a subsequent setup.
Replace backup codes with begin/confirm purpose recovery, proved by TOTP or
an unused backup code. The returned ten-code set invalidates the old set. A lost
response cannot redisplay plaintext: inspect status, then authorize another
replacement with a valid remaining proof. Remove a factor with purpose unlink
and valid proof; replacement is removal followed by new setup/confirmation, not
a dedicated atomic replace operation. Removal clears factor policy, backup codes
and outstanding challenges. Lost all proofs requires authorized
manager reset.
Login continuation, limits and retries
Browser-direct token pickup can return outcome: two_factor_required and a
five-minute continuation. No usable new token/session exists until proof and
final admission succeed. /two-factor/login must use the original browser key
and bound context. On success redeem its new code with the original PKCE verifier;
see token pickup.
Hosted and Device Code screens preserve their own original authorization/grant
context and do not let a pending factor authorize a session or device pickup.
Ordinary login/management challenges expire after five minutes. There are five failed attempts per challenge, ten failed attempts and twenty created challenges per member in fifteen minutes. Restarting the dialog does not reset that budget. TOTP uses adjacent time steps with a persistent replay check: a code just used for enrollment or another operation cannot immediately be reused. Wait for the next code. Proof consumption and local changes are transactional; concurrent proofs do not both win. Factor/policy changes invalidate stale authority.
Backup codes are single-use. Consuming the last backup code during login
retires the factor, clears its settings/codes/challenges, permits that login,
and returns two_factor_disabled: true. Offer fresh enrollment. This forced
last-code behavior is not a general rule for every management operation and
does not add the consumer UI's optional disable checkbox to the tenant API.
two_factor_invalid (400) covers rejected/expired/replayed proof;
two_factor_rate_limited (429) requires waiting fifteen minutes.
two_factor_not_allowed / email_two_factor_not_allowed (403) require current
policy/status reload. authenticator_already_linked, authenticator_unavailable,
tenant_member_unavailable or two_factor_required (409) require resolving
changed state. reauthentication_required (403) needs primary sign-in again.
Malformed fields are invalid_request. Operational failure never means “factor
off.” After lost management confirmation inspect status before starting another
operation; a consumed login continuation requires new sign-in.
Operator prerequisites
Tenant factor, challenge, backup-code and factor-policy storage is automatically ensured using the service's existing database conventions, along with org policy storage. The runtime needs permission to ensure missing tables, or a separately authorized installer must create them first. Auto-ensure does not promise to rewrite an incompatible existing schema. No destructive reset is required to enable this feature.
Enrollment/TOTP use requires the existing external encryption keyring configured
by M7_TWO_FACTOR_KEYRING_FILE and readable only by the appropriate service
identities. Preserve old key versions while enrollments reference them. API/SSO
must share compatible configuration; never expose key values, seeds or backup
codes in diagnostics. Unenrolled paths and authorized administrative clearing
do not require decrypting a seed. Reuse the deployed email service for optional
email sign-in. Configuration checks, local protocol tests and real-user/live
acceptance are separate evidence.