Skip to main content

OAuth Client

The @leanmcp/auth/client module provides a complete OAuth 2.1 client implementation for MCP applications. It handles browser-based authentication flows with PKCE, secure token storage, and automatic token refresh.

Features

OAuth 2.1 with PKCE

Secure authorization code flow with Proof Key for Code Exchange

Token Storage

Pluggable storage backends: memory, file, or OS keychain

Auto Refresh

Automatic token refresh before expiration

Dynamic Registration

RFC 7591 Dynamic Client Registration support

Installation

npm install @leanmcp/auth
For file-based storage with encryption:
npm install @leanmcp/auth
For OS keychain storage:
npm install @leanmcp/auth keytar

Quick Start

import { OAuthClient } from '@leanmcp/auth/client';
import { FileStorage } from '@leanmcp/auth/storage';

// Create client with file-based token storage
const client = new OAuthClient({
  serverUrl: 'https://api.example.com',
  authorizationEndpoint: 'https://api.example.com/oauth/authorize',
  tokenEndpoint: 'https://api.example.com/oauth/token',
  clientId: 'my-app',
  scopes: ['openid', 'profile'],
  storage: new FileStorage({
    filePath: '~/.myapp/tokens.json',
    prettyPrint: true,
  }),
  pkceEnabled: true,
  autoRefresh: true,
});

// Authenticate (opens browser)
const tokens = await client.authenticate();
console.log('Authenticated!', tokens.access_token);

// Get valid token (auto-refreshes if needed)
const token = await client.getValidToken();

// Use token in API calls
const response = await fetch('https://api.example.com/data', {
  headers: { Authorization: `Bearer ${token}` },
});

OAuthClient

The main client class for OAuth 2.1 flows.

Constructor Options

interface OAuthClientOptions {
  /** Base URL of the OAuth server */
  serverUrl: string;
  
  /** Authorization endpoint URL */
  authorizationEndpoint: string;
  
  /** Token endpoint URL */
  tokenEndpoint: string;
  
  /** Token storage backend */
  storage: TokenStorage;
  
  /** Client ID (required if not using dynamic registration) */
  clientId?: string;
  
  /** Client secret (for confidential clients) */
  clientSecret?: string;
  
  /** OAuth scopes to request */
  scopes?: string[];
  
  /** Enable PKCE (default: true) */
  pkceEnabled?: boolean;
  
  /** Automatically refresh tokens before expiry (default: false) */
  autoRefresh?: boolean;
  
  /** Callback URL for browser flow (default: http://localhost:PORT/callback) */
  redirectUri?: string;
  
  /** Port for local callback server (default: random available port) */
  callbackPort?: number;
}

Methods

authenticate()

Initiates the OAuth flow by opening a browser window for user authentication.
const tokens = await client.authenticate();
// Returns: { access_token, token_type, expires_in, refresh_token?, scope? }
Flow:
  1. Generates PKCE code verifier and challenge (if enabled)
  2. Opens browser to authorization endpoint
  3. Starts local HTTP server to receive callback
  4. Exchanges authorization code for tokens
  5. Stores tokens in configured storage

getValidToken()

Returns a valid access token, refreshing if necessary.
const token = await client.getValidToken();
// Returns: string (access token)
If the current token is expired and a refresh token is available, it will automatically refresh. Throws if no valid token is available.

getTokens()

Returns the current stored tokens without refreshing.
const tokens = await client.getTokens();
// Returns: TokenSet | null

logout()

Clears stored tokens.
await client.logout();

Token Storage

The @leanmcp/auth/storage module provides pluggable storage backends for tokens.

MemoryStorage

Stores tokens in memory. Tokens are lost when the process exits.
import { MemoryStorage } from '@leanmcp/auth/storage';

const storage = new MemoryStorage();

const client = new OAuthClient({
  // ...
  storage,
});
Use cases:
  • Development and testing
  • Short-lived CLI commands
  • Serverless functions (tokens passed externally)

FileStorage

Stores tokens in a JSON file with optional encryption.
import { FileStorage } from '@leanmcp/auth/storage';

const storage = new FileStorage({
  /** Path to token file (supports ~ for home directory) */
  filePath: '~/.myapp/tokens.json',
  
  /** Encryption key (optional, enables AES-256-GCM encryption) */
  encryptionKey?: string,
  
  /** Pretty-print JSON (default: false) */
  prettyPrint?: boolean,
});
Example with encryption:
const storage = new FileStorage({
  filePath: '~/.myapp/tokens.json',
  encryptionKey: process.env.TOKEN_ENCRYPTION_KEY,
});
If using encryption, store the encryption key securely (e.g., environment variable). Losing the key means losing access to stored tokens.

KeychainStorage

Stores tokens in the OS secure keychain (macOS Keychain, Windows Credential Manager, Linux Secret Service).
import { KeychainStorage } from '@leanmcp/auth/storage';

const storage = new KeychainStorage({
  /** Service name in keychain */
  service: 'my-app',
  
  /** Account name in keychain */
  account: 'oauth-tokens',
});
Requires the keytar package: npm install keytar
Use cases:
  • Desktop CLI applications
  • Developer tools
  • Any application where OS-level security is preferred

Custom Storage

Implement the TokenStorage interface for custom backends:
import type { TokenStorage, TokenSet } from '@leanmcp/auth/storage';

class RedisStorage implements TokenStorage {
  constructor(private redis: RedisClient, private key: string) {}
  
  async get(): Promise<TokenSet | null> {
    const data = await this.redis.get(this.key);
    return data ? JSON.parse(data) : null;
  }
  
  async set(tokens: TokenSet): Promise<void> {
    await this.redis.set(this.key, JSON.stringify(tokens));
  }
  
  async clear(): Promise<void> {
    await this.redis.del(this.key);
  }
}

PKCE Flow

PKCE (Proof Key for Code Exchange) is enabled by default and required by the MCP OAuth specification. The client automatically:
  1. Generates a cryptographically random code_verifier
  2. Creates the code_challenge using SHA-256
  3. Sends the challenge with the authorization request
  4. Sends the verifier with the token exchange
// PKCE is enabled by default
const client = new OAuthClient({
  serverUrl: 'https://api.example.com',
  // ...
  pkceEnabled: true, // default
});

Token Refresh

Automatic Refresh

When autoRefresh is enabled, getValidToken() automatically refreshes expired tokens:
const client = new OAuthClient({
  // ...
  autoRefresh: true,
});

// Always returns a valid token
const token = await client.getValidToken();

Manual Refresh

You can also manually refresh tokens:
const newTokens = await client.refreshTokens();

Complete Example

Here’s a complete CLI application that authenticates with an OAuth server:
import { OAuthClient } from '@leanmcp/auth/client';
import { FileStorage } from '@leanmcp/auth/storage';

const SERVER_URL = 'https://api.example.com';

async function main() {
  const storage = new FileStorage({
    filePath: '~/.myapp/tokens.json',
    prettyPrint: true,
  });

  const client = new OAuthClient({
    serverUrl: SERVER_URL,
    authorizationEndpoint: `${SERVER_URL}/oauth/authorize`,
    tokenEndpoint: `${SERVER_URL}/oauth/token`,
    storage,
    clientId: 'my-cli-app',
    scopes: ['openid', 'profile', 'read:data'],
    pkceEnabled: true,
    autoRefresh: true,
  });

  // Check for existing tokens
  const existing = await client.getTokens();
  if (existing?.access_token) {
    console.log('Using existing session');
  } else {
    console.log('Opening browser for authentication...');
    await client.authenticate();
    console.log('Authenticated!');
  }

  // Make authenticated API call
  const token = await client.getValidToken();
  const response = await fetch(`${SERVER_URL}/api/user`, {
    headers: { Authorization: `Bearer ${token}` },
  });
  
  const user = await response.json();
  console.log('User:', user);
}

main().catch(console.error);

API Reference

TokenSet

interface TokenSet {
  access_token: string;
  token_type: string;
  expires_in?: number;
  refresh_token?: string;
  scope?: string;
  expires_at?: number; // Unix timestamp
}

TokenStorage Interface

interface TokenStorage {
  get(): Promise<TokenSet | null>;
  set(tokens: TokenSet): Promise<void>;
  clear(): Promise<void>;
}