Two-Factor Authentication (TOTP)
Gate Identity supports TOTP-based MFA (Time-based One-Time Passwords, RFC 6238). When enabled, your end-users can link an authenticator app to their account and will be prompted for a 6-digit code at every login.
Backup codes (lookup secrets) are also supported — each user gets a set of single-use codes they can store offline in case they lose access to their authenticator.
Enabling MFA for Your Tenant
MFA is opt-in per tenant and disabled by default.
Via the Dashboard
Go to Gate Identity → Security and toggle Two-Factor Authentication (TOTP) on, then click Save Changes.
Via the API
curl -X PUT https://api.vernesoft.com/v1/gate/settings/security \
-H 'Authorization: Bearer vrn_gate_live_sk_9f8a7...' \
-H 'Content-Type: application/json' \
-d '{ "passwordless_enabled": false, "mfa_enabled": true }'
Response (200 OK)
{ "status": "ok" }
Note: Both
passwordless_enabledandmfa_enabledmust be included in the PUT body. Use the current values fromGET /v1/gate/settings/securityif you only want to change one.
Reading Current Security Settings
curl https://api.vernesoft.com/v1/gate/settings/security \
-H 'Authorization: Bearer vrn_gate_live_sk_9f8a7...'
{
"passwordless_enabled": false,
"mfa_enabled": true
}
How It Works
1. User sets up TOTP (via your settings page)
After a user logs in, your app initiates a settings flow and the response will contain group: "totp" nodes — a QR code URI and a manual entry key. The user scans this with their authenticator app.
Your app Edge Gateway Kratos
│ │ │
│── GET /v1/gate/auth/settings ──────▶ │
│ X-Session-Token: <token> │
│◀── flow JSON (with totp nodes) ───────-│
│ │ │
│── POST /v1/gate/auth/settings/submit ─▶│
│ { "method": "totp", │
│ "totp_code": "123456" } │
│◀── 200 TOTP linked ───────────────────-│
The TOTP nodes include:
group: "totp",attributes.id: "totp_secret_key"— the secret (also as QR code URI inmeta.label)group: "totp",attributes.name: "totp_code"— the verification inputgroup: "lookup_secret"nodes — the one-time backup codes
2. Login with TOTP
After a user with TOTP enabled submits their password, Kratos automatically prompts for the TOTP code by returning the login flow with group: "totp" nodes. Your UI renders the code input and submits it.
Your app Edge Gateway Kratos
│ │ │
│── GET /v1/gate/auth/login ───────────▶ │
│◀── flow + "mfa_required": true ───────-│
│ │ │
│ [user submits password to Kratos action URL]
│ │ │
│◀── 422: flow with totp nodes ─────────-│ ← Kratos requests 2nd factor
│ │ │
│ [user enters TOTP code] │
│ [submits to Kratos action URL] │
│◀── 200: session ──────────────────────-│
Note: After the initial password submit, the TOTP challenge is handled by the same Kratos action URL — your app submits
{ "method": "totp", "totp_code": "123456" }toflow.ui.action.
The mfa_required: true field in the login flow response signals that your tenant has MFA enabled, so your UI can show appropriate messaging.
Integration Guide
// ─── 1. Render TOTP setup in user settings ────────────────────────────────────
async function loadTotpSetup(sessionToken) {
// Fetch settings flow from your backend
const flow = await fetch('/account/settings/flow').then(r => r.json())
// Find TOTP nodes
const totpSecret = flow.ui.nodes.find(
n => n.group === 'totp' && n.attributes?.id === 'totp_secret_key'
)
const totpInput = flow.ui.nodes.find(
n => n.group === 'totp' && n.attributes?.name === 'totp_code'
)
const lookupNodes = flow.ui.nodes.filter(n => n.group === 'lookup_secret')
if (!totpSecret) {
// TOTP not available — mfa_enabled is false for this tenant
return null
}
return {
qrUri: totpSecret.meta?.label?.text, // otpauth://totp/... — render as QR
secretKey: totpSecret.attributes.value, // manual entry fallback
backupCodes: lookupNodes
.filter(n => n.attributes?.text?.text)
.map(n => n.attributes.text.text),
flowId: flow.id,
}
}
// ─── 2. Verify and link the authenticator ────────────────────────────────────
async function linkTotp(flowId, totpCode) {
const result = await fetch(`/account/settings/password?flow=${flowId}`, {
// Note: reuse your settings/submit proxy route — just send method: "totp"
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ method: 'totp', totp_code: totpCode }),
}).then(r => r.json())
if (result.ui?.messages?.some(m => m.type === 'error')) {
throw new Error(result.ui.messages.map(m => m.text).join(', '))
}
return result
}
// ─── 3. Handle TOTP challenge during login ────────────────────────────────────
async function handleLoginFlow(flow, email, password) {
// Phase 1: submit credentials to Kratos action URL
const phase1 = await fetch(flow.ui.action, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ method: 'password', identifier: email, password }),
}).then(r => r.json())
// Phase 2: if Kratos needs TOTP, the flow comes back with totp nodes
const needsTotp = phase1.ui?.nodes?.some(n => n.group === 'totp')
if (needsTotp) {
return { requiresTotp: true, flow: phase1 }
}
// No TOTP — session returned directly
return { requiresTotp: false, session: phase1.session }
}
async function submitTotpChallenge(flow, totpCode) {
const result = await fetch(flow.ui.action, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ method: 'totp', totp_code: totpCode }),
}).then(r => r.json())
return result.session
}
mfa_required Flag in Login Flow
When your tenant has MFA enabled, GET /v1/gate/auth/login includes "mfa_required": true in the response. Use this to:
- Show "You'll need your authenticator app" messaging before the user starts logging in
- Render a TOTP input field after the password step
{
"id": "flow_abc123",
"mfa_required": true,
"ui": { ... }
}
Backup Codes
When a user sets up TOTP, Kratos also generates backup codes (group: "lookup_secret" nodes in the settings flow). These are single-use codes the user can store securely and use instead of their authenticator if they lose their device.
Backup codes can be regenerated at any time through the settings flow by submitting { "method": "lookup_secret", "lookup_secret_regenerate": true }.
Disabling MFA
To disable MFA for your tenant, set mfa_enabled: false via the Dashboard or the API. Existing users who have TOTP configured will still be prompted for their code — they must remove TOTP via the settings flow first.
Error Handling
| HTTP status | Meaning |
|---|---|
422 with totp_code error | Invalid or expired TOTP code — user should try again |
422 with lookup_secret error | Invalid backup code |
403 on settings submit | Session expired — user must log in again |