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 samejti(session identifier) or user identifier.sendPasscode– Routes the generated code to the appropriate message connector (Email or SMS) based on whether the payload contains anemailorphonefield.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 withtryCountinitialized 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 invokingcreatePasscodeandsendPasscode.POST /verification-codes/verify– Validates user input by callingverifyPasscode.
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:
- Expiration validation – Compares
passcode.createdAt + expirationMsagainstDate.now()to reject stale codes. - Retry limiting – Validates that
passcode.tryCountremains belowmaxTryCountto prevent brute-force attacks. - 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.tsand providescreatePasscode,sendPasscode, andverifyPasscodefunctions. - Database operations in
packages/core/src/queries/passcode.tshandle atomic updates for try counts, consumption status, and cleanup. - Management API endpoints at
/verification-codesand/verification-codes/verifyexpose 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
VerificationCodeReact component and supports full localization viapackages/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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →