| OAuth2 | Yes | No | OAuth2 does not participate in local 2FA, but 2FA-enabled users may log in with OAuth2 |
@ -61,11 +63,23 @@ Client flow:
1. Complete the first-factor login request.
2. If MFA is required, read the remaining available methods.
3. If email is available, call the email-code send API immediately.
4. Complete MFA with the session ID and second-factor proof.
5. Store the final access and refresh tokens.
4. If `MFA_METHOD_TOTP` is available, call `VerifyMfaTotp`.
5. If `MFA_METHOD_RECOVERY_CODE` is available, call `VerifyMfaRecoveryCode`.
6. Complete MFA with the session ID and second-factor proof.
7. Store the final access and refresh tokens.
</Steps>
Failed MFA password attempts are recorded by brute-force protection and cannot bypass the normal password failure counters.
Failed password, TOTP, and recovery-code verification attempts enter their matching brute-force controls. TOTP and recovery codes share a per-user failure counter and include the client IP in enforcement.
## TOTP and Recovery Codes
The server generates a random 20-byte TOTP secret and returns it in an `otpauth://` URI. The client renders a QR code from that URI and can show the response secret for manual entry. Setup expires after 10 minutes. The user must submit a valid TOTP code before the server confirms the credential and returns recovery codes.
The server encrypts the TOTP secret with `security.totp_encryption_key` and binds the ciphertext context to the user ID. Verification accepts the current 30-second time step and its adjacent steps. The server records the latest accepted step, so a code cannot be reused across login and sensitive-operation verification. Production hosts should run NTP or chrony to keep clock drift within the verification window.
Setup returns 10 recovery codes. The server stores their hashes and discards the plaintext; the client should require the user to save the codes. Regeneration invalidates all previous codes, and successful verification removes the submitted code.
Starting setup, regenerating recovery codes, and deleting TOTP require sensitive-operation verification. Removing TOTP reduces the local-method count. The user must disable 2FA first if removal would leave fewer than two local methods. Each sensitive-operation verification ID works once.
## User Preferences and Security Settings
@ -74,7 +88,8 @@ User preferences store user-level settings. `two_factor_enabled` is security-sen
Constraints:
- Enabling 2FA requires at least two usable local methods.
- Removing a passkey while 2FA is enabled must still leave at least two usable local methods. Other authentication-method removal flows should follow the same product rule.
- TOTP and its recovery codes count as one local method.
- Removing a passkey, unbinding email, or deleting TOTP must leave at least two usable local methods while 2FA is enabled.
- Admin-initiated preference updates must respect role hierarchy. A non-root admin cannot modify root or equally privileged users.
@ -171,7 +171,7 @@ Provider instance bindings are not user preferences. They are stored on provider
2FA constraints:
- Enabling 2FA requires at least two local methods: password, WebAuthn/passkey, verified email.
- Enabling 2FA requires at least two local methods: password, WebAuthn/passkey, TOTP, or verified email. TOTP recovery codes belong to the TOTP factor and do not increase the method count.
- OAuth2 is not counted as a local 2FA factor, but 2FA-enabled users may log in with OAuth2.
- Removing an authentication method while 2FA is enabled must still leave at least two usable local methods.
4. If `required=true`, store the short-lived `mfa.session_id` and present `available_methods`.
5. If `MFA_METHOD_EMAIL` is available, call `RequestMfaEmailCode` when the user chooses email.
6. If `MFA_METHOD_WEBAUTHN` is available, call `StartMfaPasskey` for WebAuthn options, then `FinishMfaPasskey`.
7. After the second factor succeeds, the final response returns usable tokens.
7. If `MFA_METHOD_TOTP` is available, call `VerifyMfaTotp` with `mfa_session_id` and the six-digit code.
8. If `MFA_METHOD_RECOVERY_CODE` is available, call `VerifyMfaRecoveryCode`. Tell the user that a successful request consumes that code.
9. After the second factor succeeds, the final response returns usable tokens.
</Steps>
`GetUserPreferences` returns `auth_factors`, which lets clients show whether the current user has password, WebAuthn, or verified email factors. Enabling 2FA requires at least two local factors. Password is a first factor completed through OPAQUE or direct password login. OAuth2 does not count as a local 2FA factor.
`GetUserPreferences` returns `auth_factors`, which lets clients show whether the current user has password, WebAuthn, TOTP, or verified email factors. `totp_recovery_codes_remaining` reports the remaining recovery-code count. Enabling 2FA requires at least two local factors; TOTP and its recovery codes count as one. Password is a first factor completed through OPAQUE or direct password login. OAuth2 does not count as a local 2FA factor.
<Aside type="note">
OPAQUE is the public client local password login protocol. When a password first factor triggers MFA, clients should complete the challenge with an available non-password method such as email or WebAuthn.
OPAQUE is the public client local password login protocol. When a password first factor triggers MFA, clients should complete the challenge with an available non-password method such as email, WebAuthn, or TOTP.
</Aside>
## TOTP and Recovery Codes
TOTP uses SHA-1, six-digit codes, and a 30-second period. The server returns a complete `otpauth_uri`; render the QR code from this URI instead of rebuilding the issuer, account name, or algorithm parameters in the client.
Enrollment flow:
<Steps>
1. Call `StartSensitiveOperationVerification`, complete the methods in `available_methods`, and obtain the one-time `verification_id`.
2. Call `StartTotpSetup`. The response contains `setup_id`, `secret`, `otpauth_uri`, and `expires_at`.
3. Render a QR code from `otpauth_uri` and provide `secret` for manual entry.
4. After the user enters an authenticator code, call `FinishTotpSetup` before `expires_at`.
5. Show the 10 returned `recovery_codes` and require the user to save and confirm them. The server does not return that plaintext set again.
6. To enable 2FA, obtain a new `verification_id` and call `SetTwoFactorEnabled`. TOTP enrollment and 2FA enablement are separate sensitive operations.
Regenerating recovery codes invalidates the old set. Deleting TOTP also requires a new sensitive-operation verification. If removal would leave one local method, use one `verification_id` to disable 2FA and another `verification_id` to delete TOTP.
The server rejects an accepted 30-second time step after its first use. If the user uses a code for login, wait for the authenticator to generate the next code before starting a TOTP-protected sensitive operation.
## Email Codes
Email codes can be used for login, MFA, email verification, and password reset. Keep those flows separate in client state.
@ -44,7 +44,7 @@ PostgreSQL is durable business state. Redis is shared short-lived state and coor
## Major Components
- **API and realtime**: HTTP REST, public gRPC, WebSocket, and room realtime events share the same business services and permission model.
- **Authentication**: Password, OPAQUE, passkey/WebAuthn, email codes, OAuth2, user-level 2FA, and JWT tokens make up the login layer.
- **Authentication**: Password, OPAQUE, passkey/WebAuthn, TOTP and recovery codes, email codes, OAuth2, user-level 2FA, and JWT tokens make up the login layer.
- **Media**: Providers resolve external media, the proxy performs controlled forwarding, and slice cache stores Range slices only.
- **Horizontal scaling**: Multi-node deployments use Redis, discovery, leader election, and transactional outbox delivery boundaries.
@ -41,7 +41,8 @@ These limits apply to the current release.
| Topic | Current boundary |
| --- | --- |
| 2FA | Local 2FA uses password, passkey, verified email; OAuth2 is not a local factor |
| 2FA | Local factors include password, passkey, TOTP, and verified email; TOTP recovery codes do not count as a separate factor; OAuth2 bypasses local 2FA for the OAuth2 login path |
| TOTP | Uses fixed SHA-1, six-digit codes, and a 30-second period; clients render QR codes from the server-provided `otpauth_uri`; rotating `security.totp_encryption_key` requires ciphertext migration or user re-enrollment |
| JWT revocation | Strong revocation depends on Redis token blacklist and token lifetime |
| OPAQUE secret | Long-term binding to password records; not for routine rotation |
| Provider encryption | Depends on credential encryption key; losing it affects decryption |