Implementing Passwordless Authentication with Passcodes in Logto: A Complete Technical Guide

Logto enables passwordless authentication by generating cryptographically secure 6-digit passcodes, delivering them via configurable email or SMS connectors, and verifying them against tenant-specific policies that enforce expiration windows and retry limits.

Implementing passwordless authentication with passcodes in Logto provides users with a secure, frictionless sign-in experience without requiring memorized passwords. The logto-io/logto repository implements this flow through a tenant-aware PasscodeLibrary that handles code generation, message delivery via the ConnectorKit API, and strict policy enforcement. This guide examines the source code architecture and provides practical implementation examples for integrating passcode-based verification into your identity management workflows.

Core Architecture of Logto's Passcode System

Logto's passcode implementation follows a modular architecture separating concerns between library logic, database queries, API routes, and frontend components.

PasscodeLibrary Factory

The createPasscodeLibrary function in [packages/core/src/libraries/passcode.ts](https://github.com/logto-io/logto/blob/master/packages/core/src/libraries/passcode.ts) serves as the central factory that exposes three core operations:

  • createPasscode – Generates a random 6-digit numeric code and handles database cleanup of existing unconsumed passcodes for the same jti (session identifier) or user identifier.
  • sendPasscode – Routes the generated code to the appropriate message connector (Email or SMS) based on whether the payload contains an email or phone field.
  • verifyPasscode – Validates the submitted code against the stored record, checking expiration timestamps, retry counts, and identifier matches.

Database Query Layer

Low-level database operations reside in [packages/core/src/queries/passcode.ts](https://github.com/logto-io/logto/blob/master/packages/core/src/queries/passcode.ts). These transactions ensure data consistency during the verification flow:

  • insertPasscode – Creates the initial passcode record with tryCount initialized to zero.
  • findUnconsumedPasscodeByJtiAndIdentifier – Retrieves active codes excluding already-verified entries.
  • consumePasscode – Marks the passcode as used after successful verification.
  • increasePasscodeTryCount – Increments the attempt counter during validation attempts.
  • deletePasscodesByIds – Removes expired or consumed codes during cleanup operations.

Management API Endpoints

The [packages/core/src/routes/verification-code.ts](https://github.com/logto-io/logto/blob/master/packages/core/src/routes/verification-code.ts) file exposes two public endpoints secured by Sentinel rate-limiting:

  • POST /verification-codes – Initiates the flow by invoking createPasscode and sendPasscode.
  • POST /verification-codes/verify – Validates user input by calling verifyPasscode.

These routes return 204 No Content on success or 400 Bad Request with specific error codes (e.g., verification_code.expired, verification_code.code_mismatch) on failure.

Security Policies and Validation Logic

Logto enforces tenant-aware verification policies that apply to all passcode operations within a specific tenant context.

Policy Enforcement Mechanism

The verifyPasscode implementation retrieves the tenant's verificationCodePolicy configuration (or falls back to system defaults) and performs three critical checks:

  1. Expiration validation – Compares passcode.createdAt + expirationMs against Date.now() to reject stale codes.
  2. Retry limiting – Validates that passcode.tryCount remains below maxTryCount to prevent brute-force attacks.
  3. Identifier binding – Ensures the email or phone number submitted during verification matches the original identifier stored with the passcode.

If any check fails, the library throws a RequestError with a machine-readable error code suitable for frontend localization.

Message Connector Integration

The sendPasscode function dynamically selects connectors based on the identifier type. Any connector implementing the ConnectorKit API can handle delivery, including custom SMTP servers, Twilio SMS, or proprietary messaging services. The buildVerificationCodeContext helper enriches message templates with contextual data such as application name, organization details, and user metadata.

Implementation Examples

Requesting a Passcode via REST API

Send a POST request to create and dispatch a new passcode:

curl -X POST https://your-logto-instance.com/api/verification-codes \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com"}' \
  -i

A successful request returns 204 No Content and triggers the configured email connector to deliver the 6-digit code.

Verifying a Received Passcode

Submit the user-entered code for validation:

curl -X POST https://your-logto-instance.com/api/verification-codes/verify \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com","verificationCode":"123456"}' \
  -i

Success yields 204 No Content. Invalid submissions return 400 Bad Request with an error code such as verification_code.invalid_passcode or verification_code.expired.

JavaScript SDK Integration

Use the Logto client SDK to handle passcode flows programmatically:

import { LogtoClient } from '@logto/client';

const logto = new LogtoClient({
  endpoint: 'https://your-logto-instance.com',
  appId: 'your-app-id',
});

// Request passcode delivery
await logto.sendVerificationCode({ email: 'user@example.com' });

// Verify user input
await logto.verifyVerificationCode({
  email: 'user@example.com',
  verificationCode: '123456',
});

The SDK automatically surfaces RequestError instances as JavaScript exceptions with detailed error codes for error handling.

React Component Integration

For custom sign-in experiences, import the official verification component:

import VerificationCode from '@/packages/experience/src/shared/components/VerificationCode';

function SignInPage() {
  const [code, setCode] = useState('');
  
  return (
    <VerificationCode
      name="passcode"
      value={code}
      onChange={setCode}
      placeholder="Enter verification code"
    />
  );
}

The component automatically displays localized error messages (e.g., error.invalid_passcode) when bound to the Logto error state.

Summary

  • Logto's passcode system lives in packages/core/src/libraries/passcode.ts and provides createPasscode, sendPasscode, and verifyPasscode functions.
  • Database operations in packages/core/src/queries/passcode.ts handle atomic updates for try counts, consumption status, and cleanup.
  • Management API endpoints at /verification-codes and /verification-codes/verify expose the functionality to clients with built-in rate limiting.
  • Security policies enforce expiration windows, maximum retry attempts, and identifier binding at the tenant level.
  • Frontend integration utilizes the VerificationCode React component and supports full localization via packages/phrases-experience.

Frequently Asked Questions

What is the default expiration time for passcodes in Logto?

Logto applies the verificationCodePolicy configuration defined at the tenant level, which specifies the expiration duration in milliseconds. If the tenant has not customized this policy, the system falls back to a default expiration window that ensures codes remain valid long enough for email/SMS delivery but short enough to mitigate replay attacks.

How does Logto prevent brute-force attacks on passcode verification?

The verifyPasscode function in packages/core/src/libraries/passcode.ts increments the tryCount field via increasePasscodeTryCount on each failed attempt and compares it against the maxTryCount policy limit. Once the threshold is exceeded, subsequent verification attempts automatically fail with a verification_code.code_mismatch error until a new passcode is generated.

Can I customize the email template for passcode delivery?

Yes. The sendPasscode function passes the generated code to your configured message connector through the ConnectorKit API. You can define custom templates in your email connector configuration (such as SendGrid or SMTP), and the buildVerificationCodeContext helper can inject additional variables like application name or organization branding into the template context.

What happens when a user requests multiple passcodes before using the first one?

The createPasscode implementation automatically deletes existing unconsumed passcodes for the same jti (session identifier) or user identifier before inserting a new record. This ensures only one active passcode exists per session, preventing confusion and reducing database clutter while maintaining security.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →