Skip to content
Beta — Truss is in public beta. Documentation is actively updated but may not reflect the latest changes. Report issues on GitHub.

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_session cookie)
  • TRUSS_AUTH_REQUIRED=true in your API .env

Checking MFA Status

Before setting up any MFA method, check what the user already has configured:

Terminal window
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
}
FieldDescription
totpWhether TOTP (authenticator app) is enabled
webauthnWhether any WebAuthn credentials are registered
webauthn_credentialsArray of registered WebAuthn devices with id, display_name, and added_at
lookup_secretWhether recovery codes are active
lookup_secrets_countTotal number of recovery codes generated
lookup_secrets_usedNumber 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

Terminal window
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 — An otpauth:// 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:

Terminal window
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

Terminal window
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 setup
const 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:// URI
const qrImg = document.getElementById("qr");
QRCode.toCanvas(qrImg, setup.totp_url);
// Also show the manual secret as fallback
document.getElementById("secret").textContent = setup.totp_secret;
// 3. User enters the code from their authenticator app
const 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);
}

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

Terminal window
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 server
const 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 key
const credential = await navigator.credentials.create({
publicKey: options.webauthn_options.publicKey,
});
// 3. Send attestation response to server
const 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

Terminal window
# Remove a specific credential by ID
curl -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 credentials
curl -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

Terminal window
curl http://localhost:8787/api/auth/login/passkey

Response:

{
"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 options
const options = await fetch(`${TRUSS_URL}/api/auth/login/passkey`).then(r => r.json());
// 2. Browser prompts user for biometric/key
const assertion = await navigator.credentials.get({
publicKey: options.passkey_options.publicKey,
});
// 3. Send assertion to server
const 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 session
console.log("Logged in as:", session.session.identity.traits.email);

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

Terminal window
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:

Terminal window
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:

Terminal window
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:

Terminal window
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):

kratos.yml
selfservice:
flows:
settings:
required_aal: highest_available
login:
after:
hooks:
- hook: require_verified_address
session:
whoami:
required_aal: highest_available

With 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

MethodPathDescription
GET/api/auth/mfa/statusGet MFA status for current user
POST/api/auth/mfa/totp/setupInitialize TOTP setup (returns QR URI + secret)
POST/api/auth/mfa/totp/verifyVerify TOTP code to complete setup
DELETE/api/auth/mfa/totpRemove TOTP from account
POST/api/auth/mfa/webauthn/setupInitialize WebAuthn registration
POST/api/auth/mfa/webauthn/verifyComplete WebAuthn registration
DELETE/api/auth/mfa/webauthnRemove WebAuthn credential(s)
POST/api/auth/mfa/recovery-codes/generateGenerate recovery codes
POST/api/auth/mfa/recovery-codes/confirmConfirm codes saved (activates them)
DELETE/api/auth/mfa/recovery-codesRevoke all recovery codes
GET/api/auth/login/passkeyInitialize passkey login flow
POST/api/auth/login/passkeyComplete passkey login

All MFA endpoints (except passkey login) require an authenticated session via the truss_session cookie.