Email MFA adds a second factor — a 6-digit code emailed at login — between identity verification (password or OAuth) and full session access. Phase 1 is opt-in, with a post-login nudge forDocumentation Index
Fetch the complete documentation index at: https://docs.praxis-ai.com/llms.txt
Use this file to discover all available pages before exploring further.
admin and super accounts and a self-serve toggle in every user’s profile.
Configuration prerequisite. MFA only activates when the platform environment has
MFA_ENABLED=true AND a valid MFA_PEPPER (see Operational requirements below). The worker hard-fails boot if MFA_ENABLED=true and MFA_PEPPER is missing — that’s intentional, so MFA can’t run in a half-configured state.How it works
- 6-digit numeric code (matches every other auth code users see — banks, Google, etc.)
- 5-minute code TTL
- 7-day default trusted-device window (per-instance tunable via
MFA_TRUST_DAYS) - Trust matches require both a signed cookie and an IP-subnet (/24) check
- LTI, SDK, and API-key sign-ins are exempt — they have their own identity proofs
Managing MFA per user
From Admin → Users, click a user’s row to open the editor, then switch to the Credentials tab. The Two-step verification block exposes:- Require email MFA for this user — toggle that enables / disables MFA on the target account
- Enabled
<date>caption when MFA is on - Revoke trusted devices — invalidates every browser the user has previously verified. The next login from any of those browsers will re-prompt for a code
When to disable a user’s MFA
- The user is locked out (lost email access, new email address pending, mailbox bounced)
- A security incident requires temporarily reducing friction so the user can recover their account
- A standard user opted in voluntarily and changed their mind
When to revoke trusted devices
- The user reported a device lost or stolen
- A shared / public computer was used during a recent session
- After a suspected password compromise (combined with a password reset)
Operational requirements
The following environment variables are read by the worker at boot:| Variable | Default | Required | Notes |
|---|---|---|---|
MFA_ENABLED | false | optional | Set to "true" to activate the entire MFA surface. When false, the gate short-circuits and behaves as if MFA never existed. |
MFA_PEPPER | (none) | required when MFA_ENABLED=true | At least 32 characters. Mixed into the SHA-256 hash of every 6-digit code stored in the database. Generate with node -e "console.log(require('crypto').randomBytes(32).toString('hex'))". |
MFA_TRUST_DAYS | 7 | optional | Bounded to [1, 30]. Sets the trusted-device cookie lifetime. Customers with tighter compliance can shorten it. |
MFA_PEPPER invalidates all pending challenges (any 5-minute window with codes already issued). Acceptable trade-off because rotation should be rare. To rotate cleanly: deploy new pepper, wait 5 minutes for outstanding challenges to drain, then verify no mfa_challenge rows remain in the pending state.
Audit log
Every MFA event is recorded in a dedicatedmfa_audit_log collection. The taxonomy:
| Event | Trigger |
|---|---|
mfa.enable | User self-enabled MFA |
mfa.disable | User self-disabled MFA |
mfa.admin_override | Admin enabled or disabled MFA on another user |
mfa.code.issued | A new 6-digit challenge was created |
mfa.code.resent | User clicked Resend; old challenge invalidated, new one issued |
mfa.code.verified | Code accepted, sign-in completed |
mfa.code.failed | Wrong code submitted (per-code attempts incremented) |
mfa.challenge.cancelled | User clicked Cancel on the verification screen |
mfa.challenge.expired | Verify or Resend hit an already-expired challenge |
mfa.lockout | 5 burned challenges in 1 hour → 10-minute account lock |
mfa.trusted_device.added | New device cookie issued after successful verification |
mfa.trusted_device.revoked | User or admin revoked one or all trusted devices |
mfa.prompt.dismissed | User clicked Remind me in 7 days on the opt-in screen |
GET /api/admin/users/{userId}/mfa-audit — see the Administrator API. The response is paginated by ?limit= and ?before= (ISO timestamp).
Brute-force protection
Two layers, both enforced server-side:- Per-code: 5 attempts. The 6th wrong submission invalidates the challenge — the user must click Resend for a fresh code. Resends are cooldown-limited to once per 30 seconds.
- Per-account: 5 burned codes in 1 hour. “Burned” means invalidated and had at least one wrong attempt (so a user spam-clicking Resend without submitting wrong codes doesn’t trip the lock). After 5 burns the account is locked from MFA verification for 10 minutes —
/api/auth/signinreturns HTTP 423 withretryAfter: 600. Admin intervention via the Credentials tab can disable MFA for the user if needed sooner.
Exempt paths
The MFA gate is bypassed for sign-ins that already have a strong alternate proof of identity:- LTI launches — the LMS (Canvas / Moodle / Brightspace) has already authenticated the user
- SDK launches — the embedding application’s launch token proves identity
- API-key sign-ins — the API key itself is a strong credential, intended for server-to-server use
Frequently asked questions
Can I require MFA for every user in my instance?
Can I require MFA for every user in my instance?
Not in this phase. MFA is opt-in per user. The post-login nudge encourages admin and super accounts to enable it (with a 7-day “Remind me later” cooldown), but enforcement is the user’s choice. Mandatory MFA is on the roadmap.
What happens to a user's existing session when I toggle their MFA off?
What happens to a user's existing session when I toggle their MFA off?
Their current JWT stays valid until natural expiry (default 6 hours). Their trusted-device cookies are revoked. So the user keeps working through their current session, then re-signs in normally without MFA. If you need to force an immediate logout, terminate their session from the Sessions panel separately.
Does MFA work on mobile?
Does MFA work on mobile?
Yes. The verify form uses the
autocomplete="one-time-code" attribute, so iOS Safari surfaces the 6-digit code from the Mail app directly in the keyboard suggestion bar — one tap to fill all six boxes. Android Chrome and Firefox have equivalent suggestions when SMS Retriever-style attributes are honored.Backup codes?
Backup codes?
Not in this phase. Recovery is admin-assisted (an admin or super admin can disable MFA on the locked-out account). Backup codes are tracked for a follow-up release.
What about TOTP / Authenticator apps?
What about TOTP / Authenticator apps?
Not in this phase either. The current implementation is email-only, which keeps the recovery surface simple (admin override) and avoids managing a TOTP secret store. TOTP and WebAuthn / passkeys are on the longer-term roadmap.