Skip to content

MFA Enforcement

This guide covers MFA (Multi-Factor Authentication) enforcement for the Arbitex AI Gateway platform: configuring org-level MFA policies via the admin API, understanding the three enforcement levels, protecting sensitive endpoints with MFA step-up, handling 403 + X-MFA-Required responses, and the administrator setup walkthrough.

Arbitex supports three organization-wide MFA enforcement levels (platform-0048):

LevelBehaviorUse Case
offMFA never required; users may optionally enrollDevelopment, internal tools
optionalUsers who have enrolled MFA must use it; un-enrolled users pass without MFAMigration period, low-risk orgs
requiredAll users must complete MFA enrollment before accessing the platformProduction, regulated industries

Additionally, sensitive endpoints enforce MFA step-up regardless of org policy — even in optional mode, accessing these endpoints requires a fresh MFA assertion.

GET /api/admin/org/mfa-policy
Authorization: Bearer <admin-token>

Response:

{
"enforcement_level": "optional",
"sensitive_endpoints_require_mfa": true,
"mfa_methods": ["totp", "webauthn"],
"grace_period_hours": 0,
"mfa_assertion_ttl_seconds": 3600,
"enrollment_deadline": null,
"created_at": "2026-01-15T10:00:00Z",
"updated_at": "2026-03-01T09:00:00Z"
}

Response fields:

FieldTypeDescription
enforcement_levelstringoff | optional | required
sensitive_endpoints_require_mfabooleanAlways enforce MFA for sensitive endpoints
mfa_methodsarrayEnabled MFA methods: totp, webauthn, sms
grace_period_hoursintegerHours before required kicks in for new enrollments
mfa_assertion_ttl_secondsintegerHow long a step-up MFA assertion is valid (default: 3600)
enrollment_deadlinestring|nullISO-8601 date by which all users must enroll (for required)
PUT /api/admin/org/mfa-policy
Authorization: Bearer <admin-token>
Content-Type: application/json

Request body:

{
"enforcement_level": "required",
"mfa_methods": ["totp", "webauthn"],
"grace_period_hours": 48,
"mfa_assertion_ttl_seconds": 3600,
"enrollment_deadline": "2026-04-01T00:00:00Z"
}

Response: 200 OK with the updated policy object.

Partial update — change only enforcement level:

Terminal window
curl -X PUT \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"enforcement_level": "required"}' \
https://api.arbitex.example.com/api/admin/org/mfa-policy

Important: Changing from offrequired directly will immediately block all non-MFA-enrolled users. Use a grace period or set enrollment_deadline to give users time to enroll.

  • No MFA prompts at login or during the session
  • Users can voluntarily enroll MFA through their profile settings
  • Sensitive endpoint step-up still applies if sensitive_endpoints_require_mfa: true
  • Recommended only for development environments or internal-only deployments
  • Users without MFA enrolled: pass through without MFA
  • Users with MFA enrolled: must complete MFA at each login (TOTP or WebAuthn)
  • Effective for organizations migrating to MFA — enrolled users are protected; others aren’t yet required to enroll
  • Session tokens carry an mfa_verified: true|false flag
  • Sensitive endpoints check mfa_verified regardless of enrollment status

Migration strategy:

Phase 1: enforcement_level = "optional" → encourage enrollment, monitor adoption
Phase 2: Set enrollment_deadline → notify users of upcoming requirement
Phase 3: enforcement_level = "required" → block access for un-enrolled users
  • All users must have at least one MFA method enrolled before logging in

  • Login attempt without MFA enrollment returns:

    HTTP/1.1 403 Forbidden
    X-MFA-Required: enroll
    Content-Type: application/json
    {
    "error": "mfa_enrollment_required",
    "message": "Your organization requires MFA enrollment. Please enroll a method at /settings/mfa before continuing.",
    "enroll_url": "https://app.arbitex.example.com/settings/mfa"
    }
  • If grace_period_hours > 0, newly created users can access the platform for that many hours before MFA is required

Certain endpoints always require a fresh MFA step-up assertion, regardless of enforcement_level. This is controlled by the sensitive_endpoints_require_mfa policy flag.

EndpointReason
PUT /api/admin/org/mfa-policyMFA policy changes
POST /api/admin/users/{id}/api-keysCreating API keys
DELETE /api/admin/users/{id}/api-keys/{key_id}Revoking API keys
PUT /api/admin/org/billingBilling configuration changes
POST /api/admin/gdpr/deletion-requestsGDPR deletion
PUT /api/admin/retention/policyData retention changes
POST /api/admin/outposts/{id}/rotate-certCertificate rotation
DELETE /api/admin/users/{id}User deletion
PUT /api/admin/org/saml-providers/{id}SAML IdP modifications
POST /api/admin/kill-switchProvider kill switch

When an unauthenticated MFA assertion is detected for a sensitive endpoint:

