Skip to main content

Multi-Factor Authentication (MFA)

Ithbat IAM supports multiple second-factor methods that can be enforced at the tenant level or configured per user. This guide covers TOTP (authenticator apps), SMS OTP, and backup codes — including the complete two-step login flow.

Supported Methods

MethodEndpoint prefixDescription
TOTP/api/v1/auth/mfaTime-based one-time passwords via any TOTP app (Google Authenticator, Authy, 1Password, etc.)
SMS OTP/api/v1/auth/mfa/sms6-digit code sent via SMS (feature flag: mfa_sms)
Backup Codes/api/v1/auth/mfa10 single-use emergency codes generated during TOTP setup
note

WebAuthn (passkeys and hardware keys) is supported as a second factor and is documented separately in the WebAuthn guide.


Two-Step Login Flow

When a user has MFA enabled, the login API returns an MFA challenge instead of tokens:

{
"mfaRequired": true,
"mfaToken": "mfa_challenge_abc123..."
}

The mfaToken is short-lived (5 minutes) and must be exchanged for a full session by completing the MFA challenge.

sequenceDiagram
participant User
participant App as Your App
participant Ithbat as Ithbat IAM

User->>App: Email + password
App->>Ithbat: POST /api/v1/auth/login
Ithbat-->>App: { mfaRequired: true, mfaToken: "..." }

App-->>User: Prompt for TOTP code

User->>App: Enter 6-digit TOTP code
App->>Ithbat: POST /api/v1/auth/mfa/verify { mfaToken, code }
Ithbat->>Ithbat: Validate TOTP code
Ithbat-->>App: { accessToken, refreshToken, idToken, user }
App-->>User: Signed in

TOTP Setup

TOTP setup is a two-step process: generate the secret, then verify a code from the authenticator app to confirm the setup is correct.

Step 1 — Generate TOTP Secret

POST /api/v1/auth/mfa/setup

Required header: Authorization: Bearer <accessToken>

No request body is needed.

Response — 200 OK

{
"secret": "JBSWY3DPEHPK3PXP",
"qrCodeUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA...",
"backupCodes": []
}
FieldDescription
secretBase32-encoded TOTP secret. Manually enter this in the authenticator app if QR scanning fails
qrCodeUrlBase64-encoded PNG of a QR code. Display this in your UI for the user to scan

The user scans the QR code with their authenticator app. The app will generate a new 6-digit code every 30 seconds.

TOTP parameters used:

ParameterValue
AlgorithmSHA1
Digits6
Period30 seconds
Skew1 window (±30s drift tolerated)

Step 2 — Verify and Enable TOTP

After the user scans the QR code, ask them to enter the first code to confirm the setup worked.

POST /api/v1/auth/mfa/verify-setup

Required header: Authorization: Bearer <accessToken>

{
"secret": "JBSWY3DPEHPK3PXP",
"code": "482031"
}
FieldTypeRequiredDescription
secretstringYesThe secret returned from /mfa/setup
codestringYesCurrent TOTP code from the authenticator app

Response — 200 OK

{
"backupCodes": [
"A1B2C3D4",
"E5F6G7H8",
"I9J0K1L2",
"M3N4O5P6",
"Q7R8S9T0",
"U1V2W3X4",
"Y5Z6A7B8",
"C9D0E1F2",
"G3H4I5J6",
"K7L8M9N0"
],
"message": "MFA enabled successfully. Save your backup codes in a secure location."
}
danger

Backup codes are shown only once, immediately after enabling TOTP. If the user loses them, they must regenerate them (which invalidates all previous codes). Store them somewhere safe.


Verifying MFA During Login (TOTP)

POST /api/v1/auth/mfa/verify

This endpoint is public (no Authorization header). It uses the mfaToken from the initial login response.

{
"mfaToken": "mfa_challenge_abc123...",
"code": "482031"
}

Response — 200 OK

{
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...",
"idToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"tokenType": "Bearer",
"expiresIn": 3600,
"user": {
"id": "usr_01J8X...",
"tenantId": "ten_01J8X...",
"email": "[email protected]",
"displayName": "Sara Al-Rashidi",
"roles": ["member"],
"permissions": ["profile:read"]
}
}

Backup Codes

Using a Backup Code During Login

POST /api/v1/auth/mfa/verify-backup

{
"mfaToken": "mfa_challenge_abc123...",
"code": "A1B2C3D4"
}

On success, the backup code is permanently consumed and returns the same full token response as a regular MFA verification. Each backup code can only be used once.

Response — 200 OK

Same as the TOTP verify response above.

Regenerating Backup Codes

If a user has used or lost their backup codes, they can regenerate a fresh set. Regeneration requires an active TOTP code to prevent unauthorized regeneration.

POST /api/v1/auth/mfa/backup-codes

Required header: Authorization: Bearer <accessToken>

{
"code": "482031"
}

Response — 200 OK

