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:

  1. Register an M2M application in the Logto console to receive a client_id and client_secret.
  2. Request a token by calling the /oidc/token endpoint with grant_type=client_credentials.
  3. Validate credentials – the OIDC provider verifies the secrets and issues an access token (JWT) with expires_in.
  4. Cache the token – the ClientCredentials class stores the token with its expiry timestamp.
  5. Automatic refresh – subsequent calls to getAccessToken() return cached tokens if valid, or request fresh tokens when expiry approaches.
  6. 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 accessTokenExpiryLeeway if your infrastructure experiences significant time synchronization issues.
  • Secret storage – Store client_secret values 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.ts to 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 ClientCredentials class in packages/api/src/client-credentials.ts handles token caching, automatic refresh, and expiry management with configurable leeway.
  • Tokens are obtained from the /oidc/token endpoint and must include appropriate scopes (such as management_api) to access protected resources.
  • The SDK provides ClientCredentialsError for 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:

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 →