1. Client requests sensitive endpoint (no MFA assertion):

DELETE /api/admin/users/usr_01HXYZ HTTP/1.1
Authorization: Bearer <session-token>

2. Platform responds with 403 + X-MFA-Required header:

HTTP/1.1 403 Forbidden
X-MFA-Required: step_up
X-MFA-Challenge-ID: mfa_chal_01HXYZ
Content-Type: application/json
{
"error": "mfa_required",
"message": "This action requires MFA verification.",
"challenge_id": "mfa_chal_01HXYZ",
"expires_in": 600,
"methods": ["totp", "webauthn"]
}

3. Client completes MFA challenge:

Terminal window
# TOTP completion
curl -X POST \
-H "Authorization: Bearer $SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"challenge_id": "mfa_chal_01HXYZ",
"method": "totp",
"code": "123456"
}' \
https://api.arbitex.example.com/api/auth/mfa/verify
# Response — MFA assertion token
{
"mfa_assertion_token": "mfa_ast_01HXYZ...",
"expires_at": "2026-03-12T11:00:00Z",
"ttl_seconds": 3600
}

4. Client retries sensitive endpoint with MFA assertion:

DELETE /api/admin/users/usr_01HXYZ HTTP/1.1
Authorization: Bearer <session-token>
X-MFA-Assertion: mfa_ast_01HXYZ...

5. Platform processes request:

HTTP/1.1 204 No Content

MFA assertion tokens are cached in Redis (mfa_verified:{assertion_token}) with the TTL configured by mfa_assertion_ttl_seconds. Within this window, the client can access any sensitive endpoint without re-completing MFA.

The assertion token is scope-limited — it does not grant elevated API permissions, only satisfies the MFA check. Session tokens retain their original authorization scope.

Compatible with any RFC 6238 authenticator app (Google Authenticator, Authy, 1Password, etc.).

Enrollment flow:

  1. User navigates to /settings/mfa/enroll/totp
  2. Platform generates a TOTP secret and QR code
  3. User scans QR code with authenticator app
  4. User submits a 6-digit code to confirm enrollment
  5. Platform stores the encrypted TOTP secret and marks TOTP as enrolled

Admin view of user TOTP status:

Terminal window
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
https://api.arbitex.example.com/api/admin/users/usr_01HXYZ/mfa-status
# Response
{
"user_id": "usr_01HXYZ",
"mfa_enrolled": true,
"methods": [
{
"type": "totp",
"enrolled_at": "2026-02-15T14:30:00Z",
"last_used_at": "2026-03-12T09:00:00Z"
}
],
"last_mfa_at": "2026-03-12T09:00:00Z"
}

Supports hardware security keys (YubiKey, etc.) and platform passkeys (Touch ID, Face ID, Windows Hello).

Enrollment: Users navigate to /settings/mfa/enroll/webauthn and follow the browser’s passkey enrollment flow. The platform stores the credential public key (never the private key).

Admin notes:

  • WebAuthn is phishing-resistant — always bound to the origin (api.arbitex.example.com)
  • Users can register multiple WebAuthn credentials (backup key recommended)

SMS (Optional — Requires Carrier Integration)

Section titled “SMS (Optional — Requires Carrier Integration)”

SMS OTP is supported but not recommended for high-security environments due to SIM-swapping risk. Enable via:

Terminal window
curl -X PUT \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"mfa_methods": ["totp", "webauthn", "sms"]}' \
https://api.arbitex.example.com/api/admin/org/mfa-policy

Requires SMS_PROVIDER environment variable (e.g., twilio) and provider credentials.

Before enforcing MFA, check the current enrollment rate across your org:

Terminal window
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
"https://api.arbitex.example.com/api/admin/users?fields=id,email,mfa_enrolled&limit=1000" \
| jq '[.users[] | select(.mfa_enrolled == false)] | length'

Or use the summary endpoint:

Terminal window
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
https://api.arbitex.example.com/api/admin/org/mfa-summary
# Response
{
"total_users": 250,
"enrolled": 187,
"not_enrolled": 63,
"enrollment_rate": 0.748,
"by_method": {
"totp": 150,
"webauthn": 82,
"sms": 12
}
}

Start with optional to allow enrolled users to use MFA without blocking un-enrolled users:

Terminal window
curl -X PUT \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"enforcement_level": "optional",
"mfa_methods": ["totp", "webauthn"],
"sensitive_endpoints_require_mfa": true
}' \
https://api.arbitex.example.com/api/admin/org/mfa-policy

Use the org notification API or your own communication channel to notify users:

Terminal window
# Send in-app enrollment reminder to all un-enrolled users
curl -X POST \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"target": "unenrolled_users",
"message_type": "mfa_enrollment_reminder",
"enrollment_deadline": "2026-04-01T00:00:00Z"
}' \
https://api.arbitex.example.com/api/admin/notifications/send

