How Craft Agents Handles OAuth Authentication and Token Management

Craft Agents implements OAuth 2.0 with automatic token discovery, PKCE-based authorization flows, and proactive refresh management to securely connect both MCP servers and external API sources like Google, Slack, and Microsoft.

Craft Agents authentication is built around a standards-compliant OAuth 2.0 implementation that handles everything from server discovery to automatic token refresh. According to the craft-ai-agents/craft-agents-oss source code, the system uses a three-layer architecture spanning discovery utilities, credential managers, and source-specific type guards to provide seamless, secure access to protected resources.

OAuth Discovery and Client Registration

When connecting to an MCP server, Craft Agents first determines whether the server requires OAuth through progressive discovery defined in packages/shared/src/auth/oauth.ts.

Progressive Metadata Discovery

The discoverOAuthMetadata function implements RFC 8414 and RFC 9728 to locate OAuth server metadata:

  1. Sends a request to the MCP URL expecting a 401 Unauthorized with a WWW-Authenticate header
  2. Parses the resource_metadata hint and fetches protected-resource metadata to extract the authorization_servers array
  3. Falls back to well-known locations such as /.well-known/oauth-authorization-server

If discovery succeeds, the function returns an OAuthMetadata object containing authorization_endpoint, token_endpoint, and optional registration_endpoint.

Public Client Registration

If the server exposes a registration_endpoint, Craft Agents registers a public client (no client secret) using a random redirect URI pointing to a temporary local HTTP server. The registerClient method inside CraftOAuth handles this registration, while registerMcpOAuthClient provides a reusable helper for MCP-specific flows.

If registration is denied (HTTP 401/403), the system falls back to a hard-coded client ID 'craft-agent'.

The OAuth Authorization Flow

The actual authentication flow is orchestrated by CraftOAuth.authenticate() in packages/shared/src/auth/oauth.ts, which implements the full PKCE authorization code flow.

PKCE and Local Callback Server

The flow generates PKCE parameters and cryptographic state, then starts a local callback server:

  • Port range: 8914-8924
  • Callback path: /oauth/callback
  • Security: Server listens only for the exact path and uses fetchWithTimeout with a 5-second deadline protected by SSRF guards (isUrlSafeToFetch)

The authorization URL is built with response_type=code, the client ID, redirect URI, PKCE challenge, and random state, then opened in the user's browser via openUrl.

Token Exchange

Upon receiving the authorization code at the callback server, the system exchanges it for tokens via exchangeCodeForTokens. The resulting OAuthTokens object contains the access token, optional refresh token, and expiry timestamp.

Token Storage and Security

Once obtained, tokens are persisted by the SourceCredentialManager implementation in packages/shared/src/sources/credential-manager.ts.

Credential Structure

Stored credentials include:

  • value: The access token
  • refreshToken: Optional refresh token for long-lived access
  • expiresAt: Calculated as Date.now() + expires_in × 1000

Token Masking

For security, tokens are masked in logs using maskToken from packages/shared/src/utils/mask.ts, which displays only the first 3 and last 3 characters of the token string.

Automatic Token Refresh

Craft Agents eliminates manual token maintenance through the TokenRefreshManager in packages/shared/src/sources/token-refresh-manager.ts.

Proactive Refresh Logic

The manager wraps each OAuth-enabled source and:

  • Checks if tokens are expired or will expire within 5 minutes (needsRefresh)
  • Calls refresh() to obtain fresh tokens (ensureFreshToken)
  • Updates in-memory state via markSourceAuthenticated so subsequent API calls use valid credentials

Rate Limiting

To prevent hammering authentication servers, the system implements a cooldown mechanism. After a failed refresh, the source enters a DEFAULT_COOLDOWN_MS period of 5 minutes before retry attempts.

Source Integration

The system distinguishes between MCP and API sources using type guards defined in packages/shared/src/sources/types.ts.

MCP vs. API Sources

  • MCP sources: Require authType: 'oauth' in their configuration
  • API sources: Support OAuth when provider matches API_OAUTH_PROVIDERS ('google' | 'microsoft' | 'slack')

