MFA in BitzOrcas is not a login-page checkbox. The Identity login flow decides whether a password success requires another challenge. MFA connectors implement TOTP, FIDO2, email/SMS OTP, and trusted devices. Final access and refresh tokens are issued only after the challenge completes.
Login state machine
LoginFlow creates a challenge when two-factor is enabled or risk requires it. The token purpose is MFA/Challenge, and its lifetime is exactly five minutes. Challenge state never includes final access or refresh tokens.
TOTP setup and completion
An authenticated user obtains setup material from /api/mfa/setup. The factor should become active only after the first code is confirmed. Anonymous /api/mfa/verify completes login using the short-lived MFA token, not an existing access token.
POST /api/mfa/setup HTTP/1.1Authorization: Bearer <access-token>Content-Type: application/json
{"accountName":"alice@example.com"}POST /api/mfa/verify HTTP/1.1Content-Type: application/json
{"mfaToken":"<five-minute-challenge>","code":"123456"}TOTP depends on synchronized clocks. Keep drift tolerance narrow and alert on NTP failures. Show setup secrets and recovery codes once; never place them in logs, telemetry, screenshots, or support bundles.
Factors and endpoints
| Capability | Endpoint family | Existing login |
|---|---|---|
| TOTP setup/disable | /api/mfa/setup, /disable | required |
| login MFA verification | /api/mfa/verify | challenge token |
| FIDO2 registration | /api/mfa/fido2/register/* | required |
| FIDO2 assertion | /api/mfa/fido2/assert/* | challenge context |
| email OTP | /api/mfa/email-otp/* | required |
| SMS OTP | /api/mfa/sms-otp/* | required |
| trusted device | /api/mfa/trusted-device/* | required |
Endpoints inject only IMediator; operations pass through commands, handlers, and platform ports. An OpenAPI route does not prove that a production SMS/email provider, FIDO2 RP, persistence profile, or distributed throttle is configured.
FIDO2 and WebAuthn
Registration and assertion each use begin/complete pairs. Bind the challenge to tenant, user, RP, and a short session. Completion verifies client data, origin, RP ID, signature, and counter.
registration: authenticated user → begin → authenticator creates → complete persistsassertion: login challenge → begin → authenticator signs → complete verifiesThe implementation includes credential persistence and sign-count clone-detection tests. Update RP ID and allowed Origin together when changing production domains.
Email and SMS OTP
Email/SMS factors are better suited to transition or recovery because channel compromise and SIM swapping reduce assurance. Throttle by user, destination, tenant, and IP, and return uniform responses that do not enumerate accounts.
// The endpoint delegates; ownership and send limits belong in the application flow.var result = await mediator.Send(command, httpContext.RequestAborted);
// Result mapping must not expose provider exceptions or the generated code.return result.ToHttp(httpContext);Codes need a short lifetime, limited attempts, and one-time consumption. Define whether resend invalidates the previous code. Provider failure needs observable retry/failure semantics.
Recovery codes
Recovery codes are one-use backup factors, not alternate passwords. Persist only hashes, consume atomically, and invalidate the old set when generating a new one. Audit and notify on use.
The login challenge internally asks IMfaService.GenerateRecoveryCodes(1) for challenge material and then uses the user token provider to create a five-minute token. That implementation detail does not mean a recovery code is shown during ordinary login; the public contract remains MfaToken + Code.
Trusted devices
A low-risk login may use a trusted device to reduce prompts. A fingerprint is not a secret and cannot prove identity alone. Bind enrollment, expiry, and revocation to tenant and user; ignore trust after account recovery, password change, high risk, or privileged actions.
Display device name, type, recent use, and a revoke action without revealing the full fingerprint.
Risk-driven policy
RiskDrivenMfaPolicyService currently requires TOTP for low risk and permits trusted-device skip; medium requires TOTP without skip; high and critical require TOTP plus FIDO2; Block denies.
If the risk engine is missing, returns failure, or throws, the current implementation falls back to TOTP only and disables trusted-device skip. This preserves availability while still requiring a factor; it is not a full authentication fail-open.
Two layers must be distinguished: the login-flow layer (LoginFlow) is fail-closed on risk-engine failure, returning Identity.Login.RiskAssessmentUnavailable and blocking login (see Identity login security); this section describes the MFA factor-selection layer (RiskDrivenMfaPolicyService), which decides factor degradation after a risk verdict is known. The two are not contradictory — one decides whether login may continue, the other decides which factors apply when it does.
Tenant-mandated MFA policy
Beyond user-opted two-factor, the platform has a tenant-level mandatory MFA policy described by MfaPolicyAggregate (table SysMfaPolicy, unique per tenant). It raises enforcement from an individual choice to an organizational policy.
Enforcement mode MfaEnforcementMode has three levels:
| Mode | Meaning |
|---|---|
Optional | Not mandated; user personal choice applies |
SelectedSubjects | Mandated for specified roles or specified users |
AllUsers | Mandated for every user in the tenant |
Assurance level MfaAssuranceLevel distinguishes Standard (TOTP/OTP) from PhishingResistant (FIDO2 and other anti-phishing factors). A policy can require a higher assurance level, narrowing acceptable factors to the anti-phishing set.
Policy source MfaPolicySource records why a requirement applies: PersonalChoice, TenantRole, TenantUser, TenantAllUsers, or HostBaseline. The UI can use this to explain to a user why MFA was required for a given login.
IMfaPolicyResolver merges the tenant policy with the user’s Authorization role assignments to compute the effective policy. Three endpoints expose policy management:
GET /api/identity/mfa-policyreads the tenant policy;GET /api/identity/mfa-policy/effectivereads the current user’s effective (merged) policy;PUT /api/identity/mfa-policyupdates the tenant policy.
Updating a policy requires optimistic concurrency (ExpectedVersion), a mandatory ChangeReason, and validation of role and user targets (SelectedSubjects must name at least one target, otherwise PolicyTargetsRequired).
The login-flow connection was noted above: when the effective policy requires MFA but the user has not enrolled any factor, login returns the MfaEnrollmentRequired restricted branch, guiding the user to enroll first rather than allowing an unenrolled login.
Disable and recover
/api/mfa/disable requires authorization. A commercial system should additionally require password re-entry or a strong current factor so a stolen access token cannot disable protection. Administrative disablement needs a ticket, audit evidence, and user notification.
Account recovery should revoke trusted devices, rotate recovery codes, and invalidate existing sessions. Support staff must not bypass this by editing a database flag.
Client behavior
Render the next step from a typed login result, not parsed error text. Return to password login when the challenge expires, limit retries, and discard the challenge immediately after success.
// Keep the MFA token in the transient flow, not localStorage.const result = await verifyMfa({ mfaToken: flow.token, code });if (result.ok) { flow.clearChallenge(); session.accept(result.accessToken, result.refreshToken);}Test matrix
- password success with required MFA emits no final token;
- challenge issue failure, expiry, invalid code, and replay all deny;
- TOTP setup, confirmation, disable, and one-use recovery semantics;
- FIDO2 origin/RP, challenge, sign count, and deletion;
- OTP throttling, provider failure, and anti-enumeration;
- trusted-device expiry, revoke, and high-risk bypass denial;
- every risk level maps to the expected factor set;
- credentials and challenges cannot cross tenants.
Production evidence
GA requires real-provider end-to-end evidence, clock alerts, production FIDO2 domains, OTP throttling, recovery rehearsal, and audit queries. Handler unit tests or endpoints appearing in OpenAPI are insufficient.