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
| Method | Endpoint prefix | Description |
|---|---|---|
| TOTP | /api/v1/auth/mfa | Time-based one-time passwords via any TOTP app (Google Authenticator, Authy, 1Password, etc.) |
| SMS OTP | /api/v1/auth/mfa/sms | 6-digit code sent via SMS (feature flag: mfa_sms) |
| Backup Codes | /api/v1/auth/mfa | 10 single-use emergency codes generated during TOTP setup |
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": []
}
| Field | Description |
|---|---|
secret | Base32-encoded TOTP secret. Manually enter this in the authenticator app if QR scanning fails |
qrCodeUrl | Base64-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:
| Parameter | Value |
|---|---|
| Algorithm | SHA1 |
| Digits | 6 |
| Period | 30 seconds |
| Skew | 1 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"
}
| Field | Type | Required | Description |
|---|---|---|---|
secret | string | Yes | The secret returned from /mfa/setup |
code | string | Yes | Current 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."
}
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."
}
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:
- The user should contact their tenant administrator.
- Administrators can disable MFA for a user via the admin console or the admin user API (
POST /api/v1/users/{id}/unlock). - After MFA is reset by an admin, the user logs in with password only and should immediately re-enroll in MFA.
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 Status | Error Code | Cause |
|---|---|---|
400 | VALIDATION_ERROR | Missing mfaToken, code, secret, or password |
401 | UNAUTHORIZED | Invalid or expired TOTP code |
401 | UNAUTHORIZED | MFA token expired (5 min window) |
401 | UNAUTHORIZED | Backup code already used or invalid |
409 | CONFLICT | MFA already enabled when calling setup |
Next Steps
- WebAuthn / Passkeys — phishing-resistant hardware authenticator as a second factor
- Token Lifecycle — sessions and refresh tokens after MFA login
- Email & Password — the first factor that precedes MFA