{
"backupCodes": [
"P1Q2R3S4",
"T5U6V7W8",
"..."
],
"message": "Backup codes regenerated successfully. Please save them in a secure location."
}
warning

Regenerating backup codes invalidates all previously generated codes immediately.


SMS OTP

SMS MFA is available when the mfa_sms feature is enabled for the tenant.

Setup — Register Phone Number

POST /api/v1/auth/mfa/sms/setup

Required header: Authorization: Bearer <accessToken>

{
"phoneNumber": "+966501234567"
}

This sends a verification code to the phone number.

Response — 200 OK

{
"message": "Verification code sent to your phone number. Please verify to enable SMS MFA."
}

Setup — Verify Phone Number

POST /api/v1/auth/mfa/sms/verify-setup

Required header: Authorization: Bearer <accessToken>

{
"code": "847291"
}

Response — 200 OK

{
"message": "SMS MFA has been enabled successfully."
}

Send SMS Code During Login

When a user with SMS MFA signs in, after the initial login returns mfaRequired: true, your app sends an SMS OTP request.

POST /api/v1/auth/mfa/sms/send

Required header: Authorization: Bearer <accessToken>

{
"phoneNumber": "+966501234567"
}

Response — 200 OK

{
"message": "Verification code sent successfully."
}

Verify SMS Code During Login

POST /api/v1/auth/mfa/sms/verify

This endpoint is public (uses the mfaToken, not an access token).

{
"mfaToken": "mfa_challenge_abc123...",
"code": "847291"
}

On success, returns the full token response (same as TOTP verify).

Disable SMS MFA

POST /api/v1/auth/mfa/sms/disable

Required header: Authorization: Bearer <accessToken>

No request body required.

Response — 200 OK

{
"message": "SMS MFA has been disabled successfully."
}

Disabling TOTP MFA

Disabling TOTP requires both the current password and a valid TOTP code.

POST /api/v1/auth/mfa/disable

Required header: Authorization: Bearer <accessToken>

{
"password": "SecurePass123!",
"code": "482031"
}

Response — 200 OK

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

Recovery When Device Is Lost

If a user loses their authenticator device and has no backup codes:

  1. The user should contact their tenant administrator.
  2. Administrators can disable MFA for a user via the admin console or the admin user API (POST /api/v1/users/&#123;id&#125;/unlock).
  3. After MFA is reset by an admin, the user logs in with password only and should immediately re-enroll in MFA.
tip

Encourage users to save backup codes in a password manager when they first set up MFA. This removes the need for admin intervention in most loss scenarios.


Code Examples

cURL — TOTP Setup

# Step 1: Generate secret
curl -X POST https://api.ithbat.io/api/v1/auth/mfa/setup \
-H "Authorization: Bearer eyJhbGci..."

# Step 2: Verify and enable
curl -X POST https://api.ithbat.io/api/v1/auth/mfa/verify-setup \
-H "Authorization: Bearer eyJhbGci..." \
-H "Content-Type: application/json" \
-d '{"secret": "JBSWY3DPEHPK3PXP", "code": "482031"}'

cURL — Verify MFA During Login

curl -X POST https://api.ithbat.io/api/v1/auth/mfa/verify \
-H "Content-Type: application/json" \
-d '{"mfaToken": "mfa_challenge_abc123...", "code": "482031"}'

JavaScript SDK

import { IthbatSDK } from '@ithbatiam/sdk';

const ithbat = new IthbatSDK({ tenantId: 'ten_01J8X...' });

// === Setup Flow ===

// Step 1: Generate TOTP secret
const setup = await ithbat.mfa.setupTOTP();
// Show setup.qrCodeUrl as an <img> for the user to scan
// setup.secret is the manual entry fallback

// Step 2: User enters first code from their app
const result = await ithbat.mfa.verifyTOTPSetup({
secret: setup.secret,
code: '482031',
});
// result.backupCodes — show these once and tell user to save them

// === Login Flow with MFA ===

const loginResult = await ithbat.auth.login({
email: '[email protected]',
password: 'SecurePass123!',
});

if (loginResult.mfaRequired) {
// Prompt user for TOTP code
const mfaResult = await ithbat.mfa.verifyLogin({
mfaToken: loginResult.mfaToken,
code: '482031',
});
// mfaResult.accessToken — user is now fully authenticated
}

// === Backup Code Usage ===

const backupResult = await ithbat.mfa.verifyBackupCode({
mfaToken: loginResult.mfaToken,
code: 'A1B2C3D4',
});

// === Regenerate Backup Codes ===

const newCodes = await ithbat.mfa.regenerateBackupCodes({ code: '482031' });
// newCodes.backupCodes

Error Handling

HTTP StatusError CodeCause
400VALIDATION_ERRORMissing mfaToken, code, secret, or password
401UNAUTHORIZEDInvalid or expired TOTP code
401UNAUTHORIZEDMFA token expired (5 min window)
401UNAUTHORIZEDBackup code already used or invalid
409CONFLICTMFA already enabled when calling setup

Next Steps