# How Craft Agents Handles OAuth Authentication and Token Management

> Discover how Craft Agents handles OAuth authentication and token management with automatic discovery, PKCE flows, and proactive refresh for secure API connections.

- Repository: [Craft Ai Agents/craft-agents-oss](https://github.com/craft-ai-agents/craft-agents-oss)
- Tags: how-to-guide
- Published: 2026-07-04

---

**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`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/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`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/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`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/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`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/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`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/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`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/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

```typescript
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

```typescript
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

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

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

```

## Summary

- **Discovery**: `discoverOAuthMetadata` in [`oauth.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/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`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/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`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/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`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/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.