Multi-Factor Authentication
Truss supports three MFA methods — TOTP (authenticator apps), WebAuthn (security keys and passkeys), and recovery codes. All methods are powered by Ory Kratos and managed through both the dashboard UI and REST API. This guide walks through each method from setup to verification.
For API endpoint reference, see the Authentication guide.
Prerequisites
- A running Truss instance with Kratos configured (
KRATOS_PUBLIC_URL,KRATOS_ADMIN_URL) - An authenticated user session (all MFA operations require a valid
truss_sessioncookie) TRUSS_AUTH_REQUIRED=truein your API.env
Checking MFA Status
Before setting up any MFA method, check what the user already has configured:
curl http://localhost:8787/api/auth/mfa/status \ -H "Cookie: truss_session=your-session-token"Response:
{ "totp": false, "webauthn": false, "webauthn_credentials": [], "lookup_secret": false, "lookup_secrets_count": 0, "lookup_secrets_used": 0}| Field | Description |
|---|---|
totp | Whether TOTP (authenticator app) is enabled |
webauthn | Whether any WebAuthn credentials are registered |
webauthn_credentials | Array of registered WebAuthn devices with id, display_name, and added_at |
lookup_secret | Whether recovery codes are active |
lookup_secrets_count | Total number of recovery codes generated |
lookup_secrets_used | Number of recovery codes already used |
TOTP (Authenticator App)
TOTP uses time-based one-time passwords generated by apps like Google Authenticator, Authy, or 1Password. The user scans a QR code to register, then enters a 6-digit code to verify.
Step 1: Initialize TOTP Setup
curl -X POST http://localhost:8787/api/auth/mfa/totp/setup \ -H "Cookie: truss_session=your-session-token"Response:
{ "flow_id": "abc123-flow-id", "totp_url": "otpauth://totp/Truss:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Truss", "totp_secret": "JBSWY3DPEHPK3PXP"}totp_url— Anotpauth://URI. Encode this as a QR code for the user to scan with their authenticator app.totp_secret— The raw secret for manual entry if the user cannot scan the QR code.flow_id— Required for the verification step.
Step 2: Verify the TOTP Code
After the user scans the QR code, they enter the 6-digit code from their authenticator app:
curl -X POST http://localhost:8787/api/auth/mfa/totp/verify \ -H "Cookie: truss_session=your-session-token" \ -H "Content-Type: application/json" \ -d '{ "flow_id": "abc123-flow-id", "totp_code": "123456" }'A successful response confirms TOTP is now active. From this point on, the user will need to provide a TOTP code during login when the session requires AAL2 (second-factor authentication).
Removing TOTP
curl -X DELETE http://localhost:8787/api/auth/mfa/totp \ -H "Cookie: truss_session=your-session-token"This removes the TOTP credential from the user’s identity. They will no longer be prompted for a TOTP code at login.
Full Integration Example
const TRUSS_URL = "http://localhost:8787";
// 1. Start TOTP setupconst setup = await fetch(`${TRUSS_URL}/api/auth/mfa/totp/setup`, { method: "POST", credentials: "include",}).then(r => r.json());
// 2. Show QR code to user (use a library like qrcode.js)// setup.totp_url contains the otpauth:// URIconst qrImg = document.getElementById("qr");QRCode.toCanvas(qrImg, setup.totp_url);
// Also show the manual secret as fallbackdocument.getElementById("secret").textContent = setup.totp_secret;
// 3. User enters the code from their authenticator appconst code = document.getElementById("totp-input").value;const result = await fetch(`${TRUSS_URL}/api/auth/mfa/totp/verify`, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ flow_id: setup.flow_id, totp_code: code, }),});
if (result.ok) { console.log("TOTP enabled successfully");} else { const err = await result.json(); console.error("Verification failed:", err.error);}import requests
TRUSS_URL = "http://localhost:8787"cookies = {"truss_session": "your-session-token"}
# 1. Start TOTP setupsetup = requests.post( f"{TRUSS_URL}/api/auth/mfa/totp/setup", cookies=cookies).json()
print(f"Scan this URL as QR code: {setup['totp_url']}")print(f"Or enter manually: {setup['totp_secret']}")
# 2. User enters the codetotp_code = input("Enter the 6-digit code from your authenticator app: ")
# 3. Verifyres = requests.post( f"{TRUSS_URL}/api/auth/mfa/totp/verify", cookies=cookies, json={"flow_id": setup["flow_id"], "totp_code": totp_code},)
if res.ok: print("TOTP enabled successfully")else: print(f"Verification failed: {res.json()['error']}")# 1. Initialize TOTP setupSETUP=$(curl -s -X POST http://localhost:8787/api/auth/mfa/totp/setup \ -H "Cookie: truss_session=$SESSION")
echo "QR URL: $(echo $SETUP | jq -r '.totp_url')"echo "Secret: $(echo $SETUP | jq -r '.totp_secret')"
FLOW_ID=$(echo $SETUP | jq -r '.flow_id')
# 2. Verify with code from authenticator appcurl -X POST http://localhost:8787/api/auth/mfa/totp/verify \ -H "Cookie: truss_session=$SESSION" \ -H "Content-Type: application/json" \ -d "{\"flow_id\": \"$FLOW_ID\", \"totp_code\": \"123456\"}"WebAuthn (Security Keys)
WebAuthn supports hardware security keys (YubiKey, Titan) and platform authenticators (Touch ID, Windows Hello). The browser’s WebAuthn API handles the cryptographic challenge.
Step 1: Initialize Registration
curl -X POST http://localhost:8787/api/auth/mfa/webauthn/setup \ -H "Cookie: truss_session=your-session-token"Response:
{ "flow_id": "abc123-flow-id", "webauthn_options": { "publicKey": { "rp": { "name": "Truss", "id": "localhost" }, "user": { "id": "...", "name": "user@example.com", "displayName": "User" }, "challenge": "base64-encoded-challenge", "pubKeyCredParams": [ { "type": "public-key", "alg": -7 }, { "type": "public-key", "alg": -257 } ], "authenticatorSelection": { "userVerification": "preferred" } } }}The webauthn_options object is passed directly to the browser’s navigator.credentials.create() API.
Step 2: Create Credential and Complete Registration
This step must happen in a browser environment because it requires the WebAuthn API:
const TRUSS_URL = "http://localhost:8787";
// 1. Get registration options from serverconst options = await fetch(`${TRUSS_URL}/api/auth/mfa/webauthn/setup`, { method: "POST", credentials: "include",}).then(r => r.json());
// 2. Browser prompts user to insert/touch security keyconst credential = await navigator.credentials.create({ publicKey: options.webauthn_options.publicKey,});
// 3. Send attestation response to serverconst result = await fetch(`${TRUSS_URL}/api/auth/mfa/webauthn/verify`, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ flow_id: options.flow_id, webauthn_register: JSON.stringify(credential), webauthn_register_displayname: "My YubiKey", }),});
if (result.ok) { console.log("Security key registered");}The webauthn_register_displayname is a human-readable label for the credential (defaults to “Security Key” if omitted). This label appears in the MFA status when listing registered devices.
Removing a WebAuthn Credential
# Remove a specific credential by IDcurl -X DELETE http://localhost:8787/api/auth/mfa/webauthn \ -H "Cookie: truss_session=your-session-token" \ -H "Content-Type: application/json" \ -d '{"credential_id": "credential-id-here"}'
# Remove all WebAuthn credentialscurl -X DELETE http://localhost:8787/api/auth/mfa/webauthn \ -H "Cookie: truss_session=your-session-token"You can find credential IDs from the webauthn_credentials array in the MFA status response.
Passkey Login (Passwordless)
Passkeys use the same WebAuthn protocol but for passwordless authentication instead of second-factor. The user signs in with biometrics (Face ID, fingerprint) or a security key without entering a password.
Passkeys must first be registered via the WebAuthn setup flow above. Once registered, the user can use them to sign in directly.
Step 1: Initialize Passkey Login
curl http://localhost:8787/api/auth/login/passkeyResponse:
{ "flow_id": "abc123-flow-id", "passkey_options": { "publicKey": { "challenge": "base64-encoded-challenge", "rpId": "localhost", "allowCredentials": [ { "type": "public-key", "id": "credential-id-base64" } ], "userVerification": "preferred" } }}Step 2: Complete Passkey Login
// 1. Get assertion optionsconst options = await fetch(`${TRUSS_URL}/api/auth/login/passkey`).then(r => r.json());
// 2. Browser prompts user for biometric/keyconst assertion = await navigator.credentials.get({ publicKey: options.passkey_options.publicKey,});
// 3. Send assertion to serverconst session = await fetch(`${TRUSS_URL}/api/auth/login/passkey`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ flow_id: options.flow_id, passkey_login: JSON.stringify(assertion), }),}).then(r => r.json());
// session.session contains the authenticated sessionconsole.log("Logged in as:", session.session.identity.traits.email);# 1. Initialize passkey logincurl http://localhost:8787/api/auth/login/passkey
# 2. Complete login (assertion response from browser WebAuthn API)curl -X POST http://localhost:8787/api/auth/login/passkey \ -H "Content-Type: application/json" \ -d '{"flow_id": "abc123", "passkey_login": "<assertion-json>"}'Recovery Codes
Recovery codes are one-time backup codes that let users regain access when their primary MFA device is unavailable. Truss generates a set of codes that the user must save securely. Each code can only be used once.
Step 1: Generate Codes
curl -X POST http://localhost:8787/api/auth/mfa/recovery-codes/generate \ -H "Cookie: truss_session=your-session-token"Response:
{ "flow_id": "abc123-flow-id", "codes": [ "abc12-def34", "ghi56-jkl78", "mno90-pqr12", "stu34-vwx56", "yza78-bcd90", "efg12-hij34", "klm56-nop78", "qrs90-tuv12" ]}Display these codes to the user and instruct them to save them in a secure location (password manager, printed copy, etc.).
Step 2: Confirm Codes Saved
After the user acknowledges they have saved the codes, confirm to activate them:
curl -X POST http://localhost:8787/api/auth/mfa/recovery-codes/confirm \ -H "Cookie: truss_session=your-session-token" \ -H "Content-Type: application/json" \ -d '{"flow_id": "abc123-flow-id"}'Until confirmation, the codes are not active. This two-step process ensures users have actually copied the codes before they become usable.
Revoking Recovery Codes
If codes are compromised or the user wants to regenerate them:
curl -X DELETE http://localhost:8787/api/auth/mfa/recovery-codes \ -H "Cookie: truss_session=your-session-token"After revoking, generate a new set by repeating steps 1 and 2.
Monitoring Code Usage
Use the MFA status endpoint to track how many codes remain:
curl http://localhost:8787/api/auth/mfa/status \ -H "Cookie: truss_session=your-session-token"The response includes lookup_secrets_count (total generated) and lookup_secrets_used (already consumed). When lookup_secrets_used approaches lookup_secrets_count, prompt the user to generate new codes.
MFA Enforcement
Truss supports MFA enforcement policies through Kratos configuration. When MFA is enforced, users must complete a second factor during login to achieve AAL2 (Authenticator Assurance Level 2).
How AAL Works
Kratos defines two assurance levels:
- AAL1 — Single-factor authentication (password, social login, magic link)
- AAL2 — Multi-factor authentication (AAL1 + TOTP, WebAuthn, or recovery code)
When highest_available MFA enforcement is configured, Kratos checks whether the user has any second-factor credentials registered. If they do, the login flow requires AAL2 before issuing a full session.
Configuration
MFA enforcement is configured in the Kratos identity server configuration (not in Truss directly):
selfservice: flows: settings: required_aal: highest_available login: after: hooks: - hook: require_verified_addresssession: whoami: required_aal: highest_availableWith required_aal: highest_available:
- Users without any MFA methods get AAL1 sessions (no change)
- Users with TOTP or WebAuthn registered are prompted for their second factor
- The session is only granted AAL2 after the second factor is verified
Dashboard
The MFA section in the dashboard (Authentication > Settings) provides:
- MFA status overview — shows which methods are active for the current user
- TOTP setup wizard — QR code display, secret key, and verification form
- WebAuthn registration — one-click security key registration with device naming
- Recovery code generator — generate, display, confirm, and revoke codes
- Device list — all registered WebAuthn credentials with names and registration dates, with individual removal
For admin users, the Authentication > Users tab shows each user’s MFA status in the identity detail view, including which credentials are registered and their types.
API Reference
| Method | Path | Description |
|---|---|---|
GET | /api/auth/mfa/status | Get MFA status for current user |
POST | /api/auth/mfa/totp/setup | Initialize TOTP setup (returns QR URI + secret) |
POST | /api/auth/mfa/totp/verify | Verify TOTP code to complete setup |
DELETE | /api/auth/mfa/totp | Remove TOTP from account |
POST | /api/auth/mfa/webauthn/setup | Initialize WebAuthn registration |
POST | /api/auth/mfa/webauthn/verify | Complete WebAuthn registration |
DELETE | /api/auth/mfa/webauthn | Remove WebAuthn credential(s) |
POST | /api/auth/mfa/recovery-codes/generate | Generate recovery codes |
POST | /api/auth/mfa/recovery-codes/confirm | Confirm codes saved (activates them) |
DELETE | /api/auth/mfa/recovery-codes | Revoke all recovery codes |
GET | /api/auth/login/passkey | Initialize passkey login flow |
POST | /api/auth/login/passkey | Complete passkey login |
All MFA endpoints (except passkey login) require an authenticated session via the truss_session cookie.