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
Section titled “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
Section titled “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)
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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)
Section titled “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
Section titled “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
Section titled “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
Section titled “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)
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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.