Implementing Machine-to-Machine (M2M) Authentication with Logto Client Credentials: A Complete Guide
Logto implements the OAuth 2.0 client credentials grant as a first-class OIDC flow, enabling backend services to obtain JWT access tokens via the /oidc/token endpoint and automatically cache them using the ClientCredentials class from @logto/api.
Logto's open-source identity platform (logto-io/logto) provides native support for machine-to-machine (M2M) authentication through the OAuth 2.0 client credentials grant. This implementation allows backend services, CI/CD pipelines, and other non-interactive clients to securely access the Logto Management API or protected resources without user intervention.
Core Architecture
Logto's M2M implementation consists of four primary components that work together to issue, cache, and consume access tokens.
OIDC Provider
The server-side implementation resides in packages/core/src/oidc/grants/client-credentials.ts. This module exposes the token endpoint (/oidc/token) and validates the client_id and client_secret provided by M2M clients. It implements a custom client_credentials grant that also supports organization-scoped tokens, extending standard OAuth 2.0 functionality.
ClientCredentials Class
Located in packages/api/src/client-credentials.ts, this SDK class serves as a thin wrapper around the token endpoint. It handles HTTP requests, parses responses, and implements intelligent caching with just-in-time refresh. The class stores tokens alongside their absolute expiry timestamp (expiresAt) and returns cached tokens until they approach expiration.
Event Listeners
For audit purposes, Logto registers event listeners in packages/core/src/event-listeners/index.ts that track M2M token lifecycle events. The provider emits events via provider.addListener('client_credentials.*', m2mTokenUsageListener), logging token creation, refresh, and revocation activities.
Management API
The Management API implementation in packages/api/src/management.ts demonstrates how protected resources consume these tokens. Endpoints require the management_api scope (or application-specific scopes) encoded in the JWT payload.
How the M2M Flow Works
The authentication sequence follows these steps:
- Register an M2M application in the Logto console to receive a
client_idandclient_secret. - Request a token by calling the
/oidc/tokenendpoint withgrant_type=client_credentials. - Validate credentials – the OIDC provider verifies the secrets and issues an access token (JWT) with
expires_in. - Cache the token – the
ClientCredentialsclass stores the token with its expiry timestamp. - Automatic refresh – subsequent calls to
getAccessToken()return cached tokens if valid, or request fresh tokens when expiry approaches. - Authorize requests – include the token in the
Authorization: Bearer <token>header when calling protected endpoints.
Because tokens are cached in-process, the SDK avoids unnecessary network round-trips while using a configurable accessTokenExpiryLeeway (default 60 seconds) to prevent race conditions in high-throughput services.
Implementing the Client Credentials SDK
Basic Usage
Create a ClientCredentials instance with your application credentials and token endpoint:
import { ClientCredentials } from '@logto/api/client-credentials';
const logto = new ClientCredentials({
clientId: 'YOUR_CLIENT_ID',
clientSecret: 'YOUR_CLIENT_SECRET',
tokenEndpoint: 'https://logto.example.com/oidc/token',
tokenParams: { audience: 'https://api.logto.io' },
});
async function callManagementApi() {
const { value: accessToken } = await logto.getAccessToken();
const response = await fetch('https://logto.example.com/api/users', {
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
});
const users = await response.json();
console.log('User list:', users);
}
callManagementApi().catch(console.error);
The getAccessToken() method automatically refreshes the token when it approaches expiration, eliminating manual token management.
Custom Leeway and Error Handling
Configure custom expiry leeway and handle specific error types:
const logto = new ClientCredentials({
clientId: 'id',
clientSecret: 'secret',
tokenEndpoint: 'https://logto.example.com/oidc/token',
accessTokenExpiryLeeway: 120, // refresh 2 minutes before expiry
});
try {
const token = await logto.getAccessToken();
// use token…
} catch (err) {
if (err instanceof ClientCredentialsError) {
console.error('Failed to obtain M2M token:', err.message);
}
}
The ClientCredentialsError class provides clear error messages when the token endpoint returns non-2xx status codes or unexpected payloads.
Integration with Management API
Use the built-in Management client with your credentials provider:
import { Management } from '@logto/api/management';
const credentials = new ClientCredentials({
clientId: process.env.LOGTO_CLIENT_ID!,
clientSecret: process.env.LOGTO_CLIENT_SECRET!,
tokenEndpoint: 'https://logto.example.com/oidc/token',
});
const management = new Management({
accessTokenProvider: () => credentials.getAccessToken(),
});
async function listApplications() {
const apps = await management.applications.getAll();
console.log(apps);
}
This pattern delegates token management to the ClientCredentials class while the Management client focuses on API operations.
Security Considerations
When implementing M2M authentication with Logto, observe these security practices:
- Scope restriction – Only scopes granted to the M2M client during registration are encoded in the JWT payload. Limit scopes to the minimum required for the service's function.
- Clock skew protection – The default 60-second leeway handles clock drift between client and server. Increase this value in
accessTokenExpiryLeewayif your infrastructure experiences significant time synchronization issues. - Secret storage – Store
client_secretvalues in environment variables or secure secret managers, never in source code. - Token lifecycle monitoring – Review logs emitted by the event listeners in
packages/core/src/event-listeners/index.tsto audit token usage patterns and detect anomalous access.
Summary
- Logto implements M2M authentication via the OAuth 2.0 client credentials grant in
packages/core/src/oidc/grants/client-credentials.ts. - The
ClientCredentialsclass inpackages/api/src/client-credentials.tshandles token caching, automatic refresh, and expiry management with configurable leeway. - Tokens are obtained from the
/oidc/tokenendpoint and must include appropriate scopes (such asmanagement_api) to access protected resources. - The SDK provides
ClientCredentialsErrorfor robust error handling when token requests fail. - Event listeners in the core package enable comprehensive audit logging of token lifecycle events.
Frequently Asked Questions
What is the default token expiry leeway in Logto?
The default accessTokenExpiryLeeway is 60 seconds. This means the ClientCredentials class considers a token expired 60 seconds before its actual expiry timestamp, preventing race conditions where a token might expire in transit during high-latency requests.
How does the Logto SDK handle token refresh?
The ClientCredentials class automatically requests a new token when getAccessToken() is called and the cached token is near expiry (within the configured leeway window). This happens transparently without throwing errors, ensuring continuous service availability while minimizing unnecessary token endpoint calls through in-process caching.
What scopes are required to access the Logto Management API?
The Management API requires tokens carrying the management_api scope (or specific application-defined scopes depending on your resource configuration). These scopes must be granted to the M2M client during application registration in the Logto console; the token endpoint will only issue tokens containing scopes explicitly assigned to the requesting client.
Where is the client credentials grant implemented server-side?
The server-side implementation resides in packages/core/src/oidc/grants/client-credentials.ts. This file extends the standard OIDC provider to support the client_credentials grant type, validates client credentials against the Logto database, and issues signed JWT access tokens with optional organization-specific claims.
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 →