Step 4: Set Enrollment Deadline and Grace Period

Section titled “Step 4: Set Enrollment Deadline and Grace Period”
Terminal window
curl -X PUT \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"enforcement_level": "optional",
"enrollment_deadline": "2026-04-01T00:00:00Z",
"grace_period_hours": 48
}' \
https://api.arbitex.example.com/api/admin/org/mfa-policy

After the enrollment deadline has passed and enrollment rate is acceptable:

Terminal window
curl -X PUT \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"enforcement_level": "required",
"grace_period_hours": 0
}' \
https://api.arbitex.example.com/api/admin/org/mfa-policy

If a user loses their MFA device:

Terminal window
# Generate a one-time bypass code (admin use only)
curl -X POST \
-H "Authorization: Bearer $ADMIN_TOKEN" \
https://api.arbitex.example.com/api/admin/users/usr_01HXYZ/mfa-bypass-code
# Response — single-use, expires in 1 hour
{
"bypass_code": "XXXX-XXXX-XXXX-XXXX",
"expires_at": "2026-03-12T12:00:00Z",
"note": "Code is single-use. User must re-enroll MFA after using this bypass."
}

Alternatively, reset the user’s MFA enrollment:

Terminal window
# Reset all MFA methods for a user (requires MFA step-up on admin account)
curl -X DELETE \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "X-MFA-Assertion: $MFA_ASSERTION_TOKEN" \
https://api.arbitex.example.com/api/admin/users/usr_01HXYZ/mfa-enrollment
# User will be prompted to re-enroll at next login

MFA events appear in the audit log with the following action types:

Audit ActionDescription
mfa.enrolledUser enrolled a new MFA method
mfa.unenrolledUser removed an MFA method
mfa.verifiedSuccessful MFA verification
mfa.failedFailed MFA verification attempt
mfa.bypass_usedAdmin bypass code was used
mfa.policy_updatedOrg MFA policy was changed
mfa.enrollment_resetAdmin reset user MFA enrollment

Search for MFA events via the audit log API:

Terminal window
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
"https://api.arbitex.example.com/api/admin/audit-logs?action_prefix=mfa.&limit=50"
# MFA verification success rate
rate(arbitex_mfa_verifications_total{status="success"}[5m])
/ rate(arbitex_mfa_verifications_total[5m])
# Failed MFA attempts (potential brute force)
rate(arbitex_mfa_verifications_total{status="failed"}[5m])
# Un-enrolled user logins (while policy is optional)
rate(arbitex_logins_total{mfa_enrolled="false"}[5m])

Configure an alert for excessive MFA failures:

- alert: MFABruteForceAttempt
expr: rate(arbitex_mfa_verifications_total{status="failed"}[5m]) > 10
for: 2m
labels:
severity: critical
team: platform
annotations:
summary: "High MFA failure rate — possible brute force"
description: "MFA failure rate is {{ $value }}/s — investigate for brute force activity."

The MFA enforcement middleware (backend/app/middleware/mfa_enforcement.py) is pure ASGI and runs on every HTTP request.

The middleware enforces MFA step-up on any path that begins with one of the following prefixes:

"/api/admin"
"/api/keys"
"/api/saml-admin"
"/api/policy"
"/api/auth/mfa/setup"
"/api/auth/mfa/disable"

The following paths always pass through regardless of enforcement level:

# Exact matches
"/health"
"/ready"
"/api/health"
"/api/auth/login"
"/api/auth/register"
"/api/auth/mfa/verify"
# Prefix match
"/.well-known/"

When MFA is required but mfa_verified is absent or false, the middleware returns:

HTTP/1.1 403 Forbidden
Content-Type: application/json
X-MFA-Required: true
{
"detail": "MFA verification required to access this resource",
"mfa_required": true
}

Clients should check for the X-MFA-Required: true response header and redirect to the MFA verification flow rather than displaying the raw 403 error to users.

Enforcement levels are cached in memory per org with a 60-second TTL:

Cache scope: per-process in-memory (dict, thread-safe via Lock)
Cache key: org_id (UUID string)
TTL: 60 seconds
Eviction: lazy — checked on each read, not actively expired

In a multi-instance deployment, each process has its own cache and will converge to the new policy independently within one TTL cycle of the change.

The middleware reads the standard HS256 user token from Authorization: Bearer. It checks for mfa_verified: true in the JWT payload. RS256 OAuth M2M tokens are not checked for mfa_verified — any decode failure (including RS256 key mismatch) causes fail-open pass-through.

The org ID is resolved in this order:

  1. scope["state"]["org_id"] (set by auth middleware)
  2. scope["state"]["tenant_id"]
  3. JWT tenant_id claim
  4. JWT org_id claim

If no org ID can be resolved, the request passes through (enforcement cannot apply without org context).