Obexal Docs

Docs/Authentication/Multi-factor authentication

Multi-factor authentication (MFA)

TOTP, email codes, recovery codes, step-up verification and MFA mandates per organization or group: no primary factor bypasses MFA.

Obexal treats MFA as a server-side invariant, not a UI feature: once an account has an active second factor, no primary factor can open a session without it.

Second factors at a glance

FactorTypeValidity
TOTPAuthenticator app (RFC 6238, 6 digits, 30 s period)Current code, 1 period of clock skew tolerated
Email codeNumeric one-time code sent to the verified address10 minutes
Recovery codes10 single-use backup codesUntil used or regenerated

There is no SMS factor, by design: SMS delivery would route authentication data through non-EU aggregators and remains exposed to SIM swapping, so Obexal excludes it as a sovereign choice.

The MFA chokepoint

Every primary factor terminates in the same server-side decision point: password, passwordless email, social login, SAML and LDAP. If the account has an active MFA factor, no session is opened; the API returns a challenge instead:

{ "mfaRequired": true, "mfaToken": "kJ2m...", "methods": ["totp"] }

The client completes it with the second factor:

curl -sS -X POST https://accounts.obexal.com/v1/auth/mfa \
  -H "X-CSRF-Token: $CSRF" -H 'Content-Type: application/json' \
  -d '{"mfaToken":"kJ2m...","code":"123456"}'
# 200 -> {"user":{...}} and the session cookie is set

The challenge is single use and stored hashed, expires after 5 minutes (10 minutes for email codes, so the email has time to arrive), allows at most 5 attempts, and verification is rate limited per IP. TOTP takes priority: a user who has both factors is challenged with TOTP. The one deliberate exception to the chokepoint is the passkey, itself a strong, phishing-resistant primary factor.

Note

Examples use accounts.obexal.com; replace it if your organization signs in on a custom domain.

Enroll and manage TOTP

Enrollment requires a session and follows a pending to active transition:

# 1. Enroll: creates a PENDING factor, returns the secret to scan (only shown here).
curl -sS -b cookies.txt -X POST https://accounts.obexal.com/v1/mfa/totp/enroll \
  -H "X-CSRF-Token: $CSRF"
# 200 -> {"secret":"JBSWY3DPEHPK3PXP","otpauthUri":"otpauth://totp/Obexal:a@b.eu?..."}

# 2. Activate: a valid current code flips the factor to ACTIVE.
curl -sS -b cookies.txt -X POST https://accounts.obexal.com/v1/mfa/totp/activate \
  -H "X-CSRF-Token: $CSRF" -H 'Content-Type: application/json' -d '{"code":"123456"}'
# 200 -> {"recoveryCodes":["...", "..."]}  (10 codes, shown once)

The TOTP secret is encrypted at rest. Disabling the factor (POST /v1/mfa/totp/disable) requires a valid current TOTP code: a hijacked session cannot silently remove the second factor. GET /v1/mfa lists the account's factors and their status.

Email codes as a second factor

The email code factor can be enrolled explicitly: POST /v1/mfa/email/enroll sends a code to the account's verified address and returns an mfaToken; POST /v1/mfa/email/activate with {"mfaToken","code"} activates the factor; POST /v1/mfa/email/disable removes it.

It is also applied by default as a platform policy: a password sign-in by a user who has no TOTP receives an emailed code challenge, so the password alone never opens a session. If the email is slow to arrive, request a new code:

curl -sS -X POST https://accounts.obexal.com/v1/auth/mfa/resend \
  -H "X-CSRF-Token: $CSRF" -H 'Content-Type: application/json' \
  -d '{"mfaToken":"kJ2m..."}'
# 200 -> {"mfaToken":"<new token>","codeLength":6}

Resending is rate limited per IP and each resend invalidates the previous challenge token.

Recovery codes

Activating TOTP generates 10 single-use recovery codes, returned in clear exactly once and stored hashed. Each one can complete an MFA challenge in place of a TOTP code ({"mfaToken":"...","recoveryCode":"..."}), for example after losing the phone.

POST /v1/mfa/recovery-codes/regenerate (session required, active TOTP required) replaces the whole batch: the previous codes become invalid immediately.

Step-up verification for sensitive actions

A valid session is not always enough. Sensitive endpoints require a fresh second-factor verification:

curl -sS -b cookies.txt -X POST https://accounts.obexal.com/v1/mfa/step-up \
  -H "X-CSRF-Token: $CSRF" -H 'Content-Type: application/json' -d '{"code":"123456"}'
# 204 -> the session is marked MFA-fresh for 5 minutes

Without a fresh step-up, sensitive endpoints (for example updating an AI agent's identity or policy caps, or rotating an agent secret) respond 403 step_up_required. A recovery code is accepted in place of a TOTP code. Anti-lockout rule: a user with no MFA factor is not gated, since they could never pass the step-up.

Require MFA for an organization or a group

Administrators can make MFA mandatory:

  • Per organization: in the console, organization settings, enable the MFA requirement (API: the requireMfa field of PATCH /v1/admin/tenant).
  • Per group: set requireMfa when creating or updating a group; every member is then under the mandate.
curl -sS -X POST https://accounts.obexal.com/v1/admin/groups \
  -H "Authorization: Bearer $OBEXAL_API_TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"Finance","requireMfa":true}'

At sign-in, a user under mandate who has no factor yet gets a session flagged mfaEnrollmentRequired: true, and the hosted sign-in UI forces enrollment before anything else. Self-hosted deployments can additionally set MFA_ENROLLMENT_STRICT=true: the server then refuses authenticated requests (403 mfa_enrollment_required) until a factor is enrolled, allowing only the enrollment endpoints, me, logout and csrf. See Configuration.

Conditional access rules can also require MFA dynamically, based on network, schedule or country.