# How Does MFA Work in Logto? TOTP, WebAuthn, and Backup Codes Explained

> Discover how Logto handles MFA with TOTP, WebAuthn, and backup codes. Learn about secure encrypted records and robust API validation for enhanced security.

- Repository: [Logto/logto](https://github.com/logto-io/logto)
- Tags: how-to-guide
- Published: 2026-07-05

---

**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`](https://github.com/logto-io/logto/blob/main/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`](https://github.com/logto-io/logto/blob/main/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`](https://github.com/logto-io/logto/blob/main/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`](https://github.com/logto-io/logto/blob/main/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`](https://github.com/logto-io/logto/blob/main/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`](https://github.com/logto-io/logto/blob/main/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`](https://github.com/logto-io/logto/blob/main/packages/core/src/routes/experience/verification-routes/totp-verification.openapi.json).

### TOTP Enrollment

1. **Secret Generation**: `POST /api/experience/verification/totp/secret` creates a random base32 secret, encrypts it, and returns a QR code URL for authenticator app scanning.
2. **Verification**: The user submits a code to `POST /api/experience/verification/totp/verify`, which validates the payload using `totpVerificationVerifyPayloadGuard` from [`packages/schemas/src/types/interactions.ts`](https://github.com/logto-io/logto/blob/main/packages/schemas/src/types/interactions.ts) before checking against the stored secret.

### WebAuthn Registration

1. **Challenge Creation**: `POST /api/experience/verification/webauthn/registration/options` generates a `PublicKeyCredentialCreationOptions` challenge including the allowed origins.
2. **Attestation**: The browser's `navigator.credentials.create()` returns an attestation.
3. **Verification**: `POST /api/experience/verification/webauthn/registration/verify` validates the attestation using `@simplewebauthn/server` and stores the credential in a `WebauthnVerificationRecord`.
4. **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`](https://github.com/logto-io/logto/blob/main/packages/schemas/src/types/interactions.ts) (e.g., `webauthnVerificationVerifyPayloadGuard`, `backupCodeVerificationVerifyPayloadGuard`). Each request triggers:

- **Payload validation** against the strict schema.
- **Record lookup** in the `verification_records` table.
- **Cryptographic verification** (TOTP code window check, WebAuthn signature validation, or backup code hash comparison).
- **Sentinel logging** via [`packages/schemas/src/foundations/jsonb-types/sentinel.ts`](https://github.com/logto-io/logto/blob/main/packages/schemas/src/foundations/jsonb-types/sentinel.ts) to 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`](https://github.com/logto-io/logto/blob/main/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

```typescript
// 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

```typescript
// 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

```typescript
// 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`](https://github.com/logto-io/logto/blob/main/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_centers` table, 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`](https://github.com/logto-io/logto/blob/main/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`](https://github.com/logto-io/logto/blob/main/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.