Skip to content

Multi-Factor Authentication (MFA)

Arbitex supports TOTP-based multi-factor authentication (MFA) as a second factor for user login. When MFA is enabled on a user account, every login requires a time-based one-time password in addition to the primary credentials. This guide covers the user setup flow, the login challenge flow, backup code management, account recovery, and admin operations.


Arbitex MFA is implemented using the TOTP standard (RFC 6238): 6-digit codes that rotate every 30 seconds, generated by any RFC 6238-compliant authenticator app. The server tolerates a clock skew of ±1 time step (up to 90 seconds) to accommodate minor drift between the authenticator device and the server.

Supported authenticator apps:

  • Google Authenticator (Android, iOS)
  • Authy
  • 1Password
  • Microsoft Authenticator
  • Bitwarden Authenticator
  • Any RFC 6238-compliant TOTP app

MFA is configured per user account. Administrators can force-disable MFA for any user to unlock locked-out accounts. Org-level MFA enforcement (requiring all users to enroll) is managed via policy configuration — see Policy Engine.


MFA setup is a two-step process: initiate setup (returns the secret and QR code), then confirm by submitting the first TOTP code. MFA is not active until the confirm step succeeds.

Call POST /api/auth/mfa/setup with a valid user access token. No request body is required.

Terminal window
curl -X POST https://api.arbitex.ai/api/auth/mfa/setup \
-H "Authorization: Bearer <access_token>"

Response:

{
"secret": "JBSWY3DPEHPK3PXP",
"provisioning_uri": "otpauth://totp/Arbitex:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Arbitex",
"qr_code": "<base64-encoded PNG>",
"backup_codes": [
"A1B2C3D4",
"E5F6G7H8",
"...8 more codes..."
]
}
FieldDescription
secretBase32-encoded TOTP secret. Use this to manually add the account to an authenticator app if QR scanning is not available.
provisioning_uriotpauth://totp/... URI. Can be passed directly to authenticator apps that accept URI input.
qr_codeBase64-encoded PNG image. Decode and display to the user for scanning.
backup_codes10 one-time recovery codes, each 8 characters (uppercase alphanumeric). Shown only at setup — store them securely.

The secret is stored on the user record, but MFA is not yet active. If a second call to POST /api/auth/mfa/setup is made after MFA is already enabled, the server returns 409 Conflict.

Open the authenticator app, scan the QR code (or enter the secret manually), and submit the first generated code to confirm enrollment.

Terminal window
curl -X POST https://api.arbitex.ai/api/auth/mfa/verify-setup \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"code": "123456"}'

Response:

{
"detail": "MFA has been enabled successfully"
}

MFA is now active on the account. All subsequent logins require a TOTP code.

Error responses:

StatusCondition
400Setup was not initiated (no pending secret on the user record)
400TOTP code is invalid or expired
409MFA is already enabled on this account

The 10 backup codes returned in the setup response are displayed only once. Instruct users to store them in a secure location (password manager, printed copy kept offline). Each code is single-use and 8 characters long. Backup codes cannot be retrieved after setup — if lost, an admin must force-disable MFA so the user can re-enroll.


Terminal window
curl https://api.arbitex.ai/api/auth/mfa/status \
-H "Authorization: Bearer <access_token>"

Response:

{
"mfa_enabled": true,
"backup_codes_remaining": 8
}

backup_codes_remaining reflects how many unused backup codes remain. Advise users to re-enroll (disable and re-enable MFA) when this count approaches zero.


When MFA is enabled on a user account, login is a two-step process.

sequenceDiagram
participant User
participant Client as Client Application
participant Arbitex as Arbitex Platform<br/>(api.arbitex.ai)
User->>Client: Enter email + password
Client->>Arbitex: POST /api/auth/login { email, password }
Note over Arbitex: Credentials valid<br/>mfa_enabled = true
Arbitex-->>Client: 200 { mfa_token, token_type: "mfa_challenge" }
Client->>User: Prompt for TOTP code
User->>Client: Enter 6-digit code from authenticator app
Client->>Arbitex: POST /api/auth/mfa/verify { mfa_token, code }
Note over Arbitex: Validate mfa_token + TOTP code
Arbitex-->>Client: 200 { access_token, refresh_token, token_type: "bearer", user }
Client->>User: Authenticated

Step 1 — Password login

The client submits the user’s email and password to the standard login endpoint. If MFA is enabled, the server does not issue a full access token. Instead it returns a short-lived MFA challenge token:

{
"mfa_token": "<short-lived challenge token>",
"token_type": "mfa_challenge"
}

This token has type mfa_challenge and a short expiry. It cannot be used to authenticate API requests.

Step 2 — TOTP verification

The client prompts the user for their TOTP code, then submits both the challenge token and the code:

Terminal window
curl -X POST https://api.arbitex.ai/api/auth/mfa/verify \
-H "Content-Type: application/json" \
-d '{
"mfa_token": "<mfa_challenge token>",
"code": "123456"
}'

The code field accepts either a 6-digit TOTP code from the authenticator app, or a backup code.

Response on success:

{
"access_token": "<jwt>",
"refresh_token": "<jwt>",
"token_type": "bearer",
"user": { "..." }
}

The issued access token carries a mfa_verified: true claim. Endpoints or policies that require MFA verification check for this claim.


Backup codes are generated during MFA setup and provide a recovery path when the authenticator device is unavailable.

PropertyValue
Count per enrollment10
Length8 characters
Character setUppercase alphanumeric (A–Z, 0–9)
UsageSingle-use — each code is consumed on first successful use
StorageHashed with bcrypt server-side; plaintext never stored after setup
DisplayShown once at setup only

Using a backup code:

A backup code can be submitted anywhere a TOTP code is accepted — in the code field of POST /api/auth/mfa/verify (during login) or POST /api/auth/mfa/disable. The server tries TOTP validation first; if the code is not 6 digits, it falls back to backup code validation.

Regenerating backup codes:

There is no endpoint to regenerate backup codes independently. To get a fresh set of 10 codes, the user must disable MFA and re-enroll via POST /api/auth/mfa/setup. Advise users to do this before their remaining count reaches zero.


A user can self-service disable MFA by providing a valid TOTP code or backup code. This does not require admin intervention.

Terminal window
curl -X POST https://api.arbitex.ai/api/auth/mfa/disable \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"code": "123456"}'

Response:

{
"detail": "MFA has been disabled successfully"
}

The server tries the submitted code as a TOTP code first (6-digit path), then as a backup code. Either is accepted.

Error responses:

StatusCondition
400MFA is not currently enabled on this account
400The submitted code is invalid (wrong TOTP code, expired, or backup code already used)

After disabling, the user can re-enroll at any time by calling POST /api/auth/mfa/setup again.


If a user cannot authenticate because they have lost access to their authenticator app and have no remaining backup codes, an administrator must reset MFA for them using the admin force-disable endpoint.

  1. Verify the user’s identity through an out-of-band channel before taking action.
  2. Locate the user’s ID (from the admin user list or audit logs).
  3. Call the admin force-disable endpoint:
Terminal window
curl -X DELETE https://api.arbitex.ai/api/admin/users/{user_id}/mfa \
-H "Authorization: Bearer <admin_access_token>"

Response:

{
"detail": "MFA has been disabled for the user"
}

This clears mfa_enabled, totp_secret, and all stored backup codes from the user record. The action does not require the user’s TOTP code.

  1. Notify the user that MFA has been reset. They must complete the full setup flow again (POST /api/auth/mfa/setupPOST /api/auth/mfa/verify-setup) and store their new backup codes.

Error responses for the admin endpoint:

StatusCondition
404User ID not found
400MFA is not currently enabled on the specified user account

DELETE /api/admin/users/{user_id}/mfa

  • Auth: Admin access token required.
  • No request body.
  • Clears all MFA state for the target user (mfa_enabled = false, totp_secret = null, backup_codes = null).
  • Does not require the user’s TOTP code — this is an admin override action.
  • Generates an audit log entry.
Terminal window
curl -X DELETE https://api.arbitex.ai/api/admin/users/usr_abc123/mfa \
-H "Authorization: Bearer <admin_access_token>"

When to use this endpoint:

  • User is locked out with no remaining backup codes.
  • User’s device is lost or stolen and the authenticator app cannot be recovered.
  • Offboarding: clearing MFA state before deactivating a user account.
  • Compliance: resetting MFA to enforce re-enrollment after a security event.

Do not use this endpoint as a routine support shortcut. Require identity verification before resetting MFA for any user.


For users:

  • Store backup codes in a password manager or secure offline location immediately after setup. They cannot be retrieved later.
  • Use a dedicated authenticator app rather than SMS-based codes where possible — Arbitex MFA uses TOTP and does not offer SMS as a factor.
  • If your device is lost, disable MFA via a backup code before the device is replaced, or contact your administrator.

For administrators:

  • Require identity verification (e.g., video call, manager confirmation) before issuing an admin MFA reset for any account. The force-disable endpoint bypasses all TOTP validation.
  • Audit the DELETE /api/admin/users/{user_id}/mfa endpoint logs regularly — unexpected calls may indicate a social engineering attempt.
  • Encourage users to regenerate backup codes (by re-enrolling) before they run out. The backup_codes_remaining field on GET /api/auth/mfa/status provides current count.
  • For org-level MFA enforcement (requiring all users to have MFA enabled before accessing the platform), configure the relevant policy rule in the Policy Engine rather than managing enrollment individually.