Service Detection

Helper functions like inferGoogleServiceFromUrl, inferSlackServiceFromUrl, and inferMicrosoftServiceFromUrl automatically derive service-specific scopes from API base URLs, ensuring the correct OAuth scopes are requested without manual configuration.

Code Examples

Initiating an MCP OAuth Flow

import { CraftOAuth } from '@craft-agents/shared/auth/oauth';
import type { OAuthCallbacks } from '@craft-agents/shared/auth/oauth';

const callbacks: OAuthCallbacks = {
  onStatus: (msg) => console.log('[OAuth] ', msg),
  onError:  (err) => console.error('[OAuth] ', err),
};

const oauth = new CraftOAuth(
  { mcpUrl: 'https://mcp.craft.do/my/mcp' },
  callbacks
);

async function login() {
  const { tokens, clientId } = await oauth.authenticate();
  console.log('Access token:', tokens.accessToken);
  console.log('Refresh token:', tokens.refreshToken);
  console.log('Client ID:', clientId);
}
login();

Refreshing Tokens On Demand

import { TokenRefreshManager, createTokenGetter } from '@craft-agents/shared/sources/token-refresh-manager';

const refreshMgr = new TokenRefreshManager(credentialManager);
const getToken = createTokenGetter(refreshMgr, source);

async function callGoogle() {
  const token = await getToken(); // Automatically refreshed if needed
  const resp = await fetch('https://gmail.googleapis.com/gmail/v1/users/me/profile', {
    headers: { Authorization: `Bearer ${token}` },
  });
  console.log(await resp.json());
}

Detecting OAuth Sources

import { isOAuthSource } from '@craft-agents/shared/sources/types';

if (isOAuthSource(source)) {
  console.log('This source requires OAuth authentication.');
}

Summary

  • Discovery: discoverOAuthMetadata in oauth.ts implements RFC 8414/RFC 9728 for progressive OAuth server discovery with fallback to well-known endpoints.
  • Flow: CraftOAuth.authenticate() handles PKCE generation, local callback server creation (ports 8914-8924), and token exchange with 5-second timeouts.
  • Storage: Tokens are persisted with expiry timestamps in the credential manager and masked in logs via maskToken.
  • Refresh: TokenRefreshManager proactively refreshes tokens within 5 minutes of expiry and implements 5-minute cooldowns on failure.
  • Integration: Type guards in types.ts distinguish MCP OAuth sources from API providers (Google, Microsoft, Slack) with automatic service inference.

Frequently Asked Questions

How does Craft Agents discover OAuth servers automatically?

Craft Agents uses the discoverOAuthMetadata function to implement progressive discovery per RFC 8414 and RFC 9728. It first attempts to parse a WWW-Authenticate header from a 401 Unauthorized response, then falls back to well-known OAuth metadata endpoints. This allows the system to connect to arbitrary MCP servers without hard-coded configuration.

What happens when an OAuth token expires during active use?

The TokenRefreshManager monitors token expiry and automatically refreshes credentials before they expire (within a 5-minute window) or immediately upon detecting an expired token. When making API calls, developers use createTokenGetter which returns a function that always yields a fresh, valid token without manual intervention.

Does Craft Agents support OAuth for services other than Google, Microsoft, and Slack?

Yes. While the system provides built-in support for Google, Microsoft, and Slack through API_OAUTH_PROVIDERS, any service supporting OAuth 2.0 can be configured by setting authType: 'oauth' and providing an oauth object with authorization_endpoint and token_endpoint in the source configuration. The generic OAuth implementation in oauth.ts handles the PKCE flow for any compliant provider.

How does the system protect OAuth tokens in logs and memory?

Tokens are masked using maskToken from packages/shared/src/utils/mask.ts before logging, displaying only the first three and last three characters. Additionally, the local callback server runs only during active authentication (ports 8914-8924) and validates the exact callback path /oauth/callback to prevent token interception.

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 →