How Does MFA Work in Logto? TOTP, WebAuthn, and Backup Codes Explained
Logto implements multi-factor authentication through three distinct verification records—TOTP, WebAuthn, and backup codes—each stored as encrypted JSONB records in the database and exposed via typed API endpoints that enforce strict payload validation and Sentinel-based rate limiting.
Logto's MFA architecture relies on a modular verification record system defined in the logto-io/logto repository. This design separates enrollment from authentication, stores secrets securely using platform-specific encryption, and enforces business rules—such as prohibiting backup codes as the sole MFA method—through a combination of Zod schema validation and database constraints.
MFA Methods and Data Architecture
Logto supports TOTP (Time-Based One-Time Password), WebAuthn (Passkeys), and backup codes as secondary authentication factors. Each method follows a consistent pattern: a type-specific record stored in the verification_records table, a Zod guard for request validation, and a dedicated class implementing the verification logic.
TOTP Verification Records
TOTP implementation follows RFC 6238. The database schema in packages/schemas/src/types/verification-records/totp-verification.ts defines the structure for TotpVerificationRecord, which stores the encrypted secret and algorithm parameters. The TotpVerification class in packages/core/src/routes/experience/classes/verifications/totp-verification.ts implements MfaVerificationRecord<VerificationType.TOTP> and orchestrates the verification flow using helper functions from packages/core/src/libraries/verification-helpers/totp-validation.ts.
WebAuthn Credential Storage
WebAuthn credentials store public keys, sign counts, and credential IDs. The schema in packages/schemas/src/types/verification-records/webauthn-verification.ts defines the WebauthnVerificationRecord structure. Origin validation is enforced against the webAuthnRelatedOrigins column in the account_centers table (packages/schemas/tables/account_centers.sql), ensuring credentials are only registered for HTTPS origins or explicitly allowed localhost addresses.
Backup Code Structure
Backup codes are single-use recovery credentials. The BackupCodeVerificationRecord defined in packages/schemas/src/types/verification-records/backup-code-verification.ts stores an array of SHA-256 hashed codes. Plain-text codes are generated cryptographically and displayed only once during enrollment; the system never stores unhashed versions.
Enrollment and Verification Flows
All MFA flows share a common lifecycle: challenge generation, client response, server verification, and Sentinel logging. The API endpoints are documented in OpenAPI specifications such as packages/core/src/routes/experience/verification-routes/totp-verification.openapi.json.
TOTP Enrollment
- Secret Generation:
POST /api/experience/verification/totp/secretcreates a random base32 secret, encrypts it, and returns a QR code URL for authenticator app scanning. - Verification: The user submits a code to
POST /api/experience/verification/totp/verify, which validates the payload usingtotpVerificationVerifyPayloadGuardfrompackages/schemas/src/types/interactions.tsbefore checking against the stored secret.
WebAuthn Registration
- Challenge Creation:
POST /api/experience/verification/webauthn/registration/optionsgenerates aPublicKeyCredentialCreationOptionschallenge including the allowed origins. - Attestation: The browser's
navigator.credentials.create()returns an attestation. - Verification:
POST /api/experience/verification/webauthn/registration/verifyvalidates the attestation using@simplewebauthn/serverand stores the credential in aWebauthnVerificationRecord. - Authentication: Similar endpoints exist under
/api/experience/verification/webauthn/authenticate/for generating challenges and verifying signed assertions.
Backup Code Generation
POST /api/experience/verification/backup-code/generate creates ten random 32-byte strings, hashes them with SHA-256, and stores the hashes in BackupCodeVerificationRecord. The system returns the plain-text codes only during this initial request. Logto enforces that backup codes cannot be the sole MFA method; at least one TOTP or WebAuthn credential must exist.
Unified Verification Process
All verification endpoints utilize Zod guards defined in packages/schemas/src/types/interactions.ts (e.g., webauthnVerificationVerifyPayloadGuard, backupCodeVerificationVerifyPayloadGuard). Each request triggers:
- Payload validation against the strict schema.
- Record lookup in the
verification_recordstable. - Cryptographic verification (TOTP code window check, WebAuthn signature validation, or backup code hash comparison).
- Sentinel logging via
packages/schemas/src/foundations/jsonb-types/sentinel.tsto track success and failure rates for rate limiting.
Security Implementation Details
Sentinel Rate Limiting
The Sentinel system tracks every verification attempt. Defined in packages/schemas/src/foundations/jsonb-types/sentinel.ts, it monitors failure counts per user and verification type. After exceeding configurable thresholds, the system temporarily locks the verification endpoint, preventing brute-force attacks against TOTP or backup codes.
Secret Handling and Encryption
TOTP secrets are encrypted at rest using application-level encryption before storage in the JSONB record. Backup codes utilize one-way SHA-256 hashing. WebAuthn private keys never leave the user's authenticator; only public keys and metadata reside in the database.
Origin Restrictions
WebAuthn registration strictly validates the origin property against the webAuthnRelatedOrigins array configured in the account_centers table. This prevents phishing attacks by ensuring credentials are only registered for legitimate domains.
Code Examples
The following examples demonstrate enrollment and verification using standard fetch requests. Replace BASE_URL with your Logto instance and include a valid Bearer token.
TOTP Enrollment and Verification
// Generate secret and QR code
const secretRes = await fetch(
`${BASE_URL}/api/experience/verification/totp/secret`,
{ method: 'POST', headers: { Authorization: `Bearer ${TOKEN}` } }
);
const { secret, qrCodeUrl } = await secretRes.json();
// After user scans QR code, verify the first code
const verifyRes = await fetch(
`${BASE_URL}/api/experience/verification/totp/verify`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${TOKEN}`,
},
body: JSON.stringify({ code: '123456' }),
}
);
WebAuthn Registration
// 1. Get challenge from server
const challengeRes = await fetch(
`${BASE_URL}/api/experience/verification/webauthn/registration/options`,
{ method: 'POST', headers: { Authorization: `Bearer ${TOKEN}` } }
);
const options = await challengeRes.json();
// 2. Create credential in browser
const credential = await navigator.credentials.create({ publicKey: options });
// 3. Send attestation to server
await fetch(
`${BASE_URL}/api/experience/verification/webauthn/registration/verify`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${TOKEN}`,
},
body: JSON.stringify(credential),
}
);
Backup Code Generation and Verification
// Generate codes (displayed only once)
const genRes = await fetch(
`${BASE_URL}/api/experience/verification/backup-code/generate`,
{ method: 'POST', headers: { Authorization: `Bearer ${TOKEN}` } }
);
const { codes } = await genRes.json();
console.log('Save these codes:', codes);
// Verify a backup code
await fetch(
`${BASE_URL}/api/experience/verification/backup-code/verify`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${TOKEN}`,
},
body: JSON.stringify({ code: codes[0] }),
}
);
Summary
- Logto supports three MFA methods: TOTP, WebAuthn (Passkeys), and backup codes, each implemented as distinct verification record types.
- Data is stored securely: TOTP secrets are encrypted, backup codes are SHA-256 hashed, and WebAuthn credentials store only public keys.
- API endpoints are strictly typed: All requests pass through Zod guards defined in
packages/schemas/src/types/interactions.ts, ensuring type safety and validation. - Security is enforced by Sentinel: Every verification attempt is logged for rate limiting, with automatic lockouts after repeated failures.
- Backup codes require a primary MFA method: The system enforces that backup codes cannot be the sole MFA factor, preventing account lockout scenarios.
- WebAuthn validates origins strictly: Credentials are bound to specific origins defined in the
account_centerstable, mitigating phishing risks.
Frequently Asked Questions
Can backup codes be used as the only MFA method in Logto?
No. Logto explicitly prohibits enabling backup codes as the sole MFA method. The system requires at least one active TOTP or WebAuthn credential before backup codes can be generated or used. This constraint prevents users from locking themselves out of their accounts if they lose their recovery codes.
How does Logto protect TOTP secrets from database breaches?
TOTP secrets are encrypted using application-level encryption before storage in the TotpVerificationRecord JSONB column. The TotpVerification class in packages/core/src/routes/experience/classes/verifications/totp-verification.ts handles decryption only during the verification process, ensuring that raw secrets are never persisted in plain text.
What happens when a WebAuthn authentication fails multiple times?
Failed WebAuthn attempts are tracked by the Sentinel system defined in packages/schemas/src/foundations/jsonb-types/sentinel.ts. After exceeding the configurable failure threshold, the verification endpoint returns a rate-limit error, temporarily blocking further authentication attempts for that user and method.
Are backup codes stored in plain text within the database?
No. Plain-text backup codes are displayed only once during the generation process via POST /api/experience/verification/backup-code/generate. The database stores only SHA-256 hashes of these codes in the BackupCodeVerificationRecord. During verification, the submitted code is hashed and compared against the stored hashes, ensuring that the original codes cannot be recovered from the database.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →