# ADR-001: No Refresh Tokens in browserAuth **Status:** Accepted **Date:** 2025-01-25 **Tags:** oauth, mcp, security ## Problem * Should `browserAuth` handle OAuth refresh tokens and automatic token renewal? ## Decision * `browserAuth` intentionally does not request or handle refresh tokens. * When tokens expire, `tokens()` returns `undefined`, signaling the MCP SDK to re-authenticate. * The SDK's built-in retry logic handles re-auth transparently. Rationale: * **MCP SDK lifecycle**: The SDK expects auth providers to return `undefined` for expired tokens, triggering its standard re-auth flow. * **CLI/desktop UX**: Interactive re-consent is acceptable and often preferred over silent background refresh. * **Simplicity**: Avoiding refresh eliminates token rotation complexity, race conditions, and concurrent refresh handling. * **Security**: No long-lived refresh tokens stored; each session requires explicit user consent. ## Alternatives (brief) * **Implement refresh flow** — Adds complexity (token rotation, concurrency), conflicts with SDK's re-auth expectations, stores long-lived credentials. * **Optional refresh via config** — Increases API surface, creates two divergent code paths to maintain. ## Impact * Positive: Simpler implementation, predictable behavior, aligns with MCP SDK design. * Negative/Risks: More frequent browser prompts for long-running sessions (acceptable for CLI tools). ## Links * Code: `src/auth/browser-auth.ts` * Related: MCP SDK auth interface in `@modelcontextprotocol/sdk/client/auth` --- --- url: /oauth-callback/adr/002-immediate-token-exchange.md --- # ADR-002: Immediate Token Exchange in redirectToAuthorization() **Status:** Accepted **Date:** 2025-01-25 **Tags:** oauth, mcp, sdk-integration ## Problem The MCP SDK's `auth()` flow works as follows: 1. Check `provider.tokens()` — if valid tokens exist, return `'AUTHORIZED'` 2. If no tokens, start authorization: call `redirectToAuthorization(url)` 3. **Immediately** return `'REDIRECT'` (without re-checking tokens) For web-based OAuth, this makes sense: `redirectToAuthorization()` triggers a page redirect and control never returns synchronously. The SDK expects authentication to complete in a subsequent request. For CLI/desktop apps using `browserAuth()`, control **does** return synchronously—we capture the callback in-process via a local HTTP server. We exchange tokens inside `redirectToAuthorization()`, but the SDK has already decided to return `'REDIRECT'`, causing `UnauthorizedError`. ## Decision Exchange tokens **inside** `redirectToAuthorization()` and document the retry pattern as the expected usage: ```typescript // First connect triggers OAuth flow and saves tokens, but SDK returns // 'REDIRECT' before checking. Second connect finds valid tokens. async function connectWithOAuthRetry(client, serverUrl, authProvider) { const transport = new StreamableHTTPClientTransport(serverUrl, { authProvider, }); try { await client.connect(transport); } catch (error) { if (error.message === "Unauthorized") { await client.connect( new StreamableHTTPClientTransport(serverUrl, { authProvider }), ); } else throw error; } } ``` **Why a new transport on retry?** The transport caches connection state internally. A fresh transport ensures clean reconnection. ## Rationale * **SDK constraint**: No hook exists between redirect completion and the `'REDIRECT'` return. The SDK interface (`Promise`) cannot signal "auth completed." * **In-process capture**: CLI apps don't have page redirects that would trigger a fresh auth check cycle. * **Correctness over elegance**: The retry is unusual but reliable—tokens are always saved before the error. ## Alternatives Considered | Alternative | Why Rejected | | ---------------------------------------------- | ------------------------------------------------------------- | | `transport.finishAuth(callbackUrl)` | Breaks provider encapsulation; doesn't fit in-process capture | | Return tokens from `redirectToAuthorization()` | SDK interface expects `Promise` | | Upstream SDK change | Not viable for library consumers | ## Impact * **Positive**: Self-contained auth flow; no external coordination needed * **Negative**: First connection always throws `UnauthorizedError` after OAuth—must be documented clearly ## Links * Code: `src/auth/browser-auth.ts` lines 254-368 * MCP SDK auth interface: `@modelcontextprotocol/sdk/client/auth.js` * Related: ADR-001 (no refresh tokens) --- --- url: /oauth-callback/adr/003-stable-client-metadata.md --- # ADR-003: Stable Client Metadata Across DCR **Status:** Accepted **Date:** 2025-01-25 **Tags:** oauth, dcr, security ## Problem * During Dynamic Client Registration (DCR), the authorization server may return different capabilities than requested (e.g., `token_endpoint_auth_method`). * If `clientMetadata` adapts to DCR responses, subsequent token requests may fail when the AS caches the original registration metadata. ## Decision * `clientMetadata` is immutable after construction. * `token_endpoint_auth_method` is determined at construction: `client_secret_post` if `clientSecret` is provided, `none` otherwise. DCR responses never change this value. * DCR credentials (`client_id`, `client_secret`) are stored separately and never mutate the auth method. ## Alternatives (brief) * **Dynamic metadata evolution** — Adapting to DCR responses seems flexible but causes cache mismatches with AS that remember original registration. * **Per-request method detection** — Adds complexity and non-deterministic behavior across retries. ## Impact * Positive: Predictable behavior with all AS implementations; eliminates cache-related auth failures. * Negative/Risks: None identified; the fixed method (`client_secret_post`) has universal support. ## Links * Code: `src/auth/browser-auth.ts` * Related ADRs: ADR-001 (No Refresh Tokens), ADR-002 (Immediate Token Exchange) --- --- url: /oauth-callback/adr/004-conditional-state-validation.md --- # ADR-004: Conditional OAuth State Validation **Status:** Accepted **Date:** 2025-01-25 **Tags:** oauth, security, csrf ## Problem * RFC 6749 recommends `state` for CSRF protection, but RFC 8252 (native apps) relies on loopback redirect for security. * Some authorization servers don't echo `state` back; others require it. * Strict validation breaks compatibility; no validation weakens security. ## Decision * Validate `state` only if it was present in the authorization URL. * If the auth URL includes `state` and the callback doesn't match, reject as CSRF. * If the auth URL omits `state`, accept callbacks without state validation. Rationale: * **Defense-in-depth**: Loopback binding (127.0.0.1) prevents network CSRF, but state adds protection against local attacks (malicious apps, browser extensions intercepting localhost). * **CLI threat model**: Unlike web apps, CLI tools face local machine threats—other processes can probe localhost ports. State validation detects if a callback arrives from an unrelated auth flow. * **Compatibility**: Authorization servers have inconsistent state handling. Conditional validation works with all servers while providing protection when available. ## Alternatives (brief) * **Always require state** — Breaks servers that don't echo state or don't support it. * **Never validate state** — Loopback provides baseline security, but ignores state when the AS cooperates. * **Generate state internally always** — Conflicts with auth URLs that already include state from the MCP SDK. ## Impact * Positive: Maximum security when AS supports state; universal compatibility otherwise. * Negative/Risks: If an AS echoes arbitrary state values without validation, the protection is weaker (rare edge case). ## Links * Code: `src/auth/browser-auth.ts:297-300` * RFC 6749 Section 10.12 (CSRF Protection) * RFC 8252 Section 8.1 (Loopback Redirect) --- --- url: /oauth-callback/adr/005-store-responsibility-reduction.md --- # ADR-005: OAuthStore Responsibility Reduction **Status:** Accepted **Date:** 2025-01-25 **Tags:** api, storage, simplification ## Problem The store was accumulating OAuth flow state (session, nonce, state parameter) alongside persistent data (tokens, client registration). This blurred the line between "what survives a crash" and "what's ephemeral by design," making the API harder to reason about and test. ## Decision The store is responsible **only** for data that must survive process restarts: | Stored | Not Stored | | --------------------- | ----------------- | | `tokens` | `state` parameter | | `client` (DCR result) | `nonce` | | `codeVerifier` (PKCE) | session objects | The `codeVerifier` is the sole flow artifact persisted—it enables completing an in-progress authorization if the process crashes after browser launch but before callback. ## Alternatives (brief) * **Full session persistence** — Would enable crash-recovery at any point, but adds complexity for a rare edge case. Users can simply restart the flow. * **No verifier persistence** — Simpler, but loses the most common crash scenario (user switches apps, process dies). ## Impact * Positive: Cleaner mental model; store implementations are trivial to write and test. * Negative: If the process crashes before `codeVerifier` is saved, the flow must restart. This is acceptable—it's a sub-second window. ## Links * Code: `src/storage/`, `src/mcp-types.ts` * Related: ADR-002 (Immediate Token Exchange) --- --- url: /oauth-callback/adr/000-template.md --- # ADR-NNN Title **Status:** Proposed | Accepted | Deprecated | Superseded\ **Date:** YYYY-MM-DD\ **Tags:** tag1, tag2 ## Problem * One or two sentences on the decision trigger or constraint. ## Decision * The chosen approach in a short paragraph. ## Alternatives (brief) * Option A — why not. * Option B — why not. ## Impact * Positive: * Negative/Risks: ## Links * Code/Docs: * Related ADRs: --- --- url: /oauth-callback/api.md description: >- Complete API documentation for OAuth Callback library functions, types, and interfaces. --- # API Reference OAuth Callback provides a comprehensive set of APIs for handling OAuth 2.0 authorization flows in CLI tools, desktop applications, and Model Context Protocol (MCP) clients. The library is designed with modern Web Standards APIs and works across Node.js 18+, Deno, and Bun. ## Quick Navigation ### Core Functions * [**getAuthCode**](/api/get-auth-code) - Capture OAuth authorization codes via localhost callback * [**browserAuth**](/api/browser-auth) - MCP SDK-compatible OAuth provider with DCR support ### Storage Providers * [**Storage Providers**](/api/storage-providers) - Token persistence interfaces and implementations * **inMemoryStore** - Ephemeral in-memory token storage * **fileStore** - Persistent file-based token storage ### Errors * [**OAuthError**](/api/oauth-error) - OAuth-specific error class with RFC 6749 compliance ### Type Definitions * [**Types**](/api/types) - Complete TypeScript type reference ## Import Methods OAuth Callback supports multiple import patterns to suit different use cases: ### Main Package Import ```typescript import open from "open"; // Core functionality import { getAuthCode, OAuthError } from "oauth-callback"; // Namespace import for MCP features import { mcp } from "oauth-callback"; const authProvider = mcp.browserAuth({ launch: open, store: mcp.fileStore() }); ``` ### MCP-Specific Import ```typescript // Direct MCP imports (recommended for MCP projects) import { browserAuth, fileStore, inMemoryStore } from "oauth-callback/mcp"; import type { TokenStore, OAuthStore, Tokens } from "oauth-callback/mcp"; ``` ## Core APIs ### getAuthCode(input) The primary function for capturing OAuth authorization codes through a localhost callback server. ```typescript function getAuthCode( input: string | GetAuthCodeOptions, ): Promise; ``` **Key Features:** * Automatic browser opening * Configurable port and timeout * Custom HTML templates * AbortSignal support * Comprehensive error handling [**Full Documentation →**](/api/get-auth-code) ### browserAuth(options) MCP SDK-compatible OAuth provider that handles the complete OAuth flow including Dynamic Client Registration. ```typescript function browserAuth(options?: BrowserAuthOptions): OAuthClientProvider; ``` **Key Features:** * Dynamic Client Registration (RFC 7591) * Automatic token expiry handling * PKCE support (RFC 7636) * Flexible token storage * MCP SDK integration [**Full Documentation →**](/api/browser-auth) ## Storage APIs ### Storage Interfaces OAuth Callback provides two storage interfaces for different levels of state management: ```typescript // Basic token storage interface TokenStore { get(key: string): Promise; set(key: string, tokens: Tokens): Promise; delete(key: string): Promise; } // Extended storage with DCR and PKCE support interface OAuthStore extends TokenStore { getClient(key: string): Promise; setClient(key: string, client: ClientInfo): Promise; deleteClient(key: string): Promise; getCodeVerifier(key: string): Promise; setCodeVerifier(key: string, verifier: string): Promise; deleteCodeVerifier(key: string): Promise; } ``` [**Full Documentation →**](/api/storage-providers) ### Built-in Implementations #### inMemoryStore() Ephemeral storage that keeps tokens in memory: ```typescript const authProvider = browserAuth({ launch: open, store: inMemoryStore(), // Tokens lost on restart }); ``` #### fileStore(filepath?) Persistent storage that saves tokens to a JSON file: ```typescript const authProvider = browserAuth({ launch: open, store: fileStore(), // Default: ~/.mcp/tokens.json }); ``` ## Error Handling ### OAuthError Specialized error class for OAuth-specific failures: ```typescript class OAuthError extends Error { error: string; // OAuth error code error_description?: string; // Human-readable description error_uri?: string; // URI with more information } ``` **Common Error Codes:** * `access_denied` - User denied authorization * `invalid_scope` - Requested scope is invalid * `server_error` - Authorization server error * `temporarily_unavailable` - Service temporarily down [**Full Documentation →**](/api/oauth-error) ## Type System OAuth Callback is fully typed with TypeScript, providing comprehensive type definitions for all APIs: ### Core Types ```typescript interface GetAuthCodeOptions { authorizationUrl: string; port?: number; hostname?: string; callbackPath?: string; timeout?: number; launch?: (url: string) => unknown; successHtml?: string; errorHtml?: string; signal?: AbortSignal; onRequest?: (req: Request) => void; } interface CallbackResult { code: string; state?: string; [key: string]: any; } ``` ### Storage Types ```typescript interface Tokens { accessToken: string; refreshToken?: string; expiresAt?: number; scope?: string; } interface ClientInfo { clientId: string; clientSecret?: string; clientIdIssuedAt?: number; clientSecretExpiresAt?: number; } ``` [**Full Type Reference →**](/api/types) ## Usage Patterns ### Simple OAuth Flow ```typescript import { getAuthCode } from "oauth-callback"; const result = await getAuthCode( "https://github.com/login/oauth/authorize?client_id=xxx", ); console.log("Code:", result.code); ``` ### MCP Integration ```typescript import open from "open"; import { browserAuth, fileStore } from "oauth-callback/mcp"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; const authProvider = browserAuth({ launch: open, store: fileStore(), scope: "read write", }); const transport = new StreamableHTTPClientTransport( new URL("https://mcp.example.com"), { authProvider }, ); ``` ### Handling Errors ```typescript import { getAuthCode, OAuthError } from "oauth-callback"; try { const result = await getAuthCode(authUrl); } catch (error) { if (error instanceof OAuthError) { console.error(`OAuth error: ${error.error}`); console.error(`Details: ${error.error_description}`); } } ``` ### Custom Storage ```typescript import { TokenStore, Tokens } from "oauth-callback/mcp"; class RedisStore implements TokenStore { async get(key: string): Promise { // Implementation } async set(key: string, tokens: Tokens): Promise { // Implementation } // ... other methods } const authProvider = browserAuth({ launch: open, store: new RedisStore(), }); ``` ## Security Considerations ### Built-in Security Features * **PKCE by default** - Proof Key for Code Exchange enabled * **State validation** - Automatic CSRF protection * **Localhost-only binding** - Server only accepts local connections * **Automatic cleanup** - Server shuts down after callback * **Secure file permissions** - Mode 0600 for file storage ### Best Practices ```typescript // Always validate state for CSRF protection const state = crypto.randomUUID(); const authUrl = `https://example.com/authorize?state=${state}&...`; const result = await getAuthCode(authUrl); if (result.state !== state) { throw new Error("State mismatch - possible CSRF attack"); } // Use ephemeral storage for maximum security const authProvider = browserAuth({ launch: open, store: inMemoryStore(), // No disk persistence }); // Implement PKCE for public clients const verifier = randomBytes(32).toString("base64url"); const challenge = createHash("sha256").update(verifier).digest("base64url"); ``` ## Platform Support ### Runtime Compatibility | Runtime | Minimum Version | Status | | ------- | --------------- | ------------------ | | Node.js | 18.0.0 | ✅ Fully supported | | Deno | 1.0.0 | ✅ Fully supported | | Bun | 1.0.0 | ✅ Fully supported | ### OAuth Provider Compatibility OAuth Callback works with any OAuth 2.0 provider that supports the authorization code flow: * ✅ GitHub * ✅ Google * ✅ Microsoft * ✅ Notion (with DCR) * ✅ Linear * ✅ Any RFC 6749 compliant provider ### Browser Compatibility The library opens the user's default browser for authorization. All modern browsers are supported: * ✅ Chrome/Chromium * ✅ Firefox * ✅ Safari * ✅ Edge * ✅ Any browser that handles `http://localhost` URLs ## Advanced Features ### Dynamic Client Registration Automatically register OAuth clients without pre-configuration: ```typescript // No client_id or client_secret needed! const authProvider = browserAuth({ launch: open, scope: "read write", store: fileStore(), }); ``` ### Multi-Environment Support ```typescript function createAuthProvider(env: "dev" | "staging" | "prod") { const configs = { dev: { launch: open, port: 3000, store: inMemoryStore() }, staging: { launch: open, port: 3001, store: fileStore("~/.mcp/staging.json"), }, prod: { launch: open, port: 3002, store: fileStore("~/.mcp/prod.json") }, }; return browserAuth(configs[env]); } ``` ### Request Logging ```typescript const authProvider = browserAuth({ launch: open, onRequest: (req) => { const url = new URL(req.url); console.log(`[OAuth] ${req.method} ${url.pathname}`); }, }); ``` ## Migration Guides ### From Manual OAuth Implementation ```typescript // Before: Manual OAuth flow const server = http.createServer(); server.listen(3000); // ... complex callback handling ... // After: Using OAuth Callback const result = await getAuthCode(authUrl); ``` ### To MCP Integration ```typescript // Before: Custom OAuth provider class CustomOAuthProvider { /* ... */ } // After: Using browserAuth const authProvider = browserAuth({ launch: open, store: fileStore() }); ``` ## API Stability | API | Status | Since | Notes | | ---------------- | ------ | ------ | ----------------------------- | | `getAuthCode` | Stable | v1.0.0 | Core API, backward compatible | | `getRedirectUrl` | Stable | v1.0.0 | Redirect URI helper | | `OAuthError` | Stable | v1.0.0 | OAuth-specific errors | | `TimeoutError` | Stable | v1.0.0 | Timeout error class | | `mcp` | Stable | v2.0.0 | MCP namespace export | | `browserAuth` | Stable | v2.0.0 | MCP integration | | `inMemoryStore` | Stable | v2.0.0 | Storage provider | | `fileStore` | Stable | v2.0.0 | Storage provider | | Types | Stable | v1.0.0 | TypeScript definitions | ## Related Resources * [Core Concepts](/core-concepts) - Architecture and design patterns * [Getting Started](/getting-started) - Quick start guide * [GitHub Repository](https://github.com/kriasoft/oauth-callback) - Source code and issues --- --- url: /oauth-callback/adr.md --- # Architecture Decision Records Key design decisions with context and rationale. | ADR | Decision | | ---------------------------------------------- | ----------------------------------------------------- | | [001](./001-no-refresh-tokens.md) | No refresh tokens—rely on MCP SDK's re-auth flow | | [002](./002-immediate-token-exchange.md) | Token exchange inside `redirectToAuthorization()` | | [003](./003-stable-client-metadata.md) | Immutable client metadata across DCR | | [004](./004-conditional-state-validation.md) | Validate `state` only when present in auth URL | | [005](./005-store-responsibility-reduction.md) | Store persists only tokens, client, and PKCE verifier | --- --- url: /oauth-callback/api/browser-auth.md description: >- MCP SDK-compatible OAuth provider for browser-based authentication flows with Dynamic Client Registration support. --- # browserAuth The `browserAuth` function creates an OAuth provider that integrates seamlessly with the Model Context Protocol (MCP) SDK. It handles the entire OAuth flow including Dynamic Client Registration and token storage through a browser-based authorization flow. Expired tokens trigger re-authentication; refresh tokens are not used. ## Function Signature ```typescript function browserAuth(options?: BrowserAuthOptions): OAuthClientProvider; ``` ## Parameters ### BrowserAuthOptions | Property | Type | Default | Description | | --------------- | -------------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `clientId` | `string` | *none* | Pre-registered OAuth client ID | | `clientSecret` | `string` | *none* | Pre-registered OAuth client secret | | `scope` | `string` | *none* | OAuth scopes to request. When omitted, the auth server uses its default scope. | | `port` | `number` | `3000` | Port for local callback server | | `hostname` | `string` | `"localhost"` | Hostname to bind server to | | `callbackPath` | `string` | `"/callback"` | URL path for OAuth callback | | `store` | `TokenStore` | `inMemoryStore()` | Token storage implementation | | `storeKey` | `string` | `"mcp-tokens"` | Storage key for token isolation | | `launch` | `(url: string) => unknown` | *none* | Callback to launch auth URL | | `authTimeout` | `number` | `300000` | Auth timeout in ms (5 min) | | `successHtml` | `string` | *built-in* | Custom success page HTML | | `errorHtml` | `string` | *built-in* | Custom error page HTML | | `onRequest` | `(req: Request) => void` | *none* | Request logging callback | | `authServerUrl` | `string \| URL` | *auto* | Base URL for OAuth metadata discovery. Defaults to the authorization URL's origin. Set this when the token endpoint is on a different origin. | ## Return Value Returns an `OAuthClientProvider` instance that implements the MCP SDK authentication interface: ```typescript interface OAuthClientProvider { // Completes full OAuth flow: browser → callback → token exchange → persist redirectToAuthorization(authorizationUrl: URL): Promise; // Token storage tokens(): Promise; saveTokens(tokens: OAuthTokens): Promise; // Dynamic Client Registration support clientInformation(): Promise; saveClientInformation(info: OAuthClientInformationFull): Promise; // PKCE support codeVerifier(): Promise; saveCodeVerifier(verifier: string): Promise; // State management state(): Promise; invalidateCredentials( scope: "all" | "client" | "tokens" | "verifier", ): Promise; } ``` ## Basic Usage ### Simple MCP Client The simplest usage with default settings: ```typescript import open from "open"; import { browserAuth } from "oauth-callback/mcp"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; // Create OAuth provider - pass open to launch browser const authProvider = browserAuth({ launch: open }); // Use with MCP transport const transport = new StreamableHTTPClientTransport( new URL("https://mcp.example.com"), { authProvider }, ); const client = new Client( { name: "my-app", version: "1.0.0" }, { capabilities: {} }, ); await client.connect(transport); ``` ### With Token Persistence Store tokens across sessions: ```typescript import open from "open"; import { browserAuth, fileStore } from "oauth-callback/mcp"; const authProvider = browserAuth({ launch: open, store: fileStore(), // Persists to ~/.mcp/tokens.json scope: "read write", }); ``` ## Advanced Usage ### Pre-Registered OAuth Clients If you have pre-registered OAuth credentials: ```typescript import open from "open"; const authProvider = browserAuth({ clientId: process.env.OAUTH_CLIENT_ID, clientSecret: process.env.OAUTH_CLIENT_SECRET, scope: "read write admin", launch: open, store: fileStore(), }); ``` ### Custom Storage Location Store tokens in a specific location: ```typescript import open from "open"; import { browserAuth, fileStore } from "oauth-callback/mcp"; const authProvider = browserAuth({ launch: open, store: fileStore("/path/to/my-tokens.json"), storeKey: "my-app-production", // Namespace for multiple environments }); ``` ### Custom Port and Callback Path Configure the callback server: ```typescript import open from "open"; const authProvider = browserAuth({ port: 8080, hostname: "127.0.0.1", callbackPath: "/oauth/callback", launch: open, store: fileStore(), }); ``` ::: warning Redirect URI Configuration Ensure your OAuth app's redirect URI matches your configuration: * Configuration: `port: 8080`, `callbackPath: "/oauth/callback"` * Redirect URI: `http://localhost:8080/oauth/callback` ::: ### Custom HTML Pages Provide branded callback pages: ```typescript import open from "open"; const authProvider = browserAuth({ launch: open, successHtml: ` Success!

🎉 Success!

Authorization complete. You can close this window.

`, errorHtml: `

Authorization Failed

Error:

`, }); ``` ### Request Logging Monitor OAuth flow for debugging: ```typescript import open from "open"; const authProvider = browserAuth({ launch: open, onRequest: (req) => { const url = new URL(req.url); console.log(`[OAuth] ${req.method} ${url.pathname}`); if (url.pathname === "/callback") { console.log("[OAuth] Callback params:", url.searchParams.toString()); } }, store: fileStore(), }); ``` ### Headless/CI Environment Disable browser opening for automated environments: ```typescript const authProvider = browserAuth({ launch: () => {}, // Noop - no browser opening authTimeout: 10000, // Shorter timeout for CI store: inMemoryStore(), }); ``` ## Dynamic Client Registration OAuth Callback supports [RFC 7591](https://www.rfc-editor.org/rfc/rfc7591.html) Dynamic Client Registration, allowing automatic OAuth client registration: ### How It Works ```mermaid sequenceDiagram participant App participant browserAuth participant MCP Server participant Auth Server App->>browserAuth: Create provider (no client_id) App->>MCP Server: Connect via transport MCP Server->>App: Requires authentication App->>browserAuth: Initiate OAuth browserAuth->>Auth Server: POST /register (DCR) Auth Server->>browserAuth: Return client_id, client_secret browserAuth->>browserAuth: Store credentials browserAuth->>Auth Server: Start OAuth flow Auth Server->>browserAuth: Return authorization code browserAuth->>App: Authentication complete ``` ### DCR Example No pre-registration needed: ```typescript import open from "open"; // No clientId or clientSecret required! const authProvider = browserAuth({ scope: "read write", launch: open, store: fileStore(), // Persist dynamically registered client }); // The provider will automatically: // 1. Register a new OAuth client on first use // 2. Store the client credentials // 3. Reuse them for future sessions ``` ### Benefits of DCR * **Zero Configuration**: Users don't need to manually register OAuth apps * **Automatic Setup**: Client registration happens transparently * **Credential Persistence**: Registered clients are reused across sessions * **Simplified Distribution**: Ship MCP clients without OAuth setup instructions ## Token Storage ### Storage Interfaces OAuth Callback provides two storage interfaces: #### TokenStore (Basic) ```typescript interface TokenStore { get(key: string): Promise; set(key: string, tokens: Tokens): Promise; delete(key: string): Promise; } ``` #### OAuthStore (Extended) ```typescript interface OAuthStore extends TokenStore { getClient(key: string): Promise; setClient(key: string, client: ClientInfo): Promise; deleteClient(key: string): Promise; getCodeVerifier(key: string): Promise; setCodeVerifier(key: string, verifier: string): Promise; deleteCodeVerifier(key: string): Promise; } ``` ### Built-in Implementations #### In-Memory Store Ephemeral storage (tokens lost on restart): ```typescript import open from "open"; import { browserAuth, inMemoryStore } from "oauth-callback/mcp"; const authProvider = browserAuth({ launch: open, store: inMemoryStore(), }); ``` **Use cases:** * Development and testing * Short-lived CLI sessions * Maximum security (no persistence) #### File Store Persistent storage to JSON file: ```typescript import open from "open"; import { browserAuth, fileStore } from "oauth-callback/mcp"; // Default location: ~/.mcp/tokens.json const authProvider = browserAuth({ launch: open, store: fileStore(), }); // Custom location const customAuth = browserAuth({ launch: open, store: fileStore("/path/to/tokens.json"), }); ``` **Use cases:** * Desktop applications * Long-running services * Multi-session authentication ### Custom Storage Implementation Implement your own storage backend: ```typescript import { TokenStore, Tokens } from "oauth-callback/mcp"; class RedisStore implements TokenStore { constructor(private redis: RedisClient) {} async get(key: string): Promise { const data = await this.redis.get(key); return data ? JSON.parse(data) : null; } async set(key: string, tokens: Tokens): Promise { await this.redis.set(key, JSON.stringify(tokens)); } async delete(key: string): Promise { await this.redis.del(key); } } // Use custom store const authProvider = browserAuth({ launch: open, store: new RedisStore(redisClient), }); ``` ## Security Features ### PKCE (Proof Key for Code Exchange) PKCE is always enabled for enhanced security. The MCP SDK handles PKCE automatically through the provider's `saveCodeVerifier()` and `codeVerifier()` methods. PKCE prevents authorization code interception attacks by: 1. Generating a cryptographic code verifier 2. Sending a hashed challenge with the authorization request 3. Proving possession of the verifier during token exchange ### State Parameter The `state()` method generates secure random values when called by the MCP SDK. State validation in `browserAuth` compares the callback's state against the state parameter in the authorization URL that was passed to `redirectToAuthorization()`. This means validation works regardless of whether `state()` was used - it validates whatever state is present in the URL. For localhost flows, [RFC 8252](https://www.rfc-editor.org/rfc/rfc8252) considers loopback interface binding sufficient for security. State validation adds defense-in-depth. ### Token Expiry Management Tokens are tracked with expiry times. The provider returns `undefined` from `tokens()` 60 seconds before actual expiry to prevent mid-request failures. This triggers re-authentication before tokens become invalid: ```typescript // The provider: // 1. Returns undefined 60s before token expiry // 2. SDK triggers re-auth when tokens() returns undefined // 3. Requests never fail due to mid-flight token expiry ``` ### Secure Storage File storage uses restrictive permissions: ```typescript import open from "open"; // Files are created with mode 0600 (owner read/write only) const authProvider = browserAuth({ launch: open, store: fileStore(), // Secure file permissions }); ``` ## Error Handling ### OAuth Errors The provider handles OAuth-specific errors: ```typescript try { await client.connect(transport); } catch (error) { if (error.message.includes("access_denied")) { console.log("User cancelled authorization"); } else if (error.message.includes("invalid_scope")) { console.log("Requested scope not available"); } else { console.error("Connection failed:", error); } } ``` ### Timeout Handling Configure timeout for different scenarios: ```typescript import open from "open"; const authProvider = browserAuth({ launch: open, authTimeout: 600000, // 10 minutes for first-time setup }); ``` ## Complete Examples ### Notion MCP Integration Full example with Dynamic Client Registration: ```typescript import open from "open"; import { browserAuth, fileStore } from "oauth-callback/mcp"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; async function connectToNotion() { // No client credentials needed - uses DCR! const authProvider = browserAuth({ launch: open, // Opens browser for OAuth consent store: fileStore(), // Persist tokens and client registration scope: "read write", onRequest: (req) => { console.log(`[Notion OAuth] ${new URL(req.url).pathname}`); }, }); const transport = new StreamableHTTPClientTransport( new URL("https://mcp.notion.com/mcp"), { authProvider }, ); const client = new Client( { name: "notion-client", version: "1.0.0" }, { capabilities: {} }, ); try { await client.connect(transport); console.log("Connected to Notion MCP!"); // List available tools const tools = await client.listTools(); console.log("Available tools:", tools); // Use a tool const result = await client.callTool("search", { query: "meeting notes", }); console.log("Search results:", result); } catch (error) { console.error("Failed to connect:", error); } finally { await client.close(); } } connectToNotion(); ``` ### Multi-Environment Configuration Support development, staging, and production: ```typescript import open from "open"; import { browserAuth, fileStore, inMemoryStore } from "oauth-callback/mcp"; function createAuthProvider(environment: "dev" | "staging" | "prod") { const configs = { dev: { port: 3000, launch: open, store: inMemoryStore(), // No persistence in dev authTimeout: 60000, onRequest: (req: Request) => console.log("[DEV]", req.url), }, staging: { port: 3001, launch: open, store: fileStore("~/.mcp/staging-tokens.json"), storeKey: "staging", authTimeout: 120000, }, prod: { port: 3002, launch: open, store: fileStore("~/.mcp/prod-tokens.json"), storeKey: "production", authTimeout: 300000, clientId: process.env.PROD_CLIENT_ID, clientSecret: process.env.PROD_CLIENT_SECRET, }, }; return browserAuth(configs[environment]); } // Use appropriate environment const authProvider = createAuthProvider( process.env.NODE_ENV as "dev" | "staging" | "prod", ); ``` ### Handling Expired Tokens The provider does not use refresh tokens. When tokens expire, re-authentication is triggered automatically by returning `undefined` from `tokens()`. The MCP SDK handles this transparently. For explicit control over re-authentication: ```typescript // Force re-authentication by clearing tokens await authProvider.invalidateCredentials("tokens"); ``` ## Testing ### Unit Testing Mock the OAuth provider for tests: ```typescript import { vi, describe, it, expect } from "vitest"; // Create mock provider const mockAuthProvider = { redirectToAuthorization: vi.fn(), tokens: vi.fn().mockResolvedValue({ access_token: "test_token", token_type: "Bearer", }), saveTokens: vi.fn(), clientInformation: vi.fn(), saveClientInformation: vi.fn(), state: vi.fn().mockResolvedValue("test_state"), codeVerifier: vi.fn().mockResolvedValue("test_verifier"), saveCodeVerifier: vi.fn(), invalidateCredentials: vi.fn(), }; describe("MCP Client", () => { it("should authenticate with OAuth", async () => { const transport = new StreamableHTTPClientTransport( new URL("https://test.example.com"), { authProvider: mockAuthProvider }, ); const client = new Client( { name: "test", version: "1.0.0" }, { capabilities: {} }, ); await client.connect(transport); expect(mockAuthProvider.tokens).toHaveBeenCalled(); }); }); ``` ### Integration Testing Test with a mock OAuth server: ```typescript import { browserAuth, inMemoryStore } from "oauth-callback/mcp"; import { createMockOAuthServer } from "./test-utils"; describe("OAuth Flow Integration", () => { let mockServer: MockOAuthServer; beforeAll(async () => { mockServer = createMockOAuthServer(); await mockServer.start(); }); afterAll(async () => { await mockServer.stop(); }); it("should complete full OAuth flow", async () => { const authProvider = browserAuth({ port: 3001, launch: () => {}, // Noop - don't open browser in tests store: inMemoryStore(), }); // Simulate OAuth flow await authProvider.redirectToAuthorization( new URL(`http://localhost:${mockServer.port}/authorize`), ); const tokens = await authProvider.tokens(); expect(tokens?.access_token).toBeDefined(); }); }); ``` ## Troubleshooting ### Common Issues ::: details Port Already in Use ```typescript import open from "open"; // Use a different port const authProvider = browserAuth({ launch: open, port: 8080, // Try alternative port }); ``` ::: ::: details Tokens Not Persisting ```typescript import open from "open"; // Ensure you're using file store, not in-memory const authProvider = browserAuth({ launch: open, store: fileStore(), // ✅ Persistent // store: inMemoryStore() // ❌ Lost on restart }); ``` ::: ::: details DCR Not Working Some servers may not support Dynamic Client Registration: ```typescript import open from "open"; // Fallback to pre-registered credentials const authProvider = browserAuth({ launch: open, clientId: "your-client-id", clientSecret: "your-client-secret", }); ``` ::: ::: details Browser Not Opening ```typescript import open from "open"; // Conditionally open browser based on environment const authProvider = browserAuth({ launch: process.env.CI ? () => {} : open, }); ``` ::: ## API Compatibility The `browserAuth` provider implements the MCP SDK's `OAuthClientProvider` interface: | Method | Status | Notes | | ------------------------- | -------------------- | ------------------------------------------------------------------------- | | `redirectToAuthorization` | ✅ Fully supported | Completes full OAuth flow (browser → callback → token exchange → persist) | | `tokens` | ✅ Fully supported | Returns current tokens | | `saveTokens` | ✅ Fully supported | Persists to storage | | `clientInformation` | ✅ Fully supported | Returns client credentials | | `saveClientInformation` | ✅ Fully supported | Stores DCR results | | `state` | ✅ Fully supported | Generates secure state | | `codeVerifier` | ✅ Fully supported | PKCE verifier | | `saveCodeVerifier` | ✅ Fully supported | Stores PKCE verifier | | `invalidateCredentials` | ✅ Fully supported | Clears stored data | | `validateResourceURL` | ✅ Returns undefined | Not applicable | ## Migration Guide ### From Manual OAuth to browserAuth ```typescript // Before: Manual OAuth implementation const code = await getAuthCode(authUrl); const tokens = await exchangeCodeForTokens(code); // Manual token storage and refresh... // After: Using browserAuth const authProvider = browserAuth({ launch: open, store: fileStore(), }); // Automatic handling of entire OAuth flow! ``` ### From In-Memory to Persistent Storage ```typescript // Before: Tokens lost on restart const authProvider = browserAuth({ launch: open }); // After: Tokens persist across sessions const authProvider = browserAuth({ launch: open, store: fileStore(), }); ``` ## Related APIs * [`getAuthCode`](/api/get-auth-code) - Low-level OAuth code capture * [`TokenStore`](/api/storage-providers) - Storage interface documentation * [`OAuthError`](/api/oauth-error) - OAuth error handling --- --- url: /oauth-callback/core-concepts.md description: >- Master the fundamental concepts and architecture of OAuth Callback, from the authorization flow to token management and MCP integration patterns. --- # Core Concepts {#top} Understanding the core concepts behind **OAuth Callback** will help you build robust OAuth integrations in your CLI tools, desktop applications, and MCP clients. This page covers the fundamental patterns, architectural decisions, and key abstractions that power the library. ## The Authorization Code Flow OAuth Callback implements the OAuth 2.0 Authorization Code Flow, the most secure flow for applications that can protect client secrets. This flow involves three key participants: ```mermaid flowchart LR A[Your App] -->|Step 1: Request authorization| B[Auth Server] B -->|Step 2: User authenticates| C[User] C -->|Step 3: Grants permission| B B -->|Step 4: Returns code| A A -->|Step 5: Exchange code| B B -->|Step 6: Returns tokens| A ``` ### Why Authorization Code Flow? The authorization code flow provides several security benefits: * **No token exposure**: Access tokens never pass through the browser * **Short-lived codes**: Authorization codes expire quickly (typically 10 minutes) * **Server verification**: The auth server can verify the client's identity * **PKCE support**: Protection against authorization code interception ## The Localhost Callback Pattern The core innovation of OAuth Callback is making the localhost callback pattern trivially simple to implement. This pattern, standardized in [RFC 8252](https://www.rfc-editor.org/rfc/rfc8252.html), solves a fundamental problem: how can native applications receive OAuth callbacks without a public web server? ### The Problem Traditional web applications have public URLs where OAuth providers can send callbacks: ```text https://myapp.com/oauth/callback?code=xyz123 ``` But CLI tools and desktop apps don't have public URLs. They run on the user's machine behind firewalls and NAT. ### The Solution OAuth Callback creates a temporary HTTP server on localhost that: 1. **Binds locally**: Only accepts connections from `127.0.0.1` 2. **Uses dynamic ports**: Works with any available port 3. **Auto-terminates**: Shuts down after receiving the callback 4. **Handles edge cases**: Timeouts, errors, user cancellation ```typescript import open from "open"; // This single function handles all the complexity const result = await getAuthCode({ authorizationUrl, launch: open }); ``` ## Architecture Overview OAuth Callback is built on a layered architecture that separates concerns and enables flexibility: ```mermaid flowchart TD subgraph "Application Layer" A[Your CLI/Desktop App] end subgraph "OAuth Callback Library" B[getAuthCode Function] C[HTTP Server Module] D[Browser Launcher] E[Error Handler] F[Template Engine] end subgraph "MCP Integration Layer" G[browserAuth Provider] H[Token Storage] I[Dynamic Client Registration] end A --> B B --> C B --> D B --> E B --> F A --> G G --> H G --> I ``` ### Core Components #### 1. The HTTP Server (`server.ts`) The heart of OAuth Callback is a lightweight HTTP server that: * Listens on localhost for the OAuth callback * Parses query parameters from the redirect * Serves success/error HTML pages * Implements proper cleanup on completion Internally, the server handles: * Request routing (`/callback` path matching) * Query parameter extraction (`code`, `state`, `error`) * HTML template rendering with placeholders * Graceful shutdown after callback #### 2. The Authorization Handler (`getAuthCode`) The main API surface that orchestrates the entire flow: ```typescript interface GetAuthCodeOptions { authorizationUrl: string; // OAuth provider URL port?: number; // Server port (default: 3000) timeout?: number; // Timeout in ms (default: 30000) launch?: (url: string) => unknown; // Optional URL launcher signal?: AbortSignal; // For cancellation // ... more options } ``` #### 3. Error Management (`OAuthError`) Specialized error handling for OAuth-specific failures: ```typescript class OAuthError extends Error { error: string; // OAuth error code error_description?: string; // Human-readable description error_uri?: string; // Link to more information } ``` Common OAuth errors are properly typed and handled: * `access_denied` - User declined authorization * `invalid_scope` - Requested scope is invalid * `server_error` - Authorization server error * `temporarily_unavailable` - Server is overloaded ## Token Management For applications that need to persist OAuth tokens, OAuth Callback provides a flexible storage abstraction: ### Storage Abstraction The `TokenStore` interface enables different storage strategies: ```typescript interface TokenStore { get(key: string): Promise; set(key: string, tokens: Tokens): Promise; delete(key: string): Promise; } ``` ### Built-in Implementations #### In-Memory Store Ephemeral storage for maximum security: ```typescript const store = inMemoryStore(); // Tokens exist only during process lifetime // Perfect for CLI tools that authenticate per-session ``` #### File Store Persistent storage for convenience: ```typescript const store = fileStore("~/.myapp/tokens.json"); // Tokens persist across sessions // Ideal for desktop apps with returning users ``` ### Token Lifecycle OAuth Callback uses re-authentication instead of refresh tokens. When tokens expire, the provider returns `undefined`, signaling the MCP SDK to re-initiate the OAuth flow. This simplifies implementation and avoids storing long-lived refresh credentials. ```mermaid stateDiagram-v2 [*] --> NoToken: Initial State NoToken --> Authorizing: User initiates OAuth Authorizing --> HasToken: Successful auth HasToken --> Authorizing: Token expired (re-auth) HasToken --> NoToken: User logs out ``` ## MCP Integration Pattern The Model Context Protocol (MCP) integration showcases advanced OAuth patterns: ### Dynamic Client Registration OAuth Callback supports [RFC 7591](https://www.rfc-editor.org/rfc/rfc7591.html) Dynamic Client Registration, allowing apps to register OAuth clients on-the-fly: ```mermaid sequenceDiagram participant App participant OAuth Callback participant Auth Server App->>OAuth Callback: browserAuth() (no client_id) OAuth Callback->>Auth Server: POST /register Auth Server->>OAuth Callback: Return client_id, client_secret OAuth Callback->>OAuth Callback: Store credentials OAuth Callback->>Auth Server: Start normal OAuth flow ``` This eliminates the need for users to manually register OAuth applications. ### The Provider Pattern The `browserAuth()` function returns an `OAuthClientProvider` that integrates with MCP SDK: ```typescript interface OAuthClientProvider { // Token access - returns undefined when expired, triggering re-auth tokens(): Promise; saveTokens(tokens: OAuthTokens): Promise; // Completes full OAuth flow (browser → callback → token exchange) redirectToAuthorization(authorizationUrl: URL): Promise; // PKCE and state management codeVerifier(): Promise; saveCodeVerifier(verifier: string): Promise; state(): Promise; } ``` ## Request/Response Lifecycle Understanding the complete lifecycle helps when debugging OAuth flows: ```mermaid sequenceDiagram participant User participant App participant OAuth Callback participant Browser participant Auth Server User->>App: Run command App->>OAuth Callback: getAuthCode(url) OAuth Callback->>OAuth Callback: Start HTTP server OAuth Callback->>Browser: Open auth URL Browser->>Auth Server: GET /authorize Auth Server->>Browser: Login page Browser->>User: Show login User->>Browser: Enter credentials Browser->>Auth Server: POST credentials Auth Server->>Browser: Consent page User->>Browser: Approve access Auth Server->>Browser: Redirect to localhost Browser->>OAuth Callback: GET /callback?code=xyz OAuth Callback->>Browser: Success HTML OAuth Callback->>App: Return {code: "xyz"} OAuth Callback->>OAuth Callback: Shutdown server App->>Auth Server: Exchange code for token Auth Server->>App: Return access token ``` ## State Management OAuth Callback handles multiple types of state throughout the flow: ### Server State The HTTP server maintains minimal state: * **Active**: Server is listening for callbacks * **Received**: Callback has been received * **Shutdown**: Server is closing ### OAuth State The OAuth flow tracks: * **Authorization URL**: Where to send the user * **Expected state**: For CSRF validation * **Timeout timer**: For abandonment detection * **Abort signal**: For cancellation support ### Token State When using token storage: * **No tokens**: Need to authenticate * **Valid tokens**: Can make API calls * **Expired tokens**: Triggers re-authentication (no refresh tokens used) ## Security Architecture Security is built into every layer of OAuth Callback: ### Network Security ```typescript // Localhost-only binding server.listen(port, "127.0.0.1"); // IPv6 localhost support server.listen(port, "::1"); // Reject non-localhost connections if (!isLocalhost(request.socket.remoteAddress)) { return reject(); } ``` ### OAuth Security * **State parameter**: Prevents CSRF attacks * **PKCE support**: Protects authorization codes * **Timeout enforcement**: Limits exposure window * **Automatic cleanup**: Reduces attack surface ### Token Security * **Memory storage option**: No persistence * **File permissions**: Restrictive when using file store * **No logging**: Tokens never logged or exposed * **Expiry handling**: Automatic re-auth when tokens expire ## Template System OAuth Callback includes a simple but powerful template system for success/error pages: ### Placeholder Substitution Templates support `` syntax: ```html

Error:

``` Placeholders are automatically escaped to prevent XSS attacks. ### Built-in Templates The library includes professional templates with: * Animated success checkmark * Clear error messages * Responsive design * Accessibility features ### Custom Templates Applications can provide custom HTML: ```typescript { successHtml: "

Welcome back!

", errorHtml: "

Oops!

" } ``` ## Cross-Runtime Compatibility OAuth Callback achieves cross-runtime compatibility through Web Standards APIs: ### Universal APIs ```typescript // Using Web Standards instead of Node.js-specific APIs new Request(); // Instead of http.IncomingMessage new Response(); // Instead of http.ServerResponse new URL(); // Instead of url.parse() new URLSearchParams(); // Instead of querystring ``` ### Runtime Detection The library adapts to the runtime environment: ```typescript // Node.js import { createServer } from "node:http"; // Deno Deno.serve({ port: 3000 }); // Bun Bun.serve({ port: 3000 }); ``` ## Performance Considerations OAuth Callback is designed for optimal performance: ### Fast Startup * Minimal dependencies (only `open` package) * Lazy loading of heavy modules * Pre-compiled HTML templates ### Efficient Memory Use * Server resources freed immediately after use * No persistent connections * Minimal state retention ### Quick Response * Immediate browser redirect handling * Non-blocking I/O operations * Parallel browser launch and server start ## Extension Points While OAuth Callback provides sensible defaults, it offers multiple extension points: ### Custom Storage Implement the `TokenStore` interface for custom storage: ```typescript class RedisStore implements TokenStore { async get(key: string) { /* Redis logic */ } async set(key: string, tokens: Tokens) { /* Redis logic */ } async delete(key: string) { /* Redis logic */ } } ``` ### Request Interception Monitor or modify requests with callbacks: ```typescript { onRequest: (req) => { console.log(`OAuth: ${req.method} ${req.url}`); // Add telemetry, logging, etc. }; } ``` ### Custom URL Launcher Customize how the authorization URL is opened: ```typescript import open from "open"; // Use system browser await getAuthCode({ authorizationUrl, launch: open }); // Headless mode - omit launch, print URL manually console.log(`Open: ${authorizationUrl}`); await getAuthCode({ authorizationUrl }); ``` ## Best Practices ### Error Handling Always handle both OAuth errors and unexpected failures: ```typescript try { const result = await getAuthCode(authUrl); } catch (error) { if (error instanceof OAuthError) { // Handle OAuth-specific errors } else { // Handle unexpected errors } } ``` ### State Validation Always validate the state parameter: ```typescript const state = crypto.randomUUID(); // Include in auth URL const result = await getAuthCode(authUrl); if (result.state !== state) throw new Error("CSRF detected"); ``` ### Token Storage Choose storage based on security requirements: * **CLI tools**: Use `inMemoryStore()` for per-session auth * **Desktop apps**: Use `fileStore()` for user convenience * **Sensitive apps**: Always use in-memory storage ### Timeout Configuration Set appropriate timeouts for your use case: * **Interactive apps**: 30-60 seconds * **Automated tools**: 5-10 seconds * **First-time setup**: 2-5 minutes --- --- url: /oauth-callback/examples.md --- # Examples --- --- url: /oauth-callback/api/get-auth-code.md description: >- Core function for capturing OAuth authorization codes via localhost callback in CLI tools and desktop applications. --- # getAuthCode The `getAuthCode` function is the primary API for capturing OAuth authorization codes through a localhost callback. It handles the entire OAuth flow: starting a local server, opening the browser, waiting for the callback, and returning the authorization code. ## Function Signature ```typescript function getAuthCode( input: string | GetAuthCodeOptions, ): Promise; ``` ## Parameters The function accepts either: * A **string** containing the OAuth authorization URL (uses default options) * A **GetAuthCodeOptions** object for advanced configuration ### GetAuthCodeOptions | Property | Type | Default | Description | | ------------------ | -------------------------- | ------------- | --------------------------------------------- | | `authorizationUrl` | `string` | *required* | OAuth authorization URL with query parameters | | `port` | `number` | `3000` | Port for the local callback server | | `hostname` | `string` | `"localhost"` | Hostname to bind the server to | | `callbackPath` | `string` | `"/callback"` | URL path for OAuth callback | | `timeout` | `number` | `30000` | Timeout in milliseconds | | `launch` | `(url: string) => unknown` | *none* | Optional callback to launch auth URL | | `successHtml` | `string` | *built-in* | Custom HTML for successful auth | | `errorHtml` | `string` | *built-in* | Custom HTML template for errors | | `signal` | `AbortSignal` | *none* | For programmatic cancellation | | `onRequest` | `(req: Request) => void` | *none* | Callback for request logging | ## Return Value Returns a `Promise` containing: ```typescript interface CallbackResult { code: string; // Authorization code state?: string; // State parameter (if provided) [key: string]: any; // Additional query parameters } ``` ## Exceptions The function can throw: | Error Type | Condition | Description | | ------------ | -------------------- | --------------------------------------------------------------- | | `OAuthError` | OAuth provider error | Contains `error`, `error_description`, and optional `error_uri` | | `Error` | Timeout | "Timeout waiting for callback" | | `Error` | Port in use | "EADDRINUSE" - port already occupied | | `Error` | Cancellation | "Operation aborted" via AbortSignal | ## Basic Usage ### With Browser Launch The recommended usage with automatic browser opening: ```typescript import open from "open"; import { getAuthCode } from "oauth-callback"; const authUrl = "https://github.com/login/oauth/authorize?" + new URLSearchParams({ client_id: "your_client_id", redirect_uri: "http://localhost:3000/callback", scope: "user:email", state: "random_state", }); const result = await getAuthCode({ authorizationUrl: authUrl, launch: open }); console.log("Authorization code:", result.code); console.log("State:", result.state); ``` ### With Configuration Object Using the options object for more control: ```typescript const result = await getAuthCode({ authorizationUrl: authUrl, port: 8080, timeout: 60000, hostname: "127.0.0.1", }); ``` ## Advanced Usage ### Custom Port Configuration When port 3000 is unavailable or you've registered a different redirect URI: ```typescript const result = await getAuthCode({ authorizationUrl: "https://oauth.example.com/authorize?...", port: 8888, callbackPath: "/oauth/callback", // Custom path hostname: "127.0.0.1", // Specific IP binding }); ``` ::: warning Port Configuration Ensure the port and path match your OAuth app's registered redirect URI: * Registered: `http://localhost:8888/oauth/callback` * Configuration must use: `port: 8888`, `callbackPath: "/oauth/callback"` ::: ### Custom HTML Templates Provide branded success and error pages: ```typescript const result = await getAuthCode({ authorizationUrl: authUrl, successHtml: ` Success!

✨ Authorization Successful!

You can close this window and return to the app.

`, errorHtml: `

Authorization Failed

Error:

More information `, }); ``` ::: tip Template Placeholders Error templates support these placeholders: * `` - OAuth error code * `` - Human-readable description * `` - Link to error documentation ::: ### Request Logging Monitor OAuth flow for debugging: ```typescript const result = await getAuthCode({ authorizationUrl: authUrl, onRequest: (req) => { const url = new URL(req.url); console.log(`[${new Date().toISOString()}] ${req.method} ${url.pathname}`); // Log specific paths if (url.pathname === "/callback") { console.log( "Callback received with params:", url.searchParams.toString(), ); } }, }); ``` ### Timeout Handling Configure timeout for different scenarios: ```typescript try { const result = await getAuthCode({ authorizationUrl: authUrl, timeout: 120000, // 2 minutes for first-time users }); } catch (error) { if (error.message === "Timeout waiting for callback") { console.error("Authorization took too long. Please try again."); } } ``` ### Programmatic Cancellation Support user-initiated cancellation: ```typescript const controller = new AbortController(); // Listen for Ctrl+C process.on("SIGINT", () => { console.log("\nCancelling authorization..."); controller.abort(); }); // Set a maximum time limit const timeoutId = setTimeout(() => { console.log("Authorization time limit reached"); controller.abort(); }, 300000); // 5 minutes try { const result = await getAuthCode({ authorizationUrl: authUrl, signal: controller.signal, }); clearTimeout(timeoutId); console.log("Success! Code:", result.code); } catch (error) { if (error.message === "Operation aborted") { console.log("Authorization was cancelled"); } } ``` ### Headless / Manual Browser Control For environments where you want to handle browser opening yourself (SSH, CI, etc.): ```typescript // Headless mode - print URL, let user open manually const redirectUri = "http://localhost:3000/callback"; const authUrl = `https://oauth.example.com/authorize?redirect_uri=${encodeURIComponent(redirectUri)}`; console.log("Please open this URL in your browser:"); console.log(authUrl); // Server waits for callback without opening browser const result = await getAuthCode({ port: 3000, timeout: 120000 }); ``` Or use a custom launcher: ```typescript import open from "open"; const result = await getAuthCode({ authorizationUrl: authUrl, launch: open, // Both authorizationUrl and launch are required together }); ``` ## Error Handling ### Comprehensive Error Handling Handle all possible error scenarios: ```typescript import open from "open"; import { getAuthCode, OAuthError } from "oauth-callback"; try { const result = await getAuthCode({ authorizationUrl: authUrl, launch: open }); // Success - exchange code for token return result.code; } catch (error) { if (error instanceof OAuthError) { // OAuth-specific errors from provider switch (error.error) { case "access_denied": console.log("User cancelled authorization"); break; case "invalid_scope": console.error("Requested scope is invalid:", error.error_description); break; case "server_error": console.error("OAuth server error. Please try again later."); break; case "temporarily_unavailable": console.error("OAuth service is temporarily unavailable"); break; default: console.error(`OAuth error: ${error.error}`); if (error.error_description) { console.error(`Details: ${error.error_description}`); } if (error.error_uri) { console.error(`More info: ${error.error_uri}`); } } } else if (error.code === "EADDRINUSE") { console.error(`Port ${port} is already in use. Try a different port.`); } else if (error.message === "Timeout waiting for callback") { console.error("Authorization timed out. Please try again."); } else if (error.message === "Operation aborted") { console.log("Authorization was cancelled by user"); } else { // Unexpected errors console.error("Unexpected error:", error); } throw error; // Re-throw for upstream handling } ``` ### Retry Logic Implement retry for transient failures: ```typescript async function getAuthCodeWithRetry( authUrl: string, maxAttempts = 3, ): Promise { for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { const result = await getAuthCode({ authorizationUrl: authUrl, port: 3000 + attempt - 1, // Try different ports timeout: 30000 * attempt, // Increase timeout each attempt }); return result.code; } catch (error) { console.log(`Attempt ${attempt} failed:`, error.message); if (attempt === maxAttempts) { throw error; } // Don't retry user cancellations if (error instanceof OAuthError && error.error === "access_denied") { throw error; } console.log(`Retrying... (${attempt + 1}/${maxAttempts})`); } } } ``` ## Security Best Practices ### State Parameter Validation Always validate the state parameter to prevent CSRF attacks: ```typescript import { randomBytes } from "crypto"; // Generate secure random state const state = randomBytes(32).toString("base64url"); const authUrl = new URL("https://oauth.example.com/authorize"); authUrl.searchParams.set("client_id", CLIENT_ID); authUrl.searchParams.set("redirect_uri", "http://localhost:3000/callback"); authUrl.searchParams.set("state", state); authUrl.searchParams.set("scope", "read write"); const result = await getAuthCode(authUrl.toString()); // Validate state matches if (result.state !== state) { throw new Error("State mismatch - possible CSRF attack!"); } // Safe to use authorization code console.log("Valid authorization code:", result.code); ``` ### PKCE Implementation Implement Proof Key for Code Exchange for public clients: ```typescript import { createHash, randomBytes } from "crypto"; // Generate PKCE challenge const verifier = randomBytes(32).toString("base64url"); const challenge = createHash("sha256").update(verifier).digest("base64url"); // Include challenge in authorization request const authUrl = new URL("https://oauth.example.com/authorize"); authUrl.searchParams.set("code_challenge", challenge); authUrl.searchParams.set("code_challenge_method", "S256"); // ... other parameters const result = await getAuthCode(authUrl.toString()); // Include verifier when exchanging code const tokenResponse = await fetch("https://oauth.example.com/token", { method: "POST", body: new URLSearchParams({ grant_type: "authorization_code", code: result.code, code_verifier: verifier, // Include PKCE verifier client_id: CLIENT_ID, redirect_uri: "http://localhost:3000/callback", }), }); ``` ## Complete Examples ### GitHub OAuth Integration Full example with error handling and token exchange: ```typescript import { getAuthCode, OAuthError } from "oauth-callback"; async function authenticateWithGitHub() { const CLIENT_ID = process.env.GITHUB_CLIENT_ID; const CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET; // Build authorization URL with all parameters const authUrl = new URL("https://github.com/login/oauth/authorize"); authUrl.searchParams.set("client_id", CLIENT_ID); authUrl.searchParams.set("redirect_uri", "http://localhost:3000/callback"); authUrl.searchParams.set("scope", "user:email repo"); authUrl.searchParams.set("state", crypto.randomUUID()); try { // Get authorization code console.log("Opening browser for GitHub authorization..."); const result = await getAuthCode({ authorizationUrl: authUrl.toString(), timeout: 60000, successHtml: "

✅ GitHub authorization successful!

", }); // Exchange code for access token console.log("Exchanging code for access token..."); const tokenResponse = await fetch( "https://github.com/login/oauth/access_token", { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", }, body: JSON.stringify({ client_id: CLIENT_ID, client_secret: CLIENT_SECRET, code: result.code, }), }, ); const tokens = await tokenResponse.json(); if (tokens.error) { throw new Error(`Token exchange failed: ${tokens.error_description}`); } // Use access token to get user info const userResponse = await fetch("https://api.github.com/user", { headers: { Authorization: `Bearer ${tokens.access_token}`, Accept: "application/vnd.github.v3+json", }, }); const user = await userResponse.json(); console.log(`Authenticated as: ${user.login}`); return tokens.access_token; } catch (error) { if (error instanceof OAuthError) { console.error("GitHub authorization failed:", error.error_description); } else { console.error("Authentication error:", error.message); } throw error; } } ``` ### Multi-Provider Support Handle multiple OAuth providers with a unified interface: ```typescript type Provider = "github" | "google" | "microsoft"; async function authenticate(provider: Provider): Promise { const configs = { github: { authUrl: "https://github.com/login/oauth/authorize", tokenUrl: "https://github.com/login/oauth/access_token", scope: "user:email", }, google: { authUrl: "https://accounts.google.com/o/oauth2/v2/auth", tokenUrl: "https://oauth2.googleapis.com/token", scope: "openid email profile", }, microsoft: { authUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token", scope: "user.read", }, }; const config = configs[provider]; const authUrl = new URL(config.authUrl); // Add provider-specific parameters authUrl.searchParams.set( "client_id", process.env[`${provider.toUpperCase()}_CLIENT_ID`], ); authUrl.searchParams.set("redirect_uri", "http://localhost:3000/callback"); authUrl.searchParams.set("scope", config.scope); authUrl.searchParams.set("response_type", "code"); authUrl.searchParams.set("state", crypto.randomUUID()); if (provider === "google") { authUrl.searchParams.set("access_type", "offline"); authUrl.searchParams.set("prompt", "consent"); } const result = await getAuthCode({ authorizationUrl: authUrl.toString(), timeout: 90000, onRequest: (req) => { console.log(`[${provider}] ${req.method} ${new URL(req.url).pathname}`); }, }); return result.code; } ``` ## Testing ### Unit Testing Mock the OAuth flow for testing: ```typescript import { getAuthCode } from "oauth-callback"; import { describe, it, expect } from "vitest"; describe("OAuth Flow", () => { it("should capture authorization code", async () => { // Start mock OAuth server const mockServer = createMockOAuthServer(); await mockServer.start(); const result = await getAuthCode({ authorizationUrl: `http://localhost:${mockServer.port}/authorize`, port: 3001, // No launch callback - tests simulate OAuth redirect timeout: 5000, }); expect(result.code).toBe("test_auth_code"); expect(result.state).toBe("test_state"); await mockServer.stop(); }); it("should handle OAuth errors", async () => { const mockServer = createMockOAuthServer({ error: "access_denied", }); await mockServer.start(); await expect( getAuthCode({ authorizationUrl: `http://localhost:${mockServer.port}/authorize`, // No launch - test simulates OAuth redirect }), ).rejects.toThrow(OAuthError); await mockServer.stop(); }); }); ``` ## Migration Guide ### From v1.x to v2.x ```typescript // v1.x (old) const code = await captureAuthCode(url, 3000); // v2.x (new) const result = await getAuthCode({ authorizationUrl: url, port: 3000, }); const code = result.code; ``` ## Related APIs * [`OAuthError`](/api/oauth-error) - OAuth-specific error class * [`browserAuth`](/api/browser-auth) - MCP SDK integration provider * [`TokenStore`](/api/storage-providers) - Token storage interface --- --- url: /oauth-callback/getting-started.md description: >- Quick start guide to implement OAuth 2.0 authorization code flow in your CLI tools, desktop apps, and MCP clients using oauth-callback. --- # Getting Started {#top} This guide will walk you through adding OAuth authentication to your application in just a few minutes. Whether you're building a CLI tool, desktop app, or MCP client, **OAuth Callback** handles the complexity of receiving authorization codes via localhost callbacks. ## Prerequisites Before you begin, ensure you have: * **Runtime**: Node.js 18+, Deno, or Bun installed * **OAuth App**: Registered with your OAuth provider (unless using Dynamic Client Registration) * **Redirect URI**: Set to `http://localhost:3000/callback` in your OAuth app settings ## Installation Install the package using your preferred package manager: ::: code-group ```bash [Bun] bun add oauth-callback open ``` ```bash [npm] npm install oauth-callback open ``` ```bash [pnpm] pnpm add oauth-callback open ``` ```bash [Yarn] yarn add oauth-callback open ``` ::: > **Note:** The `open` package is optional but recommended for launching the browser. Omit it for headless environments. ## Basic Usage The simplest way to capture an OAuth authorization code is with the `getAuthCode()` function: ```typescript import open from "open"; import { getAuthCode } from "oauth-callback"; // Construct your OAuth authorization URL const authUrl = "https://github.com/login/oauth/authorize?" + new URLSearchParams({ client_id: "your_client_id", redirect_uri: "http://localhost:3000/callback", scope: "user:email", state: crypto.randomUUID(), // For CSRF protection }); // Get the authorization code (launch: open opens the browser) const result = await getAuthCode({ authorizationUrl: authUrl, launch: open }); console.log("Authorization code:", result.code); console.log("State:", result.state); ``` That's it! The library will: 1. Start a local HTTP server on port 3000 2. Open the user's browser to the authorization URL 3. Capture the callback with the authorization code 4. Return the code and automatically shut down the server ## Step-by-Step Implementation Let's build a complete OAuth flow for a CLI application: ### Step 1: Register Your OAuth Application First, register your application with your OAuth provider: ::: details GitHub OAuth Setup 1. Go to **Settings** → **Developer settings** → **OAuth Apps** 2. Click **New OAuth App** 3. Fill in: * **Application name**: Your app name * **Homepage URL**: Your website or GitHub repo * **Authorization callback URL**: `http://localhost:3000/callback` 4. Save and copy your **Client ID** and **Client Secret** ::: ::: details Google OAuth Setup 1. Go to [Google Cloud Console](https://console.cloud.google.com/) 2. Create or select a project 3. Enable the necessary APIs 4. Go to **APIs & Services** → **Credentials** 5. Click **Create Credentials** → **OAuth client ID** 6. Choose **Desktop app** as application type 7. Add `http://localhost:3000/callback` to authorized redirect URIs 8. Copy your **Client ID** and **Client Secret** ::: ### Step 2: Implement the Authorization Flow Create a file `auth.ts` with your OAuth implementation: ```typescript import open from "open"; import { getAuthCode, OAuthError } from "oauth-callback"; async function authenticate() { // Generate state for CSRF protection const state = crypto.randomUUID(); // Build authorization URL const authUrl = new URL("https://github.com/login/oauth/authorize"); authUrl.searchParams.set("client_id", process.env.GITHUB_CLIENT_ID!); authUrl.searchParams.set("redirect_uri", "http://localhost:3000/callback"); authUrl.searchParams.set("scope", "user:email"); authUrl.searchParams.set("state", state); try { // Get authorization code console.log("Opening browser for authentication..."); const result = await getAuthCode({ authorizationUrl: authUrl.toString(), launch: open, }); // Validate state if (result.state !== state) { throw new Error("State mismatch - possible CSRF attack"); } console.log("✅ Authorization successful!"); return result.code; } catch (error) { if (error instanceof OAuthError) { console.error("❌ OAuth error:", error.error_description || error.error); } else { console.error("❌ Unexpected error:", error); } throw error; } } ``` ### Step 3: Exchange Code for Access Token After getting the authorization code, exchange it for an access token: ```typescript async function exchangeCodeForToken(code: string) { const response = await fetch("https://github.com/login/oauth/access_token", { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", }, body: JSON.stringify({ client_id: process.env.GITHUB_CLIENT_ID, client_secret: process.env.GITHUB_CLIENT_SECRET, code: code, }), }); if (!response.ok) { throw new Error(`Token exchange failed: ${response.statusText}`); } const data = await response.json(); if (data.error) { throw new Error(`OAuth error: ${data.error_description || data.error}`); } return data.access_token; } ``` ### Step 4: Use the Access Token Now you can use the access token to make authenticated API requests: ```typescript async function getUserInfo(accessToken: string) { const response = await fetch("https://api.github.com/user", { headers: { Authorization: `Bearer ${accessToken}`, Accept: "application/vnd.github.v3+json", }, }); if (!response.ok) { throw new Error(`API request failed: ${response.statusText}`); } return response.json(); } // Complete flow async function main() { const code = await authenticate(); const token = await exchangeCodeForToken(code); const user = await getUserInfo(token); console.log(`Hello, ${user.name}! 👋`); console.log(`Email: ${user.email}`); } main().catch(console.error); ``` ## MCP SDK Integration For Model Context Protocol applications, use the `browserAuth()` provider for seamless integration: ### Quick Setup ```typescript import open from "open"; import { browserAuth, inMemoryStore } from "oauth-callback/mcp"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; // Create OAuth provider for MCP const authProvider = browserAuth({ launch: open, store: inMemoryStore(), // Or fileStore() for persistence scope: "read write", }); // Connect to MCP server with OAuth const transport = new StreamableHTTPClientTransport( new URL("https://mcp.notion.com/mcp"), { authProvider }, ); const client = new Client( { name: "my-app", version: "1.0.0" }, { capabilities: {} }, ); await client.connect(transport); ``` ### Token Storage Options Choose between ephemeral and persistent token storage: ::: code-group ```typescript [Ephemeral (Memory)] import open from "open"; import { browserAuth, inMemoryStore } from "oauth-callback/mcp"; // Tokens are lost when the process exits const authProvider = browserAuth({ launch: open, store: inMemoryStore(), }); ``` ```typescript [Persistent (File)] import open from "open"; import { browserAuth, fileStore } from "oauth-callback/mcp"; // Tokens persist across sessions const authProvider = browserAuth({ launch: open, store: fileStore(), // Saves to ~/.mcp/tokens.json }); // Or specify custom location const customAuth = browserAuth({ launch: open, store: fileStore("/path/to/tokens.json"), }); ``` ::: ### Pre-configured Credentials If you have pre-registered OAuth credentials: ```typescript import open from "open"; const authProvider = browserAuth({ clientId: "your-client-id", clientSecret: "your-client-secret", scope: "read write", launch: open, store: fileStore(), storeKey: "my-app", // Namespace for multiple apps }); ``` ## Advanced Configuration ### Custom Port and Timeout Configure the callback server port and timeout: ```typescript const result = await getAuthCode({ authorizationUrl: authUrl, port: 8080, // Use port 8080 instead of 3000 timeout: 60000, // 60 second timeout (default: 30s) hostname: "127.0.0.1", // Bind to specific IP }); ``` ### Custom HTML Templates Customize the success and error pages shown to users: ```typescript const result = await getAuthCode({ authorizationUrl: authUrl, successHtml: `

✅ Authorization Successful!

You can now close this window and return to the application.

`, errorHtml: `

❌ Authorization Failed

Error:

Please try again or contact support.

`, }); ``` ### Request Logging Add logging for debugging OAuth flows: ```typescript const result = await getAuthCode({ authorizationUrl: authUrl, onRequest: (req) => { console.log(`[OAuth] ${req.method} ${req.url}`); console.log("[OAuth] Headers:", Object.fromEntries(req.headers)); }, }); ``` ### Programmatic Cancellation Support user cancellation with AbortSignal: ```typescript const controller = new AbortController(); // Cancel after 10 seconds setTimeout(() => controller.abort(), 10000); // Or cancel on user input process.on("SIGINT", () => { console.log("\nCancelling OAuth flow..."); controller.abort(); }); try { const result = await getAuthCode({ authorizationUrl: authUrl, signal: controller.signal, }); } catch (error) { if (error.message === "Operation aborted") { console.log("OAuth flow was cancelled"); } } ``` ## Error Handling Proper error handling ensures a good user experience: ```typescript import open from "open"; import { getAuthCode, OAuthError } from "oauth-callback"; try { const result = await getAuthCode({ authorizationUrl: authUrl, launch: open }); // Success path } catch (error) { if (error instanceof OAuthError) { // OAuth-specific errors from the provider switch (error.error) { case "access_denied": console.error("User denied access"); break; case "invalid_scope": console.error("Invalid scope requested"); break; case "server_error": console.error("Authorization server error"); break; default: console.error(`OAuth error: ${error.error_description || error.error}`); } } else if (error.message === "Timeout waiting for callback") { console.error("Authorization timed out - please try again"); } else if (error.message === "Operation aborted") { console.error("Authorization was cancelled"); } else { console.error("Unexpected error:", error); } } ``` ## Security Best Practices ### Always Use State Parameter Protect against CSRF attacks with a state parameter: ```typescript const state = crypto.randomUUID(); const authUrl = `https://example.com/authorize?state=${state}&...`; const result = await getAuthCode(authUrl); if (result.state !== state) { throw new Error("State mismatch - possible CSRF attack"); } ``` ### Implement PKCE for Public Clients For enhanced security, implement Proof Key for Code Exchange: ```typescript import { createHash, randomBytes } from "node:crypto"; // Generate PKCE challenge const verifier = randomBytes(32).toString("base64url"); const challenge = createHash("sha256").update(verifier).digest("base64url"); // Include in authorization request const authUrl = new URL("https://example.com/authorize"); authUrl.searchParams.set("code_challenge", challenge); authUrl.searchParams.set("code_challenge_method", "S256"); // ... other parameters const result = await getAuthCode(authUrl.toString()); // Include verifier in token exchange const tokenResponse = await fetch(tokenUrl, { method: "POST", body: new URLSearchParams({ code: result.code, code_verifier: verifier, // ... other parameters }), }); ``` ### Secure Token Storage Choose appropriate token storage based on your security requirements: * **Use `inMemoryStore()`** for maximum security (tokens lost on restart) * **Use `fileStore()`** only when persistence is required * **Never commit tokens** to version control * **Consider encryption** for file-based storage in production ## Testing Your Implementation ### Local Testing with Demo Test the library without real OAuth credentials: ```bash # Run interactive demo bun run example:demo # The demo includes a mock OAuth server for testing ``` ### Testing with Real Providers ::: code-group ```bash [GitHub] # Set credentials in .env file GITHUB_CLIENT_ID=your_client_id GITHUB_CLIENT_SECRET=your_client_secret # Run example bun run example:github ``` ```bash [Notion MCP] # No credentials needed - uses Dynamic Client Registration bun run example:notion ``` ::: ## Troubleshooting ### Common Issues and Solutions ::: details Port Already in Use If port 3000 is already in use: ```typescript const result = await getAuthCode({ authorizationUrl: authUrl, port: 8080, // Use a different port }); ``` Also update your OAuth app's redirect URI to match. ::: ::: details Browser Doesn't Open If you're in a headless environment or the browser doesn't open: ```typescript // Headless mode - print URL for manual opening console.log(`Please open: ${authUrl}`); const result = await getAuthCode({ port: 3000, timeout: 120000 }); ``` ::: ::: details Firewall Warnings On first run, your OS firewall may show a warning. Allow connections for: * **localhost** only * The specific port you're using (default: 3000) ::: ::: details Token Refresh Errors For MCP apps with token refresh issues: ```typescript import open from "open"; const authProvider = browserAuth({ launch: open, store: fileStore(), // Use persistent storage authTimeout: 300000, // Increase timeout to 5 minutes }); ``` ::: ## Getting Help Need assistance? Here are your options: * 📝 [GitHub Issues](https://github.com/kriasoft/oauth-callback/issues) - Report bugs or request features * 💬 [GitHub Discussions](https://github.com/kriasoft/oauth-callback/discussions) - Ask questions and share ideas * 💬 [Discord Community](https://discord.gg/zQQXyqvs5x) - Join our Discord server for real-time help and discussions * 📚 [Stack Overflow](https://stackoverflow.com/questions/tagged/oauth-callback) - Search or ask questions with the `oauth-callback` tag Happy coding! 🚀 --- --- url: /oauth-callback/examples/linear.md description: >- Integrate with Linear's Model Context Protocol server using OAuth Callback for seamless issue tracking and project management automation. --- # Linear MCP Example This example demonstrates how to integrate with Linear's Model Context Protocol (MCP) server using OAuth Callback's `browserAuth()` provider. Linear is a modern issue tracking and project management tool designed for high-performance teams. Through MCP integration, you can programmatically manage issues, projects, cycles, and more. ## Overview The Linear MCP integration enables powerful project management automation: * **Issue Management** - Create, update, and track issues programmatically * **Project Tracking** - Monitor project progress and milestones * **Cycle Management** - Work with sprints and development cycles * **Team Collaboration** - Access team data and workflows * **Real-time Updates** - Subscribe to changes via MCP resources ## Prerequisites Before starting with Linear MCP integration: * **Runtime Environment** - Bun, Node.js 18+, or Deno installed * **Linear Account** - Active Linear workspace with API access * **OAuth Application** - Linear OAuth app configured (or use DCR if supported) * **Port Availability** - Port 3000 (or custom) for OAuth callback * **Browser Access** - Default browser for authorization flow ## Installation Install the required dependencies: ::: code-group ```bash [Bun] bun add oauth-callback @modelcontextprotocol/sdk ``` ```bash [npm] npm install oauth-callback @modelcontextprotocol/sdk ``` ```bash [pnpm] pnpm add oauth-callback @modelcontextprotocol/sdk ``` ::: ## Linear OAuth Setup ### Creating a Linear OAuth Application 1. Navigate to [Linear Settings > API](https://linear.app/settings/api) 2. Click "Create new OAuth application" 3. Configure your application: * **Application name**: Your app name * **Redirect URI**: `http://localhost:3000/callback` * **Scopes**: Select required permissions (read, write, admin) 4. Save your credentials: * Client ID * Client Secret ### Required Scopes Select scopes based on your needs: | Scope | Description | | ----------------- | ------------------------------------------ | | `read` | Read access to issues, projects, and teams | | `write` | Create and modify issues and comments | | `admin` | Manage team settings and workflows | | `issues:create` | Create new issues | | `issues:update` | Update existing issues | | `comments:create` | Add comments to issues | ## Basic Implementation ### Simple Linear Connection Here's a basic example connecting to Linear's MCP server: ```typescript import open from "open"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { browserAuth, fileStore } from "oauth-callback/mcp"; async function connectToLinear() { console.log("🚀 Connecting to Linear MCP Server\n"); // Linear MCP endpoint (hypothetical - check Linear docs) const serverUrl = new URL("https://mcp.linear.app"); // Create OAuth provider with credentials const authProvider = browserAuth({ launch: open, clientId: process.env.LINEAR_CLIENT_ID, clientSecret: process.env.LINEAR_CLIENT_SECRET, scope: "read write issues:create issues:update", port: 3000, store: fileStore("~/.mcp/linear-tokens.json"), onRequest(req) { console.log(`[OAuth] ${new URL(req.url).pathname}`); }, }); try { // Create MCP transport const transport = new StreamableHTTPClientTransport(serverUrl, { authProvider, }); // Initialize MCP client const client = new Client( { name: "linear-automation", version: "1.0.0" }, { capabilities: {} }, ); // Connect to Linear await client.connect(transport); console.log("✅ Connected to Linear MCP!"); // List available capabilities const tools = await client.listTools(); console.log("\n📝 Available tools:", tools); await client.close(); } catch (error) { console.error("❌ Connection failed:", error); } } connectToLinear(); ``` ## OAuth Flow Details ### Authorization Flow Diagram ```mermaid sequenceDiagram participant App as Your Application participant OAuth as OAuth Callback participant Browser participant Linear as Linear OAuth participant MCP as Linear MCP App->>OAuth: Initialize browserAuth App->>MCP: Connect to Linear MCP MCP-->>App: 401 Unauthorized Note over OAuth,Browser: OAuth Authorization OAuth->>Browser: Open Linear OAuth URL Browser->>Linear: Request authorization Linear->>Browser: Show consent screen Browser->>Linear: User approves Linear->>Browser: Redirect to localhost:3000 Browser->>OAuth: GET /callback?code=xxx OAuth->>App: Capture auth code Note over App,Linear: Token Exchange App->>Linear: POST /oauth/token Linear-->>App: Access & refresh tokens OAuth->>OAuth: Store tokens App->>MCP: Reconnect with token MCP-->>App: 200 OK + capabilities ``` ### Configuration Options Configure the OAuth provider for Linear: ```typescript const authProvider = browserAuth({ // Browser launch callback launch: open, // OAuth credentials clientId: process.env.LINEAR_CLIENT_ID, clientSecret: process.env.LINEAR_CLIENT_SECRET, // Required permissions scope: "read write issues:create issues:update comments:create", // Server configuration port: 3000, hostname: "localhost", callbackPath: "/callback", // Token storage store: fileStore("~/.mcp/linear.json"), storeKey: "linear-production", // Timeouts authTimeout: 300000, // 5 minutes // Debugging onRequest(req) { const url = new URL(req.url); console.log(`[${new Date().toISOString()}] ${req.method} ${url.pathname}`); }, }); ``` ## Working with Linear MCP ### Issue Management Create and manage Linear issues through MCP: ```typescript // Create a new issue const newIssue = await client.callTool("create_issue", { title: "Fix authentication bug", description: "Users unable to login with SSO", teamId: "ENG", priority: 1, // Urgent labelIds: ["bug", "authentication"], assigneeId: "user_123", }); console.log("Created issue:", newIssue.identifier); // Update issue status await client.callTool("update_issue", { issueId: newIssue.id, stateId: "in_progress", }); // Add a comment await client.callTool("add_comment", { issueId: newIssue.id, body: "Started investigation - found root cause in SSO handler", }); // Search for issues const searchResults = await client.callTool("search_issues", { query: "authentication bug", teamId: "ENG", state: ["todo", "in_progress"], limit: 10, }); ``` ### Project Management Work with Linear projects and milestones: ```typescript // Get project details const project = await client.callTool("get_project", { projectId: "PROJ-123", }); // Update project progress await client.callTool("update_project", { projectId: "PROJ-123", progress: 0.75, // 75% complete status: "on_track", }); // List project issues const projectIssues = await client.callTool("list_project_issues", { projectId: "PROJ-123", includeArchived: false, }); // Create milestone const milestone = await client.callTool("create_milestone", { name: "v2.0 Release", targetDate: "2024-06-01", projectId: "PROJ-123", }); ``` ### Cycle Management Manage development cycles (sprints): ```typescript // Get current cycle const currentCycle = await client.callTool("get_current_cycle", { teamId: "ENG", }); // List cycle issues const cycleIssues = await client.callTool("list_cycle_issues", { cycleId: currentCycle.id, }); // Move issue to next cycle await client.callTool("update_issue", { issueId: "ISS-456", cycleId: currentCycle.nextCycle.id, }); // Get cycle analytics const analytics = await client.callTool("get_cycle_analytics", { cycleId: currentCycle.id, }); console.log("Cycle completion:", analytics.completionRate); console.log("Issues completed:", analytics.completedCount); ``` ### Resource Subscriptions Subscribe to Linear resources for real-time updates: ```typescript // Subscribe to team updates await client.subscribeToResource({ uri: "linear://team/ENG", }); // Subscribe to project changes await client.subscribeToResource({ uri: "linear://project/PROJ-123", }); // Handle resource updates client.on("resource_updated", (resource) => { console.log("Resource updated:", resource.uri); if (resource.uri.startsWith("linear://issue/")) { console.log("Issue changed:", resource.data); } }); ``` ## Advanced Patterns ### Custom Linear Client Class Create a reusable Linear MCP client: ```typescript import open from "open"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { browserAuth, fileStore } from "oauth-callback/mcp"; class LinearMCPClient { private client?: Client; private authProvider: any; constructor(options?: { clientId?: string; clientSecret?: string; storePath?: string; }) { this.authProvider = browserAuth({ launch: open, clientId: options?.clientId || process.env.LINEAR_CLIENT_ID, clientSecret: options?.clientSecret || process.env.LINEAR_CLIENT_SECRET, scope: "read write issues:create issues:update", store: fileStore(options?.storePath || "~/.mcp/linear.json"), port: 3000, authTimeout: 300000, }); } async connect(): Promise { const serverUrl = new URL("https://mcp.linear.app"); const transport = new StreamableHTTPClientTransport(serverUrl, { authProvider: this.authProvider, }); this.client = new Client( { name: "linear-client", version: "1.0.0" }, { capabilities: {} }, ); await this.client.connect(transport); console.log("✅ Connected to Linear MCP"); } async createIssue(params: { title: string; description?: string; teamId: string; priority?: number; labels?: string[]; }): Promise { if (!this.client) throw new Error("Not connected"); return await this.client.callTool("create_issue", { title: params.title, description: params.description, teamId: params.teamId, priority: params.priority || 3, labelIds: params.labels || [], }); } async searchIssues(query: string, teamId?: string): Promise { if (!this.client) throw new Error("Not connected"); return await this.client.callTool("search_issues", { query, teamId, limit: 20, }); } async updateIssueStatus(issueId: string, status: string): Promise { if (!this.client) throw new Error("Not connected"); const states = { todo: "state_todo_id", in_progress: "state_in_progress_id", done: "state_done_id", cancelled: "state_cancelled_id", }; await this.client.callTool("update_issue", { issueId, stateId: states[status] || status, }); } async addComment(issueId: string, comment: string): Promise { if (!this.client) throw new Error("Not connected"); return await this.client.callTool("add_comment", { issueId, body: comment, }); } async disconnect(): Promise { if (this.client) { await this.client.close(); this.client = undefined; } } } // Usage const linear = new LinearMCPClient(); await linear.connect(); const issue = await linear.createIssue({ title: "Implement OAuth integration", description: "Add OAuth support for third-party services", teamId: "ENG", priority: 2, labels: ["feature", "authentication"], }); await linear.updateIssueStatus(issue.id, "in_progress"); await linear.addComment(issue.id, "Started implementation"); await linear.disconnect(); ``` ### Automation Workflows Build powerful automations with Linear MCP: ```typescript // Auto-triage incoming issues async function autoTriageIssues(client: Client) { // Get untriaged issues const untriaged = await client.callTool("search_issues", { query: "no:assignee no:priority", state: ["todo"], limit: 50, }); for (const issue of untriaged.issues) { // Analyze issue content const keywords = issue.title.toLowerCase() + " " + issue.description.toLowerCase(); // Auto-assign based on keywords let assignee = null; let priority = 3; // Default: Medium if (keywords.includes("crash") || keywords.includes("down")) { priority = 1; // Urgent assignee = "oncall_engineer"; } else if ( keywords.includes("security") || keywords.includes("vulnerability") ) { priority = 1; assignee = "security_team"; } else if (keywords.includes("performance") || keywords.includes("slow")) { priority = 2; // High assignee = "performance_team"; } // Update issue await client.callTool("update_issue", { issueId: issue.id, priority, assigneeId: assignee, labelIds: ["auto-triaged"], }); console.log( `Triaged: ${issue.identifier} -> P${priority} ${assignee || "unassigned"}`, ); } } // Sync Linear issues with external systems async function syncWithJira(client: Client, jiraClient: any) { // Get recent Linear issues const recentIssues = await client.callTool("search_issues", { createdAfter: new Date(Date.now() - 86400000).toISOString(), // Last 24h limit: 100, }); for (const issue of recentIssues.issues) { // Check if already synced if (issue.metadata?.jiraKey) continue; // Create in Jira const jiraIssue = await jiraClient.createIssue({ summary: issue.title, description: issue.description, issueType: "Task", project: "PROJ", }); // Update Linear with Jira reference await client.callTool("update_issue", { issueId: issue.id, metadata: { jiraKey: jiraIssue.key, syncedAt: new Date().toISOString(), }, }); // Add sync comment await client.callTool("add_comment", { issueId: issue.id, body: `🔄 Synced to Jira: [${jiraIssue.key}](https://jira.example.com/browse/${jiraIssue.key})`, }); } } ``` ### Batch Operations Efficiently handle multiple operations: ```typescript class LinearBatchProcessor { constructor(private client: Client) {} async batchCreateIssues( issues: Array<{ title: string; description?: string; teamId: string; }>, ): Promise { const results = []; // Process in parallel with concurrency limit const batchSize = 5; for (let i = 0; i < issues.length; i += batchSize) { const batch = issues.slice(i, i + batchSize); const promises = batch.map((issue) => this.client.callTool("create_issue", issue), ); const batchResults = await Promise.all(promises); results.push(...batchResults); console.log( `Created batch ${i / batchSize + 1}: ${batchResults.length} issues`, ); } return results; } async bulkUpdatePriority( issueIds: string[], priority: number, ): Promise { const promises = issueIds.map((id) => this.client.callTool("update_issue", { issueId: id, priority, }), ); await Promise.all(promises); console.log(`Updated priority for ${issueIds.length} issues`); } async archiveCompletedIssues( teamId: string, olderThanDays = 30, ): Promise { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - olderThanDays); const completed = await this.client.callTool("search_issues", { teamId, state: ["done", "cancelled"], completedBefore: cutoffDate.toISOString(), limit: 100, }); for (const issue of completed.issues) { await this.client.callTool("archive_issue", { issueId: issue.id, }); } return completed.issues.length; } } ``` ## Error Handling ### Common Error Scenarios Handle Linear-specific errors gracefully: ```typescript async function robustLinearOperation( client: Client, operation: () => Promise, ) { const maxRetries = 3; let lastError; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return await operation(); } catch (error: any) { lastError = error; // Handle specific Linear errors if (error.message.includes("RATE_LIMITED")) { // Rate limit - exponential backoff const delay = Math.pow(2, attempt) * 1000; console.log(`Rate limited. Waiting ${delay}ms...`); await new Promise((resolve) => setTimeout(resolve, delay)); continue; } if (error.message.includes("INVALID_TEAM_ID")) { throw new Error( "Invalid team ID. Check your Linear workspace settings.", ); } if (error.message.includes("INSUFFICIENT_PERMISSIONS")) { throw new Error( "Missing permissions. Request additional OAuth scopes.", ); } if (error.message.includes("RESOURCE_NOT_FOUND")) { throw new Error("Linear resource not found. It may have been deleted."); } // Network errors - retry if ( error.message.includes("ECONNRESET") || error.message.includes("ETIMEDOUT") ) { console.log(`Network error on attempt ${attempt}. Retrying...`); continue; } // Unknown error - don't retry throw error; } } throw lastError; } // Usage const result = await robustLinearOperation(client, async () => { return await client.callTool("create_issue", { title: "New feature request", teamId: "ENG", }); }); ``` ## Security Best Practices ### Token Management Secure your Linear OAuth tokens: ```typescript import { browserAuth, fileStore } from "oauth-callback/mcp"; import { createCipheriv, createDecipheriv, randomBytes, scrypt } from "crypto"; import { promisify } from "util"; // Encrypted token storage class EncryptedLinearStore { private key: Buffer; private store = fileStore("~/.mcp/linear-encrypted.json"); async init(password: string) { const salt = randomBytes(16); this.key = (await promisify(scrypt)(password, salt, 32)) as Buffer; } async get(key: string): Promise { const encrypted = await this.store.get(key); if (!encrypted) return null; // Decrypt tokens return this.decrypt(encrypted); } async set(key: string, tokens: any): Promise { // Encrypt before storing const encrypted = this.encrypt(tokens); await this.store.set(key, encrypted); } private encrypt(data: any): string { const iv = randomBytes(16); const cipher = createCipheriv("aes-256-gcm", this.key, iv); // ... encryption logic return encrypted; } private decrypt(encrypted: string): any { // ... decryption logic return decrypted; } } ``` ### Environment Configuration Use environment variables for sensitive data: ```bash # .env file (never commit!) LINEAR_CLIENT_ID=lin_oauth_client_xxx LINEAR_CLIENT_SECRET=lin_oauth_secret_xxx LINEAR_WORKSPACE_ID=workspace_123 LINEAR_TEAM_ID=team_eng ``` ```typescript // Load configuration import open from "open"; import { config } from "dotenv"; config(); const authProvider = browserAuth({ launch: open, clientId: process.env.LINEAR_CLIENT_ID!, clientSecret: process.env.LINEAR_CLIENT_SECRET!, scope: "read write", store: fileStore(), }); ``` ## Testing ### Mock Linear MCP Server Test your integration without hitting Linear's API: ```typescript import { createServer } from "http"; class MockLinearMCPServer { private server: any; private issues = new Map(); async start(port = 4000) { this.server = createServer((req, res) => { // Mock MCP endpoints if (req.url === "/capabilities") { res.writeHead(200, { "Content-Type": "application/json" }); res.end( JSON.stringify({ tools: [ { name: "create_issue", description: "Create issue" }, { name: "update_issue", description: "Update issue" }, { name: "search_issues", description: "Search issues" }, ], }), ); } // ... other endpoints }); await new Promise((resolve) => { this.server.listen(port, resolve); }); } async stop() { await new Promise((resolve) => this.server.close(resolve)); } } // Test usage describe("Linear MCP Integration", () => { let mockServer: MockLinearMCPServer; beforeAll(async () => { mockServer = new MockLinearMCPServer(); await mockServer.start(); }); afterAll(async () => { await mockServer.stop(); }); it("should create an issue", async () => { // Test implementation }); }); ``` ## Troubleshooting ### Common Issues ::: details OAuth authorization fails Check your Linear OAuth app configuration: ```typescript // Verify redirect URI matches const authProvider = browserAuth({ launch: open, clientId: "...", clientSecret: "...", port: 3000, // Must match redirect URI port callbackPath: "/callback", // Must match redirect URI path }); ``` ::: ::: details Rate limiting errors Implement rate limit handling: ```typescript class RateLimitedClient { private requestCount = 0; private resetTime = Date.now() + 60000; async callTool(name: string, params: any) { // Check rate limit if (Date.now() > this.resetTime) { this.requestCount = 0; this.resetTime = Date.now() + 60000; } if (this.requestCount >= 100) { const waitTime = this.resetTime - Date.now(); await new Promise((resolve) => setTimeout(resolve, waitTime)); this.requestCount = 0; } this.requestCount++; return await this.client.callTool(name, params); } } ``` ::: ::: details Token refresh fails Handle token refresh errors: ```typescript const authProvider = browserAuth({ launch: open, store: fileStore(), onTokenRefreshError: async (error) => { console.error("Token refresh failed:", error); // Clear invalid tokens await authProvider.invalidateCredentials("tokens"); // Trigger re-authentication throw new Error("Re-authentication required"); }, }); ``` ::: ## Related Resources * [Linear API Documentation](https://developers.linear.app/docs/graphql/working-with-the-graphql-api) - Official Linear API reference * [browserAuth API](/api/browser-auth) - OAuth provider documentation * [MCP SDK Documentation](https://modelcontextprotocol.io/docs) - Model Context Protocol reference --- --- url: /oauth-callback/markdown-examples.md --- # Markdown Extension Examples This page demonstrates some of the built-in markdown extensions provided by VitePress. ## Syntax Highlighting VitePress provides Syntax Highlighting powered by [Shiki](https://github.com/shikijs/shiki), with additional features like line-highlighting: **Input** ````md ```js{4} export default { data () { return { msg: 'Highlighted!' } } } ``` ```` **Output** ```js{4} export default { data () { return { msg: 'Highlighted!' } } } ``` ## Custom Containers **Input** ```md ::: info This is an info box. ::: ::: tip This is a tip. ::: ::: warning This is a warning. ::: ::: danger This is a dangerous warning. ::: ::: details This is a details block. ::: ``` **Output** ::: info This is an info box. ::: ::: tip This is a tip. ::: ::: warning This is a warning. ::: ::: danger This is a dangerous warning. ::: ::: details This is a details block. ::: ## More Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown). --- --- url: /oauth-callback/examples/notion.md description: >- Connect to Notion's Model Context Protocol server using OAuth Callback with Dynamic Client Registration - no pre-configured OAuth app required. --- # Notion MCP Example This example demonstrates how to connect to Notion's Model Context Protocol (MCP) server using OAuth Callback's `browserAuth()` provider with Dynamic Client Registration (DCR). Unlike traditional OAuth flows that require pre-registering an OAuth application, this example shows how to automatically register and authenticate with Notion's authorization server. ## Overview The Notion MCP integration showcases several advanced features: * **Dynamic Client Registration (RFC 7591)** - Automatic OAuth client registration * **Model Context Protocol Integration** - Seamless MCP SDK authentication * **Browser-Based Authorization** - Automatic browser opening for user consent * **Token Management** - Automatic token storage and retrieval * **Zero Configuration** - No client ID or secret required ## Prerequisites Before running this example, ensure you have: * **Bun, Node.js 18+, or Deno** installed * **Port 3000** available for the OAuth callback server * **Default browser** configured for opening authorization URLs * **Internet connection** to reach Notion's servers ## Installation Install the required dependencies: ::: code-group ```bash [Bun] bun add oauth-callback @modelcontextprotocol/sdk ``` ```bash [npm] npm install oauth-callback @modelcontextprotocol/sdk ``` ```bash [pnpm] pnpm add oauth-callback @modelcontextprotocol/sdk ``` ::: ## Quick Start ### Running the Example The simplest way to run the Notion MCP example: ```bash # Clone the repository git clone https://github.com/kriasoft/oauth-callback.git cd oauth-callback # Install dependencies bun install # Run the Notion example bun run example:notion ``` The example will: 1. Start a local OAuth callback server on port 3000 2. Open your browser to Notion's authorization page 3. Capture the authorization code after you approve 4. Exchange the code for access tokens 5. Connect to Notion's MCP server 6. Display available tools and resources ## Complete Example Code Here's the full implementation demonstrating Notion MCP integration: ```typescript #!/usr/bin/env bun import open from "open"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { browserAuth, inMemoryStore } from "oauth-callback/mcp"; async function connectToNotion() { console.log("🚀 Starting OAuth flow with Notion MCP Server\n"); const serverUrl = new URL("https://mcp.notion.com/mcp"); // Create OAuth provider - no client_id or client_secret needed! const authProvider = browserAuth({ launch: open, port: 3000, scope: "read write", store: inMemoryStore(), // Use fileStore() for persistence onRequest(req) { const url = new URL(req.url); console.log(`📨 OAuth: ${req.method} ${url.pathname}`); }, }); try { // Create MCP transport with OAuth provider const transport = new StreamableHTTPClientTransport(serverUrl, { authProvider, }); // Create MCP client const client = new Client( { name: "notion-example", version: "1.0.0" }, { capabilities: {} }, ); // Connect triggers OAuth flow if needed await client.connect(transport); console.log("✅ Connected to Notion MCP server!"); // List available tools const tools = await client.listTools(); console.log("\n📝 Available tools:"); for (const tool of tools.tools || []) { console.log(` - ${tool.name}: ${tool.description}`); } // List available resources const resources = await client.listResources(); console.log("\n📂 Available resources:"); for (const resource of resources.resources || []) { console.log(` - ${resource.uri}: ${resource.name}`); } await client.close(); } catch (error) { console.error("❌ Connection failed:", error); } } connectToNotion(); ``` ## How It Works ### OAuth Flow Sequence ```mermaid sequenceDiagram participant App as Your App participant OAuth as OAuth Callback participant Browser participant Notion as Notion Auth participant MCP as Notion MCP App->>OAuth: Create browserAuth provider App->>MCP: Connect via transport MCP-->>App: 401 Unauthorized Note over App,OAuth: Dynamic Client Registration App->>Notion: POST /oauth/register Notion-->>App: client_id, client_secret OAuth->>OAuth: Store credentials Note over OAuth,Browser: Authorization Flow OAuth->>Browser: Open authorization URL Browser->>Notion: User authorizes Notion->>Browser: Redirect to localhost:3000 Browser->>OAuth: GET /callback?code=xxx OAuth->>App: Return auth code Note over App,MCP: Token Exchange App->>Notion: POST /oauth/token Notion-->>App: access_token, refresh_token OAuth->>OAuth: Store tokens App->>MCP: Connect with token MCP-->>App: 200 OK + capabilities ``` ### Dynamic Client Registration Unlike traditional OAuth, Notion's MCP server supports Dynamic Client Registration (RFC 7591): 1. **No Pre-Registration** - You don't need to manually register an OAuth app 2. **Automatic Registration** - The client registers itself on first use 3. **Credential Persistence** - Client credentials are stored for reuse 4. **Simplified Distribution** - Ship apps without OAuth setup instructions ## Key Features ### Browser-Based Authorization The `browserAuth()` provider handles the complete OAuth flow: ```typescript import open from "open"; const authProvider = browserAuth({ launch: open, // Opens browser for authorization port: 3000, // Callback server port scope: "read write", // Requested permissions store: inMemoryStore(), // Token storage onRequest(req) { // Request logging console.log(`OAuth: ${req.url}`); }, }); ``` ### Token Storage Options Choose between ephemeral and persistent storage: ::: code-group ```typescript [Ephemeral Storage] // Tokens lost on restart (more secure) const authProvider = browserAuth({ launch: open, store: inMemoryStore(), }); ``` ```typescript [Persistent Storage] // Tokens saved to disk (convenient) import { fileStore } from "oauth-callback/mcp"; const authProvider = browserAuth({ launch: open, store: fileStore(), // Default: ~/.mcp/tokens.json }); ``` ```typescript [Custom Location] // Specify custom file path const authProvider = browserAuth({ launch: open, store: fileStore("~/my-app/notion-tokens.json"), }); ``` ::: ### Error Handling Properly handle authentication failures: ```typescript try { await client.connect(transport); } catch (error) { if (error.message.includes("Unauthorized")) { console.log("Authorization required - check browser"); } else if (error.message.includes("access_denied")) { console.log("User cancelled authorization"); } else { console.error("Connection failed:", error); } } ``` ## Working with Notion MCP ### Available Tools Once connected, you can use Notion's MCP tools: ```typescript // Search for content const searchResults = await client.callTool("search_objects", { query: "meeting notes", limit: 10, }); // Create a new page const newPage = await client.callTool("create_page", { title: "My New Page", content: "Page content here", }); // Update existing content const updated = await client.callTool("update_page", { page_id: "page-123", content: "Updated content", }); ``` ### Available Resources Access Notion resources through MCP: ```typescript // List all resources const resources = await client.listResources(); // Read a specific resource const pageContent = await client.readResource({ uri: "notion://page/page-123", }); // Subscribe to changes await client.subscribeToResource({ uri: "notion://database/db-456", }); ``` ## Advanced Configuration ### Custom Success Pages Provide branded callback pages: ```typescript const authProvider = browserAuth({ successHtml: ` Notion Connected!

✨ Connected to Notion!

You can close this window and return to your app.

`, }); ``` ### Request Logging Debug OAuth flow with detailed logging: ```typescript import open from "open"; const authProvider = browserAuth({ launch: open, onRequest(req) { const url = new URL(req.url); const timestamp = new Date().toISOString(); console.log(`[${timestamp}] OAuth Request`); console.log(` Method: ${req.method}`); console.log(` Path: ${url.pathname}`); if (url.pathname === "/callback") { console.log(` Code: ${url.searchParams.get("code")}`); console.log(` State: ${url.searchParams.get("state")}`); } }, }); ``` ### Multi-Account Support Support multiple Notion accounts: ```typescript import open from "open"; function createNotionAuth(accountName: string) { return browserAuth({ launch: open, store: fileStore(`~/.mcp/notion-${accountName}.json`), storeKey: `notion-${accountName}`, port: 3000 + Math.floor(Math.random() * 1000), // Random port }); } // Use different accounts const personalAuth = createNotionAuth("personal"); const workAuth = createNotionAuth("work"); ``` ## Troubleshooting ### Common Issues and Solutions ::: details Browser doesn't open automatically If you're in a headless environment: ```typescript const authProvider = browserAuth({ launch: () => {}, // Noop - disable browser opening }); // Manually instruct user console.log("Please open this URL in your browser:"); console.log(authorizationUrl); ``` ::: ::: details Port 3000 is already in use Use a different port for the callback server: ```typescript const authProvider = browserAuth({ port: 8080, // Use alternative port }); ``` ::: ::: details Tokens not persisting Ensure you're using file storage, not in-memory: ```typescript import open from "open"; import { fileStore } from "oauth-callback/mcp"; const authProvider = browserAuth({ launch: open, store: fileStore(), // ✅ Persistent storage // store: inMemoryStore() // ❌ Lost on restart }); ``` ::: ::: details Authorization fails repeatedly Clear stored credentials and try again: ```typescript // Clear all stored data await authProvider.invalidateCredentials("all"); // Or clear specific data await authProvider.invalidateCredentials("tokens"); await authProvider.invalidateCredentials("client"); ``` ::: ## Security Considerations ### Best Practices 1. **Use Ephemeral Storage for Sensitive Data** ```typescript import open from "open"; // Tokens are never written to disk const authProvider = browserAuth({ launch: open, store: inMemoryStore(), }); ``` 2. **Validate State Parameter** * The library automatically generates and validates state parameters * Prevents CSRF attacks during authorization 3. **PKCE Protection** * Enabled by default for enhanced security * Prevents authorization code interception 4. **Secure File Permissions** * File storage uses mode 0600 (owner read/write only) * Tokens are protected from other users on the system ### Token Security ::: warning Never commit tokens to version control: ```bash # Add to .gitignore ~/.mcp/ *.json tokens.json ``` ::: ## Complete Working Example For a production-ready implementation with full error handling: ```typescript import open from "open"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { browserAuth, fileStore } from "oauth-callback/mcp"; class NotionMCPClient { private client?: Client; private authProvider: any; constructor() { this.authProvider = browserAuth({ launch: open, port: 3000, scope: "read write", store: fileStore("~/.mcp/notion.json"), authTimeout: 300000, // 5 minutes onRequest: this.logRequest.bind(this), }); } private logRequest(req: Request) { const url = new URL(req.url); console.log(`[OAuth] ${req.method} ${url.pathname}`); } async connect(): Promise { const serverUrl = new URL("https://mcp.notion.com/mcp"); const transport = new StreamableHTTPClientTransport(serverUrl, { authProvider: this.authProvider, }); this.client = new Client( { name: "notion-client", version: "1.0.0" }, { capabilities: {} }, ); try { await this.client.connect(transport); console.log("✅ Connected to Notion MCP"); } catch (error: any) { if (error.message.includes("Unauthorized")) { throw new Error("Authorization required. Please check your browser."); } throw error; } } async search(query: string): Promise { if (!this.client) throw new Error("Not connected"); return await this.client.callTool("search_objects", { query, limit: 10, }); } async disconnect(): Promise { if (this.client) { await this.client.close(); this.client = undefined; } } } // Usage async function main() { const notion = new NotionMCPClient(); try { await notion.connect(); const results = await notion.search("project roadmap"); console.log("Search results:", results); await notion.disconnect(); } catch (error) { console.error("Error:", error); } } main(); ``` ## Related Resources * [browserAuth API Documentation](/api/browser-auth) - Complete API reference * [Storage Providers](/api/storage-providers) - Token storage options * [Core Concepts](/core-concepts) - OAuth and MCP architecture * [GitHub Example](https://github.com/kriasoft/oauth-callback/blob/main/examples/notion.ts) - Source code --- --- url: /oauth-callback/api/oauth-error.md description: >- OAuth-specific error class for handling authorization failures and provider errors according to RFC 6749. --- # OAuthError The `OAuthError` class represents OAuth-specific errors that occur during the authorization flow. It extends the standard JavaScript `Error` class and provides structured access to OAuth error details as defined in [RFC 6749 Section 4.1.2.1](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1). ## Class Definition ```typescript class OAuthError extends Error { error: string; // OAuth error code error_description?: string; // Human-readable description error_uri?: string; // URI with more information constructor(error: string, description?: string, uri?: string); } ``` ## Properties | Property | Type | Description | | ------------------- | --------------------- | ------------------------------------------------ | | `name` | `string` | Always `"OAuthError"` for instanceof checks | | `error` | `string` | OAuth error code (e.g., `"access_denied"`) | | `error_description` | `string \| undefined` | Human-readable error description | | `error_uri` | `string \| undefined` | URI with additional error information | | `message` | `string` | Inherited from Error (description or error code) | | `stack` | `string` | Inherited stack trace from Error | ## OAuth Error Codes According to [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1) and common provider extensions: ### Standard OAuth 2.0 Error Codes | Error Code | Description | User Action Required | | --------------------------- | --------------------------------------------------- | --------------------------- | | `invalid_request` | Request is missing required parameters or malformed | Fix request parameters | | `unauthorized_client` | Client not authorized for this grant type | Check client configuration | | `access_denied` | User denied the authorization request | User must approve access | | `unsupported_response_type` | Response type not supported | Use supported response type | | `invalid_scope` | Requested scope is invalid or unknown | Request valid scopes | | `server_error` | Authorization server error (500) | Retry later | | `temporarily_unavailable` | Server temporarily overloaded (503) | Retry with backoff | ### Common Provider Extensions | Error Code | Provider | Description | | ---------------------------- | ---------------- | ------------------------------ | | `consent_required` | Microsoft | User consent needed | | `interaction_required` | Microsoft | User interaction needed | | `login_required` | Google/Microsoft | User must authenticate | | `account_selection_required` | Microsoft | User must select account | | `invalid_client` | Various | Client authentication failed | | `invalid_grant` | Various | Grant or refresh token invalid | ## Basic Usage ### Catching OAuth Errors ```typescript import { getAuthCode, OAuthError } from "oauth-callback"; try { const result = await getAuthCode(authorizationUrl); console.log("Success! Code:", result.code); } catch (error) { if (error instanceof OAuthError) { console.error(`OAuth error: ${error.error}`); if (error.error_description) { console.error(`Details: ${error.error_description}`); } if (error.error_uri) { console.error(`More info: ${error.error_uri}`); } } else { // Handle other errors (timeout, network, etc.) console.error("Unexpected error:", error); } } ``` ### Type Guard ```typescript function isOAuthError(error: unknown): error is OAuthError { return error instanceof OAuthError; } // Usage catch (error) { if (isOAuthError(error)) { // TypeScript knows error is OAuthError handleOAuthError(error.error, error.error_description); } } ``` ## Error Handling Patterns ### Comprehensive Error Handler ```typescript import { OAuthError } from "oauth-callback"; async function handleOAuthFlow(authUrl: string) { try { const result = await getAuthCode(authUrl); return result.code; } catch (error) { if (error instanceof OAuthError) { switch (error.error) { case "access_denied": // User cancelled - this is expected console.log("User cancelled authorization"); return null; case "invalid_scope": throw new Error( `Invalid permissions requested: ${error.error_description}`, ); case "server_error": case "temporarily_unavailable": // Retry with exponential backoff console.warn("Server error, retrying..."); await delay(1000); return handleOAuthFlow(authUrl); case "unauthorized_client": throw new Error( "Application not authorized. Please check OAuth app settings.", ); default: // Unknown OAuth error throw new Error( `OAuth authorization failed: ${error.error_description || error.error}`, ); } } // Non-OAuth errors throw error; } } ``` ### User-Friendly Error Messages ```typescript function getErrorMessage(error: OAuthError): string { const messages: Record = { access_denied: "You cancelled the authorization. Please try again when ready.", invalid_scope: "The requested permissions are not available.", server_error: "The authorization server encountered an error. Please try again.", temporarily_unavailable: "The service is temporarily unavailable. Please try again later.", unauthorized_client: "This application is not authorized. Please contact support.", invalid_request: "The authorization request was invalid. Please try again.", consent_required: "Please provide consent to continue.", login_required: "Please log in to continue.", interaction_required: "Additional interaction is required. Please complete the authorization in your browser." }; return messages[error.error] || error.error_description || `Authorization failed: ${error.error}`; } // Usage catch (error) { if (error instanceof OAuthError) { const userMessage = getErrorMessage(error); showUserNotification(userMessage); } } ``` ### Retry Logic ```typescript async function authorizeWithRetry( authUrl: string, maxAttempts = 3, ): Promise { let lastError: Error | undefined; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { const result = await getAuthCode(authUrl); return result.code; } catch (error) { lastError = error as Error; if (error instanceof OAuthError) { // Don't retry user-actionable errors if ( ["access_denied", "invalid_scope", "unauthorized_client"].includes( error.error, ) ) { throw error; } // Retry server errors if (["server_error", "temporarily_unavailable"].includes(error.error)) { const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000); console.warn(`Attempt ${attempt} failed, retrying in ${delay}ms...`); await new Promise((resolve) => setTimeout(resolve, delay)); continue; } } // Don't retry other errors throw error; } } throw lastError || new Error("Authorization failed after retries"); } ``` ## Error Recovery Strategies ### Graceful Degradation ```typescript class OAuthService { private cachedToken?: string; async getAccessToken(): Promise { try { // Try to get fresh token const code = await getAuthCode(this.authUrl); const token = await this.exchangeCodeForToken(code); this.cachedToken = token; return token; } catch (error) { if (error instanceof OAuthError) { if (error.error === "access_denied") { // User cancelled - try using cached token if available if (this.cachedToken) { console.log("Using cached token after user cancellation"); return this.cachedToken; } return null; } if (error.error === "temporarily_unavailable" && this.cachedToken) { // Service down - use cached token console.warn("OAuth service unavailable, using cached token"); return this.cachedToken; } } throw error; } } } ``` ### Error Logging ```typescript import { OAuthError } from "oauth-callback"; function logOAuthError(error: OAuthError, context: Record) { const errorLog = { timestamp: new Date().toISOString(), type: "oauth_error", error_code: error.error, error_description: error.error_description, error_uri: error.error_uri, context, stack: error.stack }; // Send to logging service if (["server_error", "temporarily_unavailable"].includes(error.error)) { console.error("OAuth provider error:", errorLog); // Report to monitoring service reportToMonitoring(errorLog); } else { console.warn("OAuth user error:", errorLog); // Track user analytics trackUserEvent("oauth_error", { code: error.error }); } } // Usage catch (error) { if (error instanceof OAuthError) { logOAuthError(error, { provider: "github", client_id: CLIENT_ID, scopes: ["user:email", "repo"] }); } } ``` ## Testing OAuth Errors ### Unit Testing ```typescript import { OAuthError } from "oauth-callback"; import { describe, it, expect } from "vitest"; describe("OAuthError", () => { it("should create error with all properties", () => { const error = new OAuthError( "invalid_scope", "The requested scope is invalid", "https://example.com/docs/scopes", ); expect(error).toBeInstanceOf(Error); expect(error).toBeInstanceOf(OAuthError); expect(error.name).toBe("OAuthError"); expect(error.error).toBe("invalid_scope"); expect(error.error_description).toBe("The requested scope is invalid"); expect(error.error_uri).toBe("https://example.com/docs/scopes"); expect(error.message).toBe("The requested scope is invalid"); }); it("should use error code as message when description is missing", () => { const error = new OAuthError("access_denied"); expect(error.message).toBe("access_denied"); }); }); ``` ### Mock OAuth Errors ```typescript import { OAuthError } from "oauth-callback"; class MockOAuthProvider { private shouldFail: string | null = null; simulateError(errorCode: string) { this.shouldFail = errorCode; } async authorize(): Promise { if (this.shouldFail) { throw new OAuthError( this.shouldFail, this.getErrorDescription(this.shouldFail), "https://example.com/oauth/errors", ); } return "mock_auth_code"; } private getErrorDescription(code: string): string { const descriptions: Record = { access_denied: "User denied access to the application", invalid_scope: "One or more scopes are invalid", server_error: "Authorization server encountered an error", }; return descriptions[code] || "Unknown error"; } } // Usage in tests describe("OAuth Flow", () => { it("should handle access_denied error", async () => { const provider = new MockOAuthProvider(); provider.simulateError("access_denied"); await expect(provider.authorize()).rejects.toThrow(OAuthError); await expect(provider.authorize()).rejects.toThrow("User denied access"); }); }); ``` ## Integration with MCP When using with the MCP SDK through `browserAuth()`: ```typescript import { browserAuth } from "oauth-callback/mcp"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; async function connectWithErrorHandling() { const authProvider = browserAuth({ store: fileStore(), onRequest: (req) => { // Log OAuth flow for debugging console.log(`OAuth: ${req.url}`); }, }); const client = new Client( { name: "my-app", version: "1.0.0" }, { capabilities: {} }, ); try { await client.connect(transport); } catch (error) { // OAuth errors from browserAuth are wrapped if (error.message?.includes("OAuthError")) { // Extract OAuth error details const match = error.message.match(/OAuthError: (\w+)/); if (match) { const errorCode = match[1]; console.error(`OAuth failed with code: ${errorCode}`); if (errorCode === "access_denied") { console.log("User cancelled MCP authorization"); return null; } } } throw error; } } ``` ## Error Flow Diagram ```mermaid flowchart TD Start([User initiates OAuth]) --> Auth[Open authorization URL] Auth --> Decision{User action} Decision -->|Approves| Success[Return code] Decision -->|Denies| Denied[OAuthError: access_denied] Decision -->|Invalid scope| Scope[OAuthError: invalid_scope] Decision -->|Timeout| Timeout[TimeoutError] Auth --> ServerCheck{Server status} ServerCheck -->|Error 500| ServerErr[OAuthError: server_error] ServerCheck -->|Error 503| Unavailable[OAuthError: temporarily_unavailable] ServerCheck -->|Invalid client| Unauthorized[OAuthError: unauthorized_client] Denied --> Handle[Error Handler] Scope --> Handle ServerErr --> Retry{Retry?} Unavailable --> Retry Unauthorized --> Handle Timeout --> Handle Retry -->|Yes| Auth Retry -->|No| Handle Handle --> UserMessage[Show user message] Success --> Complete([Complete]) style Denied fill:#f96 style Scope fill:#f96 style ServerErr fill:#fa6 style Unavailable fill:#fa6 style Unauthorized fill:#f96 style Timeout fill:#f96 style Success fill:#6f9 ``` ## Best Practices ### 1. Always Check Error Type ```typescript catch (error) { if (error instanceof OAuthError) { // Handle OAuth-specific errors } else if (error.message === "Timeout waiting for callback") { // Handle timeout } else { // Handle unexpected errors } } ``` ### 2. Log Errors Appropriately ```typescript if (error instanceof OAuthError) { if (error.error === "access_denied") { // User action - info level console.info("User cancelled OAuth flow"); } else if ( ["server_error", "temporarily_unavailable"].includes(error.error) ) { // Provider issue - error level console.error("OAuth provider error:", error); } else { // Configuration issue - warning level console.warn("OAuth configuration error:", error); } } ``` ### 3. Provide Clear User Feedback ```typescript function getUserMessage(error: OAuthError): string { // Prefer provider's description if available if (error.error_description) { return error.error_description; } // Fall back to generic messages return userFriendlyMessages[error.error] || "Authorization failed"; } ``` ### 4. Handle Errors at the Right Level ```typescript // Low level - preserve error details async function getOAuthCode(url: string) { const result = await getAuthCode(url); // Let OAuthError propagate up return result.code; } // High level - translate to user actions async function authenticateUser() { try { const code = await getOAuthCode(authUrl); return { success: true, code }; } catch (error) { if (error instanceof OAuthError && error.error === "access_denied") { return { success: false, cancelled: true }; } return { success: false, error: error.message }; } } ``` ## Related APIs * [`getAuthCode`](/api/get-auth-code) - Main function that throws OAuthError * [`browserAuth`](/api/browser-auth) - MCP provider that handles OAuth errors * [`TimeoutError`](#timeouterror) - Related timeout error class ## TimeoutError The library also exports a `TimeoutError` class for timeout-specific failures: ```typescript class TimeoutError extends Error { name: "TimeoutError"; constructor(message?: string); } ``` Usage: ```typescript catch (error) { if (error instanceof TimeoutError) { console.error("OAuth flow timed out"); // Suggest user tries again } } ``` --- --- url: /oauth-callback/api-examples.md --- # Runtime API Examples This page demonstrates usage of some of the runtime APIs provided by VitePress. The main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files: ```md ## Results ### Theme Data


### Page Data

{"url"=>"/llms-full.txt", "content"=>"# ADR-001: No Refresh Tokens in browserAuth\n\n**Status:** Accepted\n**Date:** 2025-01-25\n**Tags:** oauth, mcp, security\n\n## Problem\n\n* Should `browserAuth` handle OAuth refresh tokens and automatic token renewal?\n\n## Decision\n\n* `browserAuth` intentionally does not request or handle refresh tokens.\n* When tokens expire, `tokens()` returns `undefined`, signaling the MCP SDK to re-authenticate.\n* The SDK's built-in retry logic handles re-auth transparently.\n\nRationale:\n\n* **MCP SDK lifecycle**: The SDK expects auth providers to return `undefined` for expired tokens, triggering its standard re-auth flow.\n* **CLI/desktop UX**: Interactive re-consent is acceptable and often preferred over silent background refresh.\n* **Simplicity**: Avoiding refresh eliminates token rotation complexity, race conditions, and concurrent refresh handling.\n* **Security**: No long-lived refresh tokens stored; each session requires explicit user consent.\n\n## Alternatives (brief)\n\n* **Implement refresh flow** — Adds complexity (token rotation, concurrency), conflicts with SDK's re-auth expectations, stores long-lived credentials.\n* **Optional refresh via config** — Increases API surface, creates two divergent code paths to maintain.\n\n## Impact\n\n* Positive: Simpler implementation, predictable behavior, aligns with MCP SDK design.\n* Negative/Risks: More frequent browser prompts for long-running sessions (acceptable for CLI tools).\n\n## Links\n\n* Code: `src/auth/browser-auth.ts`\n* Related: MCP SDK auth interface in `@modelcontextprotocol/sdk/client/auth`\n\n---\n\n---\nurl: /oauth-callback/adr/002-immediate-token-exchange.md\n---\n# ADR-002: Immediate Token Exchange in redirectToAuthorization()\n\n**Status:** Accepted\n**Date:** 2025-01-25\n**Tags:** oauth, mcp, sdk-integration\n\n## Problem\n\nThe MCP SDK's `auth()` flow works as follows:\n\n1. Check `provider.tokens()` — if valid tokens exist, return `'AUTHORIZED'`\n2. If no tokens, start authorization: call `redirectToAuthorization(url)`\n3. **Immediately** return `'REDIRECT'` (without re-checking tokens)\n\nFor web-based OAuth, this makes sense: `redirectToAuthorization()` triggers a page redirect and control never returns synchronously. The SDK expects authentication to complete in a subsequent request.\n\nFor CLI/desktop apps using `browserAuth()`, control **does** return synchronously—we capture the callback in-process via a local HTTP server. We exchange tokens inside `redirectToAuthorization()`, but the SDK has already decided to return `'REDIRECT'`, causing `UnauthorizedError`.\n\n## Decision\n\nExchange tokens **inside** `redirectToAuthorization()` and document the retry pattern as the expected usage:\n\n```typescript\n// First connect triggers OAuth flow and saves tokens, but SDK returns\n// 'REDIRECT' before checking. Second connect finds valid tokens.\nasync function connectWithOAuthRetry(client, serverUrl, authProvider) {\n  const transport = new StreamableHTTPClientTransport(serverUrl, {\n    authProvider,\n  });\n  try {\n    await client.connect(transport);\n  } catch (error) {\n    if (error.message === \"Unauthorized\") {\n      await client.connect(\n        new StreamableHTTPClientTransport(serverUrl, { authProvider }),\n      );\n    } else throw error;\n  }\n}\n```\n\n**Why a new transport on retry?** The transport caches connection state internally. A fresh transport ensures clean reconnection.\n\n## Rationale\n\n* **SDK constraint**: No hook exists between redirect completion and the `'REDIRECT'` return. The SDK interface (`Promise`) cannot signal \"auth completed.\"\n* **In-process capture**: CLI apps don't have page redirects that would trigger a fresh auth check cycle.\n* **Correctness over elegance**: The retry is unusual but reliable—tokens are always saved before the error.\n\n## Alternatives Considered\n\n| Alternative                                    | Why Rejected                                                  |\n| ---------------------------------------------- | ------------------------------------------------------------- |\n| `transport.finishAuth(callbackUrl)`            | Breaks provider encapsulation; doesn't fit in-process capture |\n| Return tokens from `redirectToAuthorization()` | SDK interface expects `Promise`                         |\n| Upstream SDK change                            | Not viable for library consumers                              |\n\n## Impact\n\n* **Positive**: Self-contained auth flow; no external coordination needed\n* **Negative**: First connection always throws `UnauthorizedError` after OAuth—must be documented clearly\n\n## Links\n\n* Code: `src/auth/browser-auth.ts` lines 254-368\n* MCP SDK auth interface: `@modelcontextprotocol/sdk/client/auth.js`\n* Related: ADR-001 (no refresh tokens)\n\n---\n\n---\nurl: /oauth-callback/adr/003-stable-client-metadata.md\n---\n# ADR-003: Stable Client Metadata Across DCR\n\n**Status:** Accepted\n**Date:** 2025-01-25\n**Tags:** oauth, dcr, security\n\n## Problem\n\n* During Dynamic Client Registration (DCR), the authorization server may return different capabilities than requested (e.g., `token_endpoint_auth_method`).\n* If `clientMetadata` adapts to DCR responses, subsequent token requests may fail when the AS caches the original registration metadata.\n\n## Decision\n\n* `clientMetadata` is immutable after construction.\n* `token_endpoint_auth_method` is determined at construction: `client_secret_post` if `clientSecret` is provided, `none` otherwise. DCR responses never change this value.\n* DCR credentials (`client_id`, `client_secret`) are stored separately and never mutate the auth method.\n\n## Alternatives (brief)\n\n* **Dynamic metadata evolution** — Adapting to DCR responses seems flexible but causes cache mismatches with AS that remember original registration.\n* **Per-request method detection** — Adds complexity and non-deterministic behavior across retries.\n\n## Impact\n\n* Positive: Predictable behavior with all AS implementations; eliminates cache-related auth failures.\n* Negative/Risks: None identified; the fixed method (`client_secret_post`) has universal support.\n\n## Links\n\n* Code: `src/auth/browser-auth.ts`\n* Related ADRs: ADR-001 (No Refresh Tokens), ADR-002 (Immediate Token Exchange)\n\n---\n\n---\nurl: /oauth-callback/adr/004-conditional-state-validation.md\n---\n# ADR-004: Conditional OAuth State Validation\n\n**Status:** Accepted\n**Date:** 2025-01-25\n**Tags:** oauth, security, csrf\n\n## Problem\n\n* RFC 6749 recommends `state` for CSRF protection, but RFC 8252 (native apps) relies on loopback redirect for security.\n* Some authorization servers don't echo `state` back; others require it.\n* Strict validation breaks compatibility; no validation weakens security.\n\n## Decision\n\n* Validate `state` only if it was present in the authorization URL.\n* If the auth URL includes `state` and the callback doesn't match, reject as CSRF.\n* If the auth URL omits `state`, accept callbacks without state validation.\n\nRationale:\n\n* **Defense-in-depth**: Loopback binding (127.0.0.1) prevents network CSRF, but state adds protection against local attacks (malicious apps, browser extensions intercepting localhost).\n* **CLI threat model**: Unlike web apps, CLI tools face local machine threats—other processes can probe localhost ports. State validation detects if a callback arrives from an unrelated auth flow.\n* **Compatibility**: Authorization servers have inconsistent state handling. Conditional validation works with all servers while providing protection when available.\n\n## Alternatives (brief)\n\n* **Always require state** — Breaks servers that don't echo state or don't support it.\n* **Never validate state** — Loopback provides baseline security, but ignores state when the AS cooperates.\n* **Generate state internally always** — Conflicts with auth URLs that already include state from the MCP SDK.\n\n## Impact\n\n* Positive: Maximum security when AS supports state; universal compatibility otherwise.\n* Negative/Risks: If an AS echoes arbitrary state values without validation, the protection is weaker (rare edge case).\n\n## Links\n\n* Code: `src/auth/browser-auth.ts:297-300`\n* RFC 6749 Section 10.12 (CSRF Protection)\n* RFC 8252 Section 8.1 (Loopback Redirect)\n\n---\n\n---\nurl: /oauth-callback/adr/005-store-responsibility-reduction.md\n---\n# ADR-005: OAuthStore Responsibility Reduction\n\n**Status:** Accepted\n**Date:** 2025-01-25\n**Tags:** api, storage, simplification\n\n## Problem\n\nThe store was accumulating OAuth flow state (session, nonce, state parameter) alongside persistent data (tokens, client registration). This blurred the line between \"what survives a crash\" and \"what's ephemeral by design,\" making the API harder to reason about and test.\n\n## Decision\n\nThe store is responsible **only** for data that must survive process restarts:\n\n| Stored                | Not Stored        |\n| --------------------- | ----------------- |\n| `tokens`              | `state` parameter |\n| `client` (DCR result) | `nonce`           |\n| `codeVerifier` (PKCE) | session objects   |\n\nThe `codeVerifier` is the sole flow artifact persisted—it enables completing an in-progress authorization if the process crashes after browser launch but before callback.\n\n## Alternatives (brief)\n\n* **Full session persistence** — Would enable crash-recovery at any point, but adds complexity for a rare edge case. Users can simply restart the flow.\n* **No verifier persistence** — Simpler, but loses the most common crash scenario (user switches apps, process dies).\n\n## Impact\n\n* Positive: Cleaner mental model; store implementations are trivial to write and test.\n* Negative: If the process crashes before `codeVerifier` is saved, the flow must restart. This is acceptable—it's a sub-second window.\n\n## Links\n\n* Code: `src/storage/`, `src/mcp-types.ts`\n* Related: ADR-002 (Immediate Token Exchange)\n\n---\n\n---\nurl: /oauth-callback/adr/000-template.md\n---\n# ADR-NNN Title\n\n**Status:** Proposed | Accepted | Deprecated | Superseded\\\n**Date:** YYYY-MM-DD\\\n**Tags:** tag1, tag2\n\n## Problem\n\n* One or two sentences on the decision trigger or constraint.\n\n## Decision\n\n* The chosen approach in a short paragraph.\n\n## Alternatives (brief)\n\n* Option A — why not.\n* Option B — why not.\n\n## Impact\n\n* Positive:\n* Negative/Risks:\n\n## Links\n\n* Code/Docs:\n* Related ADRs:\n\n---\n\n---\nurl: /oauth-callback/api.md\ndescription: >-\n  Complete API documentation for OAuth Callback library functions, types, and\n  interfaces.\n---\n\n# API Reference\n\nOAuth Callback provides a comprehensive set of APIs for handling OAuth 2.0 authorization flows in CLI tools, desktop applications, and Model Context Protocol (MCP) clients. The library is designed with modern Web Standards APIs and works across Node.js 18+, Deno, and Bun.\n\n## Quick Navigation\n\n### Core Functions\n\n* [**getAuthCode**](/api/get-auth-code) - Capture OAuth authorization codes via localhost callback\n* [**browserAuth**](/api/browser-auth) - MCP SDK-compatible OAuth provider with DCR support\n\n### Storage Providers\n\n* [**Storage Providers**](/api/storage-providers) - Token persistence interfaces and implementations\n* **inMemoryStore** - Ephemeral in-memory token storage\n* **fileStore** - Persistent file-based token storage\n\n### Errors\n\n* [**OAuthError**](/api/oauth-error) - OAuth-specific error class with RFC 6749 compliance\n\n### Type Definitions\n\n* [**Types**](/api/types) - Complete TypeScript type reference\n\n## Import Methods\n\nOAuth Callback supports multiple import patterns to suit different use cases:\n\n### Main Package Import\n\n```typescript\nimport open from \"open\";\n\n// Core functionality\nimport { getAuthCode, OAuthError } from \"oauth-callback\";\n\n// Namespace import for MCP features\nimport { mcp } from \"oauth-callback\";\nconst authProvider = mcp.browserAuth({ launch: open, store: mcp.fileStore() });\n```\n\n### MCP-Specific Import\n\n```typescript\n// Direct MCP imports (recommended for MCP projects)\nimport { browserAuth, fileStore, inMemoryStore } from \"oauth-callback/mcp\";\nimport type { TokenStore, OAuthStore, Tokens } from \"oauth-callback/mcp\";\n```\n\n## Core APIs\n\n### getAuthCode(input)\n\nThe primary function for capturing OAuth authorization codes through a localhost callback server.\n\n```typescript\nfunction getAuthCode(\n  input: string | GetAuthCodeOptions,\n): Promise;\n```\n\n**Key Features:**\n\n* Automatic browser opening\n* Configurable port and timeout\n* Custom HTML templates\n* AbortSignal support\n* Comprehensive error handling\n\n[**Full Documentation →**](/api/get-auth-code)\n\n### browserAuth(options)\n\nMCP SDK-compatible OAuth provider that handles the complete OAuth flow including Dynamic Client Registration.\n\n```typescript\nfunction browserAuth(options?: BrowserAuthOptions): OAuthClientProvider;\n```\n\n**Key Features:**\n\n* Dynamic Client Registration (RFC 7591)\n* Automatic token expiry handling\n* PKCE support (RFC 7636)\n* Flexible token storage\n* MCP SDK integration\n\n[**Full Documentation →**](/api/browser-auth)\n\n## Storage APIs\n\n### Storage Interfaces\n\nOAuth Callback provides two storage interfaces for different levels of state management:\n\n```typescript\n// Basic token storage\ninterface TokenStore {\n  get(key: string): Promise;\n  set(key: string, tokens: Tokens): Promise;\n  delete(key: string): Promise;\n}\n\n// Extended storage with DCR and PKCE support\ninterface OAuthStore extends TokenStore {\n  getClient(key: string): Promise;\n  setClient(key: string, client: ClientInfo): Promise;\n  deleteClient(key: string): Promise;\n  getCodeVerifier(key: string): Promise;\n  setCodeVerifier(key: string, verifier: string): Promise;\n  deleteCodeVerifier(key: string): Promise;\n}\n```\n\n[**Full Documentation →**](/api/storage-providers)\n\n### Built-in Implementations\n\n#### inMemoryStore()\n\nEphemeral storage that keeps tokens in memory:\n\n```typescript\nconst authProvider = browserAuth({\n  launch: open,\n  store: inMemoryStore(), // Tokens lost on restart\n});\n```\n\n#### fileStore(filepath?)\n\nPersistent storage that saves tokens to a JSON file:\n\n```typescript\nconst authProvider = browserAuth({\n  launch: open,\n  store: fileStore(), // Default: ~/.mcp/tokens.json\n});\n```\n\n## Error Handling\n\n### OAuthError\n\nSpecialized error class for OAuth-specific failures:\n\n```typescript\nclass OAuthError extends Error {\n  error: string; // OAuth error code\n  error_description?: string; // Human-readable description\n  error_uri?: string; // URI with more information\n}\n```\n\n**Common Error Codes:**\n\n* `access_denied` - User denied authorization\n* `invalid_scope` - Requested scope is invalid\n* `server_error` - Authorization server error\n* `temporarily_unavailable` - Service temporarily down\n\n[**Full Documentation →**](/api/oauth-error)\n\n## Type System\n\nOAuth Callback is fully typed with TypeScript, providing comprehensive type definitions for all APIs:\n\n### Core Types\n\n```typescript\ninterface GetAuthCodeOptions {\n  authorizationUrl: string;\n  port?: number;\n  hostname?: string;\n  callbackPath?: string;\n  timeout?: number;\n  launch?: (url: string) => unknown;\n  successHtml?: string;\n  errorHtml?: string;\n  signal?: AbortSignal;\n  onRequest?: (req: Request) => void;\n}\n\ninterface CallbackResult {\n  code: string;\n  state?: string;\n  [key: string]: any;\n}\n```\n\n### Storage Types\n\n```typescript\ninterface Tokens {\n  accessToken: string;\n  refreshToken?: string;\n  expiresAt?: number;\n  scope?: string;\n}\n\ninterface ClientInfo {\n  clientId: string;\n  clientSecret?: string;\n  clientIdIssuedAt?: number;\n  clientSecretExpiresAt?: number;\n}\n```\n\n[**Full Type Reference →**](/api/types)\n\n## Usage Patterns\n\n### Simple OAuth Flow\n\n```typescript\nimport { getAuthCode } from \"oauth-callback\";\n\nconst result = await getAuthCode(\n  \"https://github.com/login/oauth/authorize?client_id=xxx\",\n);\nconsole.log(\"Code:\", result.code);\n```\n\n### MCP Integration\n\n```typescript\nimport open from \"open\";\nimport { browserAuth, fileStore } from \"oauth-callback/mcp\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\n\nconst authProvider = browserAuth({\n  launch: open,\n  store: fileStore(),\n  scope: \"read write\",\n});\n\nconst transport = new StreamableHTTPClientTransport(\n  new URL(\"https://mcp.example.com\"),\n  { authProvider },\n);\n```\n\n### Handling Errors\n\n```typescript\nimport { getAuthCode, OAuthError } from \"oauth-callback\";\n\ntry {\n  const result = await getAuthCode(authUrl);\n} catch (error) {\n  if (error instanceof OAuthError) {\n    console.error(`OAuth error: ${error.error}`);\n    console.error(`Details: ${error.error_description}`);\n  }\n}\n```\n\n### Custom Storage\n\n```typescript\nimport { TokenStore, Tokens } from \"oauth-callback/mcp\";\n\nclass RedisStore implements TokenStore {\n  async get(key: string): Promise {\n    // Implementation\n  }\n  async set(key: string, tokens: Tokens): Promise {\n    // Implementation\n  }\n  // ... other methods\n}\n\nconst authProvider = browserAuth({\n  launch: open,\n  store: new RedisStore(),\n});\n```\n\n## Security Considerations\n\n### Built-in Security Features\n\n* **PKCE by default** - Proof Key for Code Exchange enabled\n* **State validation** - Automatic CSRF protection\n* **Localhost-only binding** - Server only accepts local connections\n* **Automatic cleanup** - Server shuts down after callback\n* **Secure file permissions** - Mode 0600 for file storage\n\n### Best Practices\n\n```typescript\n// Always validate state for CSRF protection\nconst state = crypto.randomUUID();\nconst authUrl = `https://example.com/authorize?state=${state}&...`;\nconst result = await getAuthCode(authUrl);\nif (result.state !== state) {\n  throw new Error(\"State mismatch - possible CSRF attack\");\n}\n\n// Use ephemeral storage for maximum security\nconst authProvider = browserAuth({\n  launch: open,\n  store: inMemoryStore(), // No disk persistence\n});\n\n// Implement PKCE for public clients\nconst verifier = randomBytes(32).toString(\"base64url\");\nconst challenge = createHash(\"sha256\").update(verifier).digest(\"base64url\");\n```\n\n## Platform Support\n\n### Runtime Compatibility\n\n| Runtime | Minimum Version | Status             |\n| ------- | --------------- | ------------------ |\n| Node.js | 18.0.0          | ✅ Fully supported |\n| Deno    | 1.0.0           | ✅ Fully supported |\n| Bun     | 1.0.0           | ✅ Fully supported |\n\n### OAuth Provider Compatibility\n\nOAuth Callback works with any OAuth 2.0 provider that supports the authorization code flow:\n\n* ✅ GitHub\n* ✅ Google\n* ✅ Microsoft\n* ✅ Notion (with DCR)\n* ✅ Linear\n* ✅ Any RFC 6749 compliant provider\n\n### Browser Compatibility\n\nThe library opens the user's default browser for authorization. All modern browsers are supported:\n\n* ✅ Chrome/Chromium\n* ✅ Firefox\n* ✅ Safari\n* ✅ Edge\n* ✅ Any browser that handles `http://localhost` URLs\n\n## Advanced Features\n\n### Dynamic Client Registration\n\nAutomatically register OAuth clients without pre-configuration:\n\n```typescript\n// No client_id or client_secret needed!\nconst authProvider = browserAuth({\n  launch: open,\n  scope: \"read write\",\n  store: fileStore(),\n});\n```\n\n### Multi-Environment Support\n\n```typescript\nfunction createAuthProvider(env: \"dev\" | \"staging\" | \"prod\") {\n  const configs = {\n    dev: { launch: open, port: 3000, store: inMemoryStore() },\n    staging: {\n      launch: open,\n      port: 3001,\n      store: fileStore(\"~/.mcp/staging.json\"),\n    },\n    prod: { launch: open, port: 3002, store: fileStore(\"~/.mcp/prod.json\") },\n  };\n  return browserAuth(configs[env]);\n}\n```\n\n### Request Logging\n\n```typescript\nconst authProvider = browserAuth({\n  launch: open,\n  onRequest: (req) => {\n    const url = new URL(req.url);\n    console.log(`[OAuth] ${req.method} ${url.pathname}`);\n  },\n});\n```\n\n## Migration Guides\n\n### From Manual OAuth Implementation\n\n```typescript\n// Before: Manual OAuth flow\nconst server = http.createServer();\nserver.listen(3000);\n// ... complex callback handling ...\n\n// After: Using OAuth Callback\nconst result = await getAuthCode(authUrl);\n```\n\n### To MCP Integration\n\n```typescript\n// Before: Custom OAuth provider\nclass CustomOAuthProvider {\n  /* ... */\n}\n\n// After: Using browserAuth\nconst authProvider = browserAuth({ launch: open, store: fileStore() });\n```\n\n## API Stability\n\n| API              | Status | Since  | Notes                         |\n| ---------------- | ------ | ------ | ----------------------------- |\n| `getAuthCode`    | Stable | v1.0.0 | Core API, backward compatible |\n| `getRedirectUrl` | Stable | v1.0.0 | Redirect URI helper           |\n| `OAuthError`     | Stable | v1.0.0 | OAuth-specific errors         |\n| `TimeoutError`   | Stable | v1.0.0 | Timeout error class           |\n| `mcp`            | Stable | v2.0.0 | MCP namespace export          |\n| `browserAuth`    | Stable | v2.0.0 | MCP integration               |\n| `inMemoryStore`  | Stable | v2.0.0 | Storage provider              |\n| `fileStore`      | Stable | v2.0.0 | Storage provider              |\n| Types            | Stable | v1.0.0 | TypeScript definitions        |\n\n## Related Resources\n\n* [Core Concepts](/core-concepts) - Architecture and design patterns\n* [Getting Started](/getting-started) - Quick start guide\n* [GitHub Repository](https://github.com/kriasoft/oauth-callback) - Source code and issues\n\n---\n\n---\nurl: /oauth-callback/adr.md\n---\n# Architecture Decision Records\n\nKey design decisions with context and rationale.\n\n| ADR                                            | Decision                                              |\n| ---------------------------------------------- | ----------------------------------------------------- |\n| [001](./001-no-refresh-tokens.md)              | No refresh tokens—rely on MCP SDK's re-auth flow      |\n| [002](./002-immediate-token-exchange.md)       | Token exchange inside `redirectToAuthorization()`     |\n| [003](./003-stable-client-metadata.md)         | Immutable client metadata across DCR                  |\n| [004](./004-conditional-state-validation.md)   | Validate `state` only when present in auth URL        |\n| [005](./005-store-responsibility-reduction.md) | Store persists only tokens, client, and PKCE verifier |\n\n---\n\n---\nurl: /oauth-callback/api/browser-auth.md\ndescription: >-\n  MCP SDK-compatible OAuth provider for browser-based authentication flows with\n  Dynamic Client Registration support.\n---\n\n# browserAuth\n\nThe `browserAuth` function creates an OAuth provider that integrates seamlessly with the Model Context Protocol (MCP) SDK. It handles the entire OAuth flow including Dynamic Client Registration and token storage through a browser-based authorization flow. Expired tokens trigger re-authentication; refresh tokens are not used.\n\n## Function Signature\n\n```typescript\nfunction browserAuth(options?: BrowserAuthOptions): OAuthClientProvider;\n```\n\n## Parameters\n\n### BrowserAuthOptions\n\n| Property        | Type                       | Default           | Description                                                                                                                                   |\n| --------------- | -------------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |\n| `clientId`      | `string`                   | *none*            | Pre-registered OAuth client ID                                                                                                                |\n| `clientSecret`  | `string`                   | *none*            | Pre-registered OAuth client secret                                                                                                            |\n| `scope`         | `string`                   | *none*            | OAuth scopes to request. When omitted, the auth server uses its default scope.                                                                |\n| `port`          | `number`                   | `3000`            | Port for local callback server                                                                                                                |\n| `hostname`      | `string`                   | `\"localhost\"`     | Hostname to bind server to                                                                                                                    |\n| `callbackPath`  | `string`                   | `\"/callback\"`     | URL path for OAuth callback                                                                                                                   |\n| `store`         | `TokenStore`               | `inMemoryStore()` | Token storage implementation                                                                                                                  |\n| `storeKey`      | `string`                   | `\"mcp-tokens\"`    | Storage key for token isolation                                                                                                               |\n| `launch`        | `(url: string) => unknown` | *none*            | Callback to launch auth URL                                                                                                                   |\n| `authTimeout`   | `number`                   | `300000`          | Auth timeout in ms (5 min)                                                                                                                    |\n| `successHtml`   | `string`                   | *built-in*        | Custom success page HTML                                                                                                                      |\n| `errorHtml`     | `string`                   | *built-in*        | Custom error page HTML                                                                                                                        |\n| `onRequest`     | `(req: Request) => void`   | *none*            | Request logging callback                                                                                                                      |\n| `authServerUrl` | `string \\| URL`            | *auto*            | Base URL for OAuth metadata discovery. Defaults to the authorization URL's origin. Set this when the token endpoint is on a different origin. |\n\n## Return Value\n\nReturns an `OAuthClientProvider` instance that implements the MCP SDK authentication interface:\n\n```typescript\ninterface OAuthClientProvider {\n  // Completes full OAuth flow: browser → callback → token exchange → persist\n  redirectToAuthorization(authorizationUrl: URL): Promise;\n\n  // Token storage\n  tokens(): Promise;\n  saveTokens(tokens: OAuthTokens): Promise;\n\n  // Dynamic Client Registration support\n  clientInformation(): Promise;\n  saveClientInformation(info: OAuthClientInformationFull): Promise;\n\n  // PKCE support\n  codeVerifier(): Promise;\n  saveCodeVerifier(verifier: string): Promise;\n\n  // State management\n  state(): Promise;\n  invalidateCredentials(\n    scope: \"all\" | \"client\" | \"tokens\" | \"verifier\",\n  ): Promise;\n}\n```\n\n## Basic Usage\n\n### Simple MCP Client\n\nThe simplest usage with default settings:\n\n```typescript\nimport open from \"open\";\nimport { browserAuth } from \"oauth-callback/mcp\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { StreamableHTTPClientTransport } from \"@modelcontextprotocol/sdk/client/streamableHttp.js\";\n\n// Create OAuth provider - pass open to launch browser\nconst authProvider = browserAuth({ launch: open });\n\n// Use with MCP transport\nconst transport = new StreamableHTTPClientTransport(\n  new URL(\"https://mcp.example.com\"),\n  { authProvider },\n);\n\nconst client = new Client(\n  { name: \"my-app\", version: \"1.0.0\" },\n  { capabilities: {} },\n);\n\nawait client.connect(transport);\n```\n\n### With Token Persistence\n\nStore tokens across sessions:\n\n```typescript\nimport open from \"open\";\nimport { browserAuth, fileStore } from \"oauth-callback/mcp\";\n\nconst authProvider = browserAuth({\n  launch: open,\n  store: fileStore(), // Persists to ~/.mcp/tokens.json\n  scope: \"read write\",\n});\n```\n\n## Advanced Usage\n\n### Pre-Registered OAuth Clients\n\nIf you have pre-registered OAuth credentials:\n\n```typescript\nimport open from \"open\";\n\nconst authProvider = browserAuth({\n  clientId: process.env.OAUTH_CLIENT_ID,\n  clientSecret: process.env.OAUTH_CLIENT_SECRET,\n  scope: \"read write admin\",\n  launch: open,\n  store: fileStore(),\n});\n```\n\n### Custom Storage Location\n\nStore tokens in a specific location:\n\n```typescript\nimport open from \"open\";\nimport { browserAuth, fileStore } from \"oauth-callback/mcp\";\n\nconst authProvider = browserAuth({\n  launch: open,\n  store: fileStore(\"/path/to/my-tokens.json\"),\n  storeKey: \"my-app-production\", // Namespace for multiple environments\n});\n```\n\n### Custom Port and Callback Path\n\nConfigure the callback server:\n\n```typescript\nimport open from \"open\";\n\nconst authProvider = browserAuth({\n  port: 8080,\n  hostname: \"127.0.0.1\",\n  callbackPath: \"/oauth/callback\",\n  launch: open,\n  store: fileStore(),\n});\n```\n\n::: warning Redirect URI Configuration\nEnsure your OAuth app's redirect URI matches your configuration:\n\n* Configuration: `port: 8080`, `callbackPath: \"/oauth/callback\"`\n* Redirect URI: `http://localhost:8080/oauth/callback`\n  :::\n\n### Custom HTML Pages\n\nProvide branded callback pages:\n\n```typescript\nimport open from \"open\";\n\nconst authProvider = browserAuth({\n  launch: open,\n  successHtml: `\n    \n    \n      \n        Success!\n        \n      \n      \n        
\n

🎉 Success!

\n

Authorization complete. You can close this window.

\n
\n \n \n `,\n errorHtml: `\n \n \n \n

Authorization Failed

\n

Error: {{error}}

\n

{{error_description}}

\n \n \n `,\n});\n```\n\n### Request Logging\n\nMonitor OAuth flow for debugging:\n\n```typescript\nimport open from \"open\";\n\nconst authProvider = browserAuth({\n launch: open,\n onRequest: (req) => {\n const url = new URL(req.url);\n console.log(`[OAuth] ${req.method} ${url.pathname}`);\n\n if (url.pathname === \"/callback\") {\n console.log(\"[OAuth] Callback params:\", url.searchParams.toString());\n }\n },\n store: fileStore(),\n});\n```\n\n### Headless/CI Environment\n\nDisable browser opening for automated environments:\n\n```typescript\nconst authProvider = browserAuth({\n launch: () => {}, // Noop - no browser opening\n authTimeout: 10000, // Shorter timeout for CI\n store: inMemoryStore(),\n});\n```\n\n## Dynamic Client Registration\n\nOAuth Callback supports [RFC 7591](https://www.rfc-editor.org/rfc/rfc7591.html) Dynamic Client Registration, allowing automatic OAuth client registration:\n\n### How It Works\n\n```mermaid\nsequenceDiagram\n participant App\n participant browserAuth\n participant MCP Server\n participant Auth Server\n\n App->>browserAuth: Create provider (no client_id)\n App->>MCP Server: Connect via transport\n MCP Server->>App: Requires authentication\n App->>browserAuth: Initiate OAuth\n browserAuth->>Auth Server: POST /register (DCR)\n Auth Server->>browserAuth: Return client_id, client_secret\n browserAuth->>browserAuth: Store credentials\n browserAuth->>Auth Server: Start OAuth flow\n Auth Server->>browserAuth: Return authorization code\n browserAuth->>App: Authentication complete\n```\n\n### DCR Example\n\nNo pre-registration needed:\n\n```typescript\nimport open from \"open\";\n\n// No clientId or clientSecret required!\nconst authProvider = browserAuth({\n scope: \"read write\",\n launch: open,\n store: fileStore(), // Persist dynamically registered client\n});\n\n// The provider will automatically:\n// 1. Register a new OAuth client on first use\n// 2. Store the client credentials\n// 3. Reuse them for future sessions\n```\n\n### Benefits of DCR\n\n* **Zero Configuration**: Users don't need to manually register OAuth apps\n* **Automatic Setup**: Client registration happens transparently\n* **Credential Persistence**: Registered clients are reused across sessions\n* **Simplified Distribution**: Ship MCP clients without OAuth setup instructions\n\n## Token Storage\n\n### Storage Interfaces\n\nOAuth Callback provides two storage interfaces:\n\n#### TokenStore (Basic)\n\n```typescript\ninterface TokenStore {\n get(key: string): Promise;\n set(key: string, tokens: Tokens): Promise;\n delete(key: string): Promise;\n}\n```\n\n#### OAuthStore (Extended)\n\n```typescript\ninterface OAuthStore extends TokenStore {\n getClient(key: string): Promise;\n setClient(key: string, client: ClientInfo): Promise;\n deleteClient(key: string): Promise;\n getCodeVerifier(key: string): Promise;\n setCodeVerifier(key: string, verifier: string): Promise;\n deleteCodeVerifier(key: string): Promise;\n}\n```\n\n### Built-in Implementations\n\n#### In-Memory Store\n\nEphemeral storage (tokens lost on restart):\n\n```typescript\nimport open from \"open\";\nimport { browserAuth, inMemoryStore } from \"oauth-callback/mcp\";\n\nconst authProvider = browserAuth({\n launch: open,\n store: inMemoryStore(),\n});\n```\n\n**Use cases:**\n\n* Development and testing\n* Short-lived CLI sessions\n* Maximum security (no persistence)\n\n#### File Store\n\nPersistent storage to JSON file:\n\n```typescript\nimport open from \"open\";\nimport { browserAuth, fileStore } from \"oauth-callback/mcp\";\n\n// Default location: ~/.mcp/tokens.json\nconst authProvider = browserAuth({\n launch: open,\n store: fileStore(),\n});\n\n// Custom location\nconst customAuth = browserAuth({\n launch: open,\n store: fileStore(\"/path/to/tokens.json\"),\n});\n```\n\n**Use cases:**\n\n* Desktop applications\n* Long-running services\n* Multi-session authentication\n\n### Custom Storage Implementation\n\nImplement your own storage backend:\n\n```typescript\nimport { TokenStore, Tokens } from \"oauth-callback/mcp\";\n\nclass RedisStore implements TokenStore {\n constructor(private redis: RedisClient) {}\n\n async get(key: string): Promise {\n const data = await this.redis.get(key);\n return data ? JSON.parse(data) : null;\n }\n\n async set(key: string, tokens: Tokens): Promise {\n await this.redis.set(key, JSON.stringify(tokens));\n }\n\n async delete(key: string): Promise {\n await this.redis.del(key);\n }\n}\n\n// Use custom store\nconst authProvider = browserAuth({\n launch: open,\n store: new RedisStore(redisClient),\n});\n```\n\n## Security Features\n\n### PKCE (Proof Key for Code Exchange)\n\nPKCE is always enabled for enhanced security. The MCP SDK handles PKCE automatically through the provider's `saveCodeVerifier()` and `codeVerifier()` methods.\n\nPKCE prevents authorization code interception attacks by:\n\n1. Generating a cryptographic code verifier\n2. Sending a hashed challenge with the authorization request\n3. Proving possession of the verifier during token exchange\n\n### State Parameter\n\nThe `state()` method generates secure random values when called by the MCP SDK. State validation in `browserAuth` compares the callback's state against the state parameter in the authorization URL that was passed to `redirectToAuthorization()`. This means validation works regardless of whether `state()` was used - it validates whatever state is present in the URL.\n\nFor localhost flows, [RFC 8252](https://www.rfc-editor.org/rfc/rfc8252) considers loopback interface binding sufficient for security. State validation adds defense-in-depth.\n\n### Token Expiry Management\n\nTokens are tracked with expiry times. The provider returns `undefined` from `tokens()` 60 seconds before actual expiry to prevent mid-request failures. This triggers re-authentication before tokens become invalid:\n\n```typescript\n// The provider:\n// 1. Returns undefined 60s before token expiry\n// 2. SDK triggers re-auth when tokens() returns undefined\n// 3. Requests never fail due to mid-flight token expiry\n```\n\n### Secure Storage\n\nFile storage uses restrictive permissions:\n\n```typescript\nimport open from \"open\";\n\n// Files are created with mode 0600 (owner read/write only)\nconst authProvider = browserAuth({\n launch: open,\n store: fileStore(), // Secure file permissions\n});\n```\n\n## Error Handling\n\n### OAuth Errors\n\nThe provider handles OAuth-specific errors:\n\n```typescript\ntry {\n await client.connect(transport);\n} catch (error) {\n if (error.message.includes(\"access_denied\")) {\n console.log(\"User cancelled authorization\");\n } else if (error.message.includes(\"invalid_scope\")) {\n console.log(\"Requested scope not available\");\n } else {\n console.error(\"Connection failed:\", error);\n }\n}\n```\n\n### Timeout Handling\n\nConfigure timeout for different scenarios:\n\n```typescript\nimport open from \"open\";\n\nconst authProvider = browserAuth({\n launch: open,\n authTimeout: 600000, // 10 minutes for first-time setup\n});\n```\n\n## Complete Examples\n\n### Notion MCP Integration\n\nFull example with Dynamic Client Registration:\n\n```typescript\nimport open from \"open\";\nimport { browserAuth, fileStore } from \"oauth-callback/mcp\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { StreamableHTTPClientTransport } from \"@modelcontextprotocol/sdk/client/streamableHttp.js\";\n\nasync function connectToNotion() {\n // No client credentials needed - uses DCR!\n const authProvider = browserAuth({\n launch: open, // Opens browser for OAuth consent\n store: fileStore(), // Persist tokens and client registration\n scope: \"read write\",\n onRequest: (req) => {\n console.log(`[Notion OAuth] ${new URL(req.url).pathname}`);\n },\n });\n\n const transport = new StreamableHTTPClientTransport(\n new URL(\"https://mcp.notion.com/mcp\"),\n { authProvider },\n );\n\n const client = new Client(\n { name: \"notion-client\", version: \"1.0.0\" },\n { capabilities: {} },\n );\n\n try {\n await client.connect(transport);\n console.log(\"Connected to Notion MCP!\");\n\n // List available tools\n const tools = await client.listTools();\n console.log(\"Available tools:\", tools);\n\n // Use a tool\n const result = await client.callTool(\"search\", {\n query: \"meeting notes\",\n });\n console.log(\"Search results:\", result);\n } catch (error) {\n console.error(\"Failed to connect:\", error);\n } finally {\n await client.close();\n }\n}\n\nconnectToNotion();\n```\n\n### Multi-Environment Configuration\n\nSupport development, staging, and production:\n\n```typescript\nimport open from \"open\";\nimport { browserAuth, fileStore, inMemoryStore } from \"oauth-callback/mcp\";\n\nfunction createAuthProvider(environment: \"dev\" | \"staging\" | \"prod\") {\n const configs = {\n dev: {\n port: 3000,\n launch: open,\n store: inMemoryStore(), // No persistence in dev\n authTimeout: 60000,\n onRequest: (req: Request) => console.log(\"[DEV]\", req.url),\n },\n staging: {\n port: 3001,\n launch: open,\n store: fileStore(\"~/.mcp/staging-tokens.json\"),\n storeKey: \"staging\",\n authTimeout: 120000,\n },\n prod: {\n port: 3002,\n launch: open,\n store: fileStore(\"~/.mcp/prod-tokens.json\"),\n storeKey: \"production\",\n authTimeout: 300000,\n clientId: process.env.PROD_CLIENT_ID,\n clientSecret: process.env.PROD_CLIENT_SECRET,\n },\n };\n\n return browserAuth(configs[environment]);\n}\n\n// Use appropriate environment\nconst authProvider = createAuthProvider(\n process.env.NODE_ENV as \"dev\" | \"staging\" | \"prod\",\n);\n```\n\n### Handling Expired Tokens\n\nThe provider does not use refresh tokens. When tokens expire, re-authentication is triggered automatically by returning `undefined` from `tokens()`. The MCP SDK handles this transparently.\n\nFor explicit control over re-authentication:\n\n```typescript\n// Force re-authentication by clearing tokens\nawait authProvider.invalidateCredentials(\"tokens\");\n```\n\n## Testing\n\n### Unit Testing\n\nMock the OAuth provider for tests:\n\n```typescript\nimport { vi, describe, it, expect } from \"vitest\";\n\n// Create mock provider\nconst mockAuthProvider = {\n redirectToAuthorization: vi.fn(),\n tokens: vi.fn().mockResolvedValue({\n access_token: \"test_token\",\n token_type: \"Bearer\",\n }),\n saveTokens: vi.fn(),\n clientInformation: vi.fn(),\n saveClientInformation: vi.fn(),\n state: vi.fn().mockResolvedValue(\"test_state\"),\n codeVerifier: vi.fn().mockResolvedValue(\"test_verifier\"),\n saveCodeVerifier: vi.fn(),\n invalidateCredentials: vi.fn(),\n};\n\ndescribe(\"MCP Client\", () => {\n it(\"should authenticate with OAuth\", async () => {\n const transport = new StreamableHTTPClientTransport(\n new URL(\"https://test.example.com\"),\n { authProvider: mockAuthProvider },\n );\n\n const client = new Client(\n { name: \"test\", version: \"1.0.0\" },\n { capabilities: {} },\n );\n\n await client.connect(transport);\n\n expect(mockAuthProvider.tokens).toHaveBeenCalled();\n });\n});\n```\n\n### Integration Testing\n\nTest with a mock OAuth server:\n\n```typescript\nimport { browserAuth, inMemoryStore } from \"oauth-callback/mcp\";\nimport { createMockOAuthServer } from \"./test-utils\";\n\ndescribe(\"OAuth Flow Integration\", () => {\n let mockServer: MockOAuthServer;\n\n beforeAll(async () => {\n mockServer = createMockOAuthServer();\n await mockServer.start();\n });\n\n afterAll(async () => {\n await mockServer.stop();\n });\n\n it(\"should complete full OAuth flow\", async () => {\n const authProvider = browserAuth({\n port: 3001,\n launch: () => {}, // Noop - don't open browser in tests\n store: inMemoryStore(),\n });\n\n // Simulate OAuth flow\n await authProvider.redirectToAuthorization(\n new URL(`http://localhost:${mockServer.port}/authorize`),\n );\n\n const tokens = await authProvider.tokens();\n expect(tokens?.access_token).toBeDefined();\n });\n});\n```\n\n## Troubleshooting\n\n### Common Issues\n\n::: details Port Already in Use\n\n```typescript\nimport open from \"open\";\n\n// Use a different port\nconst authProvider = browserAuth({\n launch: open,\n port: 8080, // Try alternative port\n});\n```\n\n:::\n\n::: details Tokens Not Persisting\n\n```typescript\nimport open from \"open\";\n\n// Ensure you're using file store, not in-memory\nconst authProvider = browserAuth({\n launch: open,\n store: fileStore(), // ✅ Persistent\n // store: inMemoryStore() // ❌ Lost on restart\n});\n```\n\n:::\n\n::: details DCR Not Working\nSome servers may not support Dynamic Client Registration:\n\n```typescript\nimport open from \"open\";\n\n// Fallback to pre-registered credentials\nconst authProvider = browserAuth({\n launch: open,\n clientId: \"your-client-id\",\n clientSecret: \"your-client-secret\",\n});\n```\n\n:::\n\n::: details Browser Not Opening\n\n```typescript\nimport open from \"open\";\n\n// Conditionally open browser based on environment\nconst authProvider = browserAuth({\n launch: process.env.CI ? () => {} : open,\n});\n```\n\n:::\n\n## API Compatibility\n\nThe `browserAuth` provider implements the MCP SDK's `OAuthClientProvider` interface:\n\n| Method | Status | Notes |\n| ------------------------- | -------------------- | ------------------------------------------------------------------------- |\n| `redirectToAuthorization` | ✅ Fully supported | Completes full OAuth flow (browser → callback → token exchange → persist) |\n| `tokens` | ✅ Fully supported | Returns current tokens |\n| `saveTokens` | ✅ Fully supported | Persists to storage |\n| `clientInformation` | ✅ Fully supported | Returns client credentials |\n| `saveClientInformation` | ✅ Fully supported | Stores DCR results |\n| `state` | ✅ Fully supported | Generates secure state |\n| `codeVerifier` | ✅ Fully supported | PKCE verifier |\n| `saveCodeVerifier` | ✅ Fully supported | Stores PKCE verifier |\n| `invalidateCredentials` | ✅ Fully supported | Clears stored data |\n| `validateResourceURL` | ✅ Returns undefined | Not applicable |\n\n## Migration Guide\n\n### From Manual OAuth to browserAuth\n\n```typescript\n// Before: Manual OAuth implementation\nconst code = await getAuthCode(authUrl);\nconst tokens = await exchangeCodeForTokens(code);\n// Manual token storage and refresh...\n\n// After: Using browserAuth\nconst authProvider = browserAuth({\n launch: open,\n store: fileStore(),\n});\n// Automatic handling of entire OAuth flow!\n```\n\n### From In-Memory to Persistent Storage\n\n```typescript\n// Before: Tokens lost on restart\nconst authProvider = browserAuth({ launch: open });\n\n// After: Tokens persist across sessions\nconst authProvider = browserAuth({\n launch: open,\n store: fileStore(),\n});\n```\n\n## Related APIs\n\n* [`getAuthCode`](/api/get-auth-code) - Low-level OAuth code capture\n* [`TokenStore`](/api/storage-providers) - Storage interface documentation\n* [`OAuthError`](/api/oauth-error) - OAuth error handling\n\n---\n\n---\nurl: /oauth-callback/core-concepts.md\ndescription: >-\n Master the fundamental concepts and architecture of OAuth Callback, from the\n authorization flow to token management and MCP integration patterns.\n---\n\n# Core Concepts {#top}\n\nUnderstanding the core concepts behind **OAuth Callback** will help you build robust OAuth integrations in your CLI tools, desktop applications, and MCP clients. This page covers the fundamental patterns, architectural decisions, and key abstractions that power the library.\n\n## The Authorization Code Flow\n\nOAuth Callback implements the OAuth 2.0 Authorization Code Flow, the most secure flow for applications that can protect client secrets. This flow involves three key participants:\n\n```mermaid\nflowchart LR\n A[Your App] -->|Step 1: Request authorization| B[Auth Server]\n B -->|Step 2: User authenticates| C[User]\n C -->|Step 3: Grants permission| B\n B -->|Step 4: Returns code| A\n A -->|Step 5: Exchange code| B\n B -->|Step 6: Returns tokens| A\n```\n\n### Why Authorization Code Flow?\n\nThe authorization code flow provides several security benefits:\n\n* **No token exposure**: Access tokens never pass through the browser\n* **Short-lived codes**: Authorization codes expire quickly (typically 10 minutes)\n* **Server verification**: The auth server can verify the client's identity\n* **PKCE support**: Protection against authorization code interception\n\n## The Localhost Callback Pattern\n\nThe core innovation of OAuth Callback is making the localhost callback pattern trivially simple to implement. This pattern, standardized in [RFC 8252](https://www.rfc-editor.org/rfc/rfc8252.html), solves a fundamental problem: how can native applications receive OAuth callbacks without a public web server?\n\n### The Problem\n\nTraditional web applications have public URLs where OAuth providers can send callbacks:\n\n```text\nhttps://myapp.com/oauth/callback?code=xyz123\n```\n\nBut CLI tools and desktop apps don't have public URLs. They run on the user's machine behind firewalls and NAT.\n\n### The Solution\n\nOAuth Callback creates a temporary HTTP server on localhost that:\n\n1. **Binds locally**: Only accepts connections from `127.0.0.1`\n2. **Uses dynamic ports**: Works with any available port\n3. **Auto-terminates**: Shuts down after receiving the callback\n4. **Handles edge cases**: Timeouts, errors, user cancellation\n\n```typescript\nimport open from \"open\";\n\n// This single function handles all the complexity\nconst result = await getAuthCode({ authorizationUrl, launch: open });\n```\n\n## Architecture Overview\n\nOAuth Callback is built on a layered architecture that separates concerns and enables flexibility:\n\n```mermaid\nflowchart TD\n subgraph \"Application Layer\"\n A[Your CLI/Desktop App]\n end\n\n subgraph \"OAuth Callback Library\"\n B[getAuthCode Function]\n C[HTTP Server Module]\n D[Browser Launcher]\n E[Error Handler]\n F[Template Engine]\n end\n\n subgraph \"MCP Integration Layer\"\n G[browserAuth Provider]\n H[Token Storage]\n I[Dynamic Client Registration]\n end\n\n A --> B\n B --> C\n B --> D\n B --> E\n B --> F\n A --> G\n G --> H\n G --> I\n```\n\n### Core Components\n\n#### 1. The HTTP Server (`server.ts`)\n\nThe heart of OAuth Callback is a lightweight HTTP server that:\n\n* Listens on localhost for the OAuth callback\n* Parses query parameters from the redirect\n* Serves success/error HTML pages\n* Implements proper cleanup on completion\n\nInternally, the server handles:\n\n* Request routing (`/callback` path matching)\n* Query parameter extraction (`code`, `state`, `error`)\n* HTML template rendering with placeholders\n* Graceful shutdown after callback\n\n#### 2. The Authorization Handler (`getAuthCode`)\n\nThe main API surface that orchestrates the entire flow:\n\n```typescript\ninterface GetAuthCodeOptions {\n authorizationUrl: string; // OAuth provider URL\n port?: number; // Server port (default: 3000)\n timeout?: number; // Timeout in ms (default: 30000)\n launch?: (url: string) => unknown; // Optional URL launcher\n signal?: AbortSignal; // For cancellation\n // ... more options\n}\n```\n\n#### 3. Error Management (`OAuthError`)\n\nSpecialized error handling for OAuth-specific failures:\n\n```typescript\nclass OAuthError extends Error {\n error: string; // OAuth error code\n error_description?: string; // Human-readable description\n error_uri?: string; // Link to more information\n}\n```\n\nCommon OAuth errors are properly typed and handled:\n\n* `access_denied` - User declined authorization\n* `invalid_scope` - Requested scope is invalid\n* `server_error` - Authorization server error\n* `temporarily_unavailable` - Server is overloaded\n\n## Token Management\n\nFor applications that need to persist OAuth tokens, OAuth Callback provides a flexible storage abstraction:\n\n### Storage Abstraction\n\nThe `TokenStore` interface enables different storage strategies:\n\n```typescript\ninterface TokenStore {\n get(key: string): Promise;\n set(key: string, tokens: Tokens): Promise;\n delete(key: string): Promise;\n}\n```\n\n### Built-in Implementations\n\n#### In-Memory Store\n\nEphemeral storage for maximum security:\n\n```typescript\nconst store = inMemoryStore();\n// Tokens exist only during process lifetime\n// Perfect for CLI tools that authenticate per-session\n```\n\n#### File Store\n\nPersistent storage for convenience:\n\n```typescript\nconst store = fileStore(\"~/.myapp/tokens.json\");\n// Tokens persist across sessions\n// Ideal for desktop apps with returning users\n```\n\n### Token Lifecycle\n\nOAuth Callback uses re-authentication instead of refresh tokens. When tokens expire, the provider returns `undefined`, signaling the MCP SDK to re-initiate the OAuth flow. This simplifies implementation and avoids storing long-lived refresh credentials.\n\n```mermaid\nstateDiagram-v2\n [*] --> NoToken: Initial State\n NoToken --> Authorizing: User initiates OAuth\n Authorizing --> HasToken: Successful auth\n HasToken --> Authorizing: Token expired (re-auth)\n HasToken --> NoToken: User logs out\n```\n\n## MCP Integration Pattern\n\nThe Model Context Protocol (MCP) integration showcases advanced OAuth patterns:\n\n### Dynamic Client Registration\n\nOAuth Callback supports [RFC 7591](https://www.rfc-editor.org/rfc/rfc7591.html) Dynamic Client Registration, allowing apps to register OAuth clients on-the-fly:\n\n```mermaid\nsequenceDiagram\n participant App\n participant OAuth Callback\n participant Auth Server\n\n App->>OAuth Callback: browserAuth() (no client_id)\n OAuth Callback->>Auth Server: POST /register\n Auth Server->>OAuth Callback: Return client_id, client_secret\n OAuth Callback->>OAuth Callback: Store credentials\n OAuth Callback->>Auth Server: Start normal OAuth flow\n```\n\nThis eliminates the need for users to manually register OAuth applications.\n\n### The Provider Pattern\n\nThe `browserAuth()` function returns an `OAuthClientProvider` that integrates with MCP SDK:\n\n```typescript\ninterface OAuthClientProvider {\n // Token access - returns undefined when expired, triggering re-auth\n tokens(): Promise;\n saveTokens(tokens: OAuthTokens): Promise;\n\n // Completes full OAuth flow (browser → callback → token exchange)\n redirectToAuthorization(authorizationUrl: URL): Promise;\n\n // PKCE and state management\n codeVerifier(): Promise;\n saveCodeVerifier(verifier: string): Promise;\n state(): Promise;\n}\n```\n\n## Request/Response Lifecycle\n\nUnderstanding the complete lifecycle helps when debugging OAuth flows:\n\n```mermaid\nsequenceDiagram\n participant User\n participant App\n participant OAuth Callback\n participant Browser\n participant Auth Server\n\n User->>App: Run command\n App->>OAuth Callback: getAuthCode(url)\n OAuth Callback->>OAuth Callback: Start HTTP server\n OAuth Callback->>Browser: Open auth URL\n Browser->>Auth Server: GET /authorize\n Auth Server->>Browser: Login page\n Browser->>User: Show login\n User->>Browser: Enter credentials\n Browser->>Auth Server: POST credentials\n Auth Server->>Browser: Consent page\n User->>Browser: Approve access\n Auth Server->>Browser: Redirect to localhost\n Browser->>OAuth Callback: GET /callback?code=xyz\n OAuth Callback->>Browser: Success HTML\n OAuth Callback->>App: Return {code: \"xyz\"}\n OAuth Callback->>OAuth Callback: Shutdown server\n App->>Auth Server: Exchange code for token\n Auth Server->>App: Return access token\n```\n\n## State Management\n\nOAuth Callback handles multiple types of state throughout the flow:\n\n### Server State\n\nThe HTTP server maintains minimal state:\n\n* **Active**: Server is listening for callbacks\n* **Received**: Callback has been received\n* **Shutdown**: Server is closing\n\n### OAuth State\n\nThe OAuth flow tracks:\n\n* **Authorization URL**: Where to send the user\n* **Expected state**: For CSRF validation\n* **Timeout timer**: For abandonment detection\n* **Abort signal**: For cancellation support\n\n### Token State\n\nWhen using token storage:\n\n* **No tokens**: Need to authenticate\n* **Valid tokens**: Can make API calls\n* **Expired tokens**: Triggers re-authentication (no refresh tokens used)\n\n## Security Architecture\n\nSecurity is built into every layer of OAuth Callback:\n\n### Network Security\n\n```typescript\n// Localhost-only binding\nserver.listen(port, \"127.0.0.1\");\n\n// IPv6 localhost support\nserver.listen(port, \"::1\");\n\n// Reject non-localhost connections\nif (!isLocalhost(request.socket.remoteAddress)) {\n return reject();\n}\n```\n\n### OAuth Security\n\n* **State parameter**: Prevents CSRF attacks\n* **PKCE support**: Protects authorization codes\n* **Timeout enforcement**: Limits exposure window\n* **Automatic cleanup**: Reduces attack surface\n\n### Token Security\n\n* **Memory storage option**: No persistence\n* **File permissions**: Restrictive when using file store\n* **No logging**: Tokens never logged or exposed\n* **Expiry handling**: Automatic re-auth when tokens expire\n\n## Template System\n\nOAuth Callback includes a simple but powerful template system for success/error pages:\n\n### Placeholder Substitution\n\nTemplates support `{{placeholder}}` syntax:\n\n```html\n

Error: {{error_description}}

\n```\n\nPlaceholders are automatically escaped to prevent XSS attacks.\n\n### Built-in Templates\n\nThe library includes professional templates with:\n\n* Animated success checkmark\n* Clear error messages\n* Responsive design\n* Accessibility features\n\n### Custom Templates\n\nApplications can provide custom HTML:\n\n```typescript\n{\n successHtml: \"

Welcome back!

\",\n errorHtml: \"

Oops! {{error}}

\"\n}\n```\n\n## Cross-Runtime Compatibility\n\nOAuth Callback achieves cross-runtime compatibility through Web Standards APIs:\n\n### Universal APIs\n\n```typescript\n// Using Web Standards instead of Node.js-specific APIs\nnew Request(); // Instead of http.IncomingMessage\nnew Response(); // Instead of http.ServerResponse\nnew URL(); // Instead of url.parse()\nnew URLSearchParams(); // Instead of querystring\n```\n\n### Runtime Detection\n\nThe library adapts to the runtime environment:\n\n```typescript\n// Node.js\nimport { createServer } from \"node:http\";\n\n// Deno\nDeno.serve({ port: 3000 });\n\n// Bun\nBun.serve({ port: 3000 });\n```\n\n## Performance Considerations\n\nOAuth Callback is designed for optimal performance:\n\n### Fast Startup\n\n* Minimal dependencies (only `open` package)\n* Lazy loading of heavy modules\n* Pre-compiled HTML templates\n\n### Efficient Memory Use\n\n* Server resources freed immediately after use\n* No persistent connections\n* Minimal state retention\n\n### Quick Response\n\n* Immediate browser redirect handling\n* Non-blocking I/O operations\n* Parallel browser launch and server start\n\n## Extension Points\n\nWhile OAuth Callback provides sensible defaults, it offers multiple extension points:\n\n### Custom Storage\n\nImplement the `TokenStore` interface for custom storage:\n\n```typescript\nclass RedisStore implements TokenStore {\n async get(key: string) {\n /* Redis logic */\n }\n async set(key: string, tokens: Tokens) {\n /* Redis logic */\n }\n async delete(key: string) {\n /* Redis logic */\n }\n}\n```\n\n### Request Interception\n\nMonitor or modify requests with callbacks:\n\n```typescript\n{\n onRequest: (req) => {\n console.log(`OAuth: ${req.method} ${req.url}`);\n // Add telemetry, logging, etc.\n };\n}\n```\n\n### Custom URL Launcher\n\nCustomize how the authorization URL is opened:\n\n```typescript\nimport open from \"open\";\n\n// Use system browser\nawait getAuthCode({ authorizationUrl, launch: open });\n\n// Headless mode - omit launch, print URL manually\nconsole.log(`Open: ${authorizationUrl}`);\nawait getAuthCode({ authorizationUrl });\n```\n\n## Best Practices\n\n### Error Handling\n\nAlways handle both OAuth errors and unexpected failures:\n\n```typescript\ntry {\n const result = await getAuthCode(authUrl);\n} catch (error) {\n if (error instanceof OAuthError) {\n // Handle OAuth-specific errors\n } else {\n // Handle unexpected errors\n }\n}\n```\n\n### State Validation\n\nAlways validate the state parameter:\n\n```typescript\nconst state = crypto.randomUUID();\n// Include in auth URL\nconst result = await getAuthCode(authUrl);\nif (result.state !== state) throw new Error(\"CSRF detected\");\n```\n\n### Token Storage\n\nChoose storage based on security requirements:\n\n* **CLI tools**: Use `inMemoryStore()` for per-session auth\n* **Desktop apps**: Use `fileStore()` for user convenience\n* **Sensitive apps**: Always use in-memory storage\n\n### Timeout Configuration\n\nSet appropriate timeouts for your use case:\n\n* **Interactive apps**: 30-60 seconds\n* **Automated tools**: 5-10 seconds\n* **First-time setup**: 2-5 minutes\n\n---\n\n---\nurl: /oauth-callback/examples.md\n---\n# Examples\n\n---\n\n---\nurl: /oauth-callback/api/get-auth-code.md\ndescription: >-\n Core function for capturing OAuth authorization codes via localhost callback\n in CLI tools and desktop applications.\n---\n\n# getAuthCode\n\nThe `getAuthCode` function is the primary API for capturing OAuth authorization codes through a localhost callback. It handles the entire OAuth flow: starting a local server, opening the browser, waiting for the callback, and returning the authorization code.\n\n## Function Signature\n\n```typescript\nfunction getAuthCode(\n input: string | GetAuthCodeOptions,\n): Promise;\n```\n\n## Parameters\n\nThe function accepts either:\n\n* A **string** containing the OAuth authorization URL (uses default options)\n* A **GetAuthCodeOptions** object for advanced configuration\n\n### GetAuthCodeOptions\n\n| Property | Type | Default | Description |\n| ------------------ | -------------------------- | ------------- | --------------------------------------------- |\n| `authorizationUrl` | `string` | *required* | OAuth authorization URL with query parameters |\n| `port` | `number` | `3000` | Port for the local callback server |\n| `hostname` | `string` | `\"localhost\"` | Hostname to bind the server to |\n| `callbackPath` | `string` | `\"/callback\"` | URL path for OAuth callback |\n| `timeout` | `number` | `30000` | Timeout in milliseconds |\n| `launch` | `(url: string) => unknown` | *none* | Optional callback to launch auth URL |\n| `successHtml` | `string` | *built-in* | Custom HTML for successful auth |\n| `errorHtml` | `string` | *built-in* | Custom HTML template for errors |\n| `signal` | `AbortSignal` | *none* | For programmatic cancellation |\n| `onRequest` | `(req: Request) => void` | *none* | Callback for request logging |\n\n## Return Value\n\nReturns a `Promise` containing:\n\n```typescript\ninterface CallbackResult {\n code: string; // Authorization code\n state?: string; // State parameter (if provided)\n [key: string]: any; // Additional query parameters\n}\n```\n\n## Exceptions\n\nThe function can throw:\n\n| Error Type | Condition | Description |\n| ------------ | -------------------- | --------------------------------------------------------------- |\n| `OAuthError` | OAuth provider error | Contains `error`, `error_description`, and optional `error_uri` |\n| `Error` | Timeout | \"Timeout waiting for callback\" |\n| `Error` | Port in use | \"EADDRINUSE\" - port already occupied |\n| `Error` | Cancellation | \"Operation aborted\" via AbortSignal |\n\n## Basic Usage\n\n### With Browser Launch\n\nThe recommended usage with automatic browser opening:\n\n```typescript\nimport open from \"open\";\nimport { getAuthCode } from \"oauth-callback\";\n\nconst authUrl =\n \"https://github.com/login/oauth/authorize?\" +\n new URLSearchParams({\n client_id: \"your_client_id\",\n redirect_uri: \"http://localhost:3000/callback\",\n scope: \"user:email\",\n state: \"random_state\",\n });\n\nconst result = await getAuthCode({ authorizationUrl: authUrl, launch: open });\nconsole.log(\"Authorization code:\", result.code);\nconsole.log(\"State:\", result.state);\n```\n\n### With Configuration Object\n\nUsing the options object for more control:\n\n```typescript\nconst result = await getAuthCode({\n authorizationUrl: authUrl,\n port: 8080,\n timeout: 60000,\n hostname: \"127.0.0.1\",\n});\n```\n\n## Advanced Usage\n\n### Custom Port Configuration\n\nWhen port 3000 is unavailable or you've registered a different redirect URI:\n\n```typescript\nconst result = await getAuthCode({\n authorizationUrl: \"https://oauth.example.com/authorize?...\",\n port: 8888,\n callbackPath: \"/oauth/callback\", // Custom path\n hostname: \"127.0.0.1\", // Specific IP binding\n});\n```\n\n::: warning Port Configuration\nEnsure the port and path match your OAuth app's registered redirect URI:\n\n* Registered: `http://localhost:8888/oauth/callback`\n* Configuration must use: `port: 8888`, `callbackPath: \"/oauth/callback\"`\n :::\n\n### Custom HTML Templates\n\nProvide branded success and error pages:\n\n```typescript\nconst result = await getAuthCode({\n authorizationUrl: authUrl,\n successHtml: `\n \n \n \n Success!\n \n \n \n
\n

✨ Authorization Successful!

\n

You can close this window and return to the app.

\n
\n \n \n `,\n errorHtml: `\n \n \n \n

Authorization Failed

\n

Error: {{error}}

\n

{{error_description}}

\n More information\n \n \n `,\n});\n```\n\n::: tip Template Placeholders\nError templates support these placeholders:\n\n* `{{error}}` - OAuth error code\n* `{{error_description}}` - Human-readable description\n* `{{error_uri}}` - Link to error documentation\n :::\n\n### Request Logging\n\nMonitor OAuth flow for debugging:\n\n```typescript\nconst result = await getAuthCode({\n authorizationUrl: authUrl,\n onRequest: (req) => {\n const url = new URL(req.url);\n console.log(`[${new Date().toISOString()}] ${req.method} ${url.pathname}`);\n\n // Log specific paths\n if (url.pathname === \"/callback\") {\n console.log(\n \"Callback received with params:\",\n url.searchParams.toString(),\n );\n }\n },\n});\n```\n\n### Timeout Handling\n\nConfigure timeout for different scenarios:\n\n```typescript\ntry {\n const result = await getAuthCode({\n authorizationUrl: authUrl,\n timeout: 120000, // 2 minutes for first-time users\n });\n} catch (error) {\n if (error.message === \"Timeout waiting for callback\") {\n console.error(\"Authorization took too long. Please try again.\");\n }\n}\n```\n\n### Programmatic Cancellation\n\nSupport user-initiated cancellation:\n\n```typescript\nconst controller = new AbortController();\n\n// Listen for Ctrl+C\nprocess.on(\"SIGINT\", () => {\n console.log(\"\\nCancelling authorization...\");\n controller.abort();\n});\n\n// Set a maximum time limit\nconst timeoutId = setTimeout(() => {\n console.log(\"Authorization time limit reached\");\n controller.abort();\n}, 300000); // 5 minutes\n\ntry {\n const result = await getAuthCode({\n authorizationUrl: authUrl,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n console.log(\"Success! Code:\", result.code);\n} catch (error) {\n if (error.message === \"Operation aborted\") {\n console.log(\"Authorization was cancelled\");\n }\n}\n```\n\n### Headless / Manual Browser Control\n\nFor environments where you want to handle browser opening yourself (SSH, CI, etc.):\n\n```typescript\n// Headless mode - print URL, let user open manually\nconst redirectUri = \"http://localhost:3000/callback\";\nconst authUrl = `https://oauth.example.com/authorize?redirect_uri=${encodeURIComponent(redirectUri)}`;\n\nconsole.log(\"Please open this URL in your browser:\");\nconsole.log(authUrl);\n\n// Server waits for callback without opening browser\nconst result = await getAuthCode({ port: 3000, timeout: 120000 });\n```\n\nOr use a custom launcher:\n\n```typescript\nimport open from \"open\";\n\nconst result = await getAuthCode({\n authorizationUrl: authUrl,\n launch: open, // Both authorizationUrl and launch are required together\n});\n```\n\n## Error Handling\n\n### Comprehensive Error Handling\n\nHandle all possible error scenarios:\n\n```typescript\nimport open from \"open\";\nimport { getAuthCode, OAuthError } from \"oauth-callback\";\n\ntry {\n const result = await getAuthCode({ authorizationUrl: authUrl, launch: open });\n // Success - exchange code for token\n return result.code;\n} catch (error) {\n if (error instanceof OAuthError) {\n // OAuth-specific errors from provider\n switch (error.error) {\n case \"access_denied\":\n console.log(\"User cancelled authorization\");\n break;\n\n case \"invalid_scope\":\n console.error(\"Requested scope is invalid:\", error.error_description);\n break;\n\n case \"server_error\":\n console.error(\"OAuth server error. Please try again later.\");\n break;\n\n case \"temporarily_unavailable\":\n console.error(\"OAuth service is temporarily unavailable\");\n break;\n\n default:\n console.error(`OAuth error: ${error.error}`);\n if (error.error_description) {\n console.error(`Details: ${error.error_description}`);\n }\n if (error.error_uri) {\n console.error(`More info: ${error.error_uri}`);\n }\n }\n } else if (error.code === \"EADDRINUSE\") {\n console.error(`Port ${port} is already in use. Try a different port.`);\n } else if (error.message === \"Timeout waiting for callback\") {\n console.error(\"Authorization timed out. Please try again.\");\n } else if (error.message === \"Operation aborted\") {\n console.log(\"Authorization was cancelled by user\");\n } else {\n // Unexpected errors\n console.error(\"Unexpected error:\", error);\n }\n\n throw error; // Re-throw for upstream handling\n}\n```\n\n### Retry Logic\n\nImplement retry for transient failures:\n\n```typescript\nasync function getAuthCodeWithRetry(\n authUrl: string,\n maxAttempts = 3,\n): Promise {\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n const result = await getAuthCode({\n authorizationUrl: authUrl,\n port: 3000 + attempt - 1, // Try different ports\n timeout: 30000 * attempt, // Increase timeout each attempt\n });\n return result.code;\n } catch (error) {\n console.log(`Attempt ${attempt} failed:`, error.message);\n\n if (attempt === maxAttempts) {\n throw error;\n }\n\n // Don't retry user cancellations\n if (error instanceof OAuthError && error.error === \"access_denied\") {\n throw error;\n }\n\n console.log(`Retrying... (${attempt + 1}/${maxAttempts})`);\n }\n }\n}\n```\n\n## Security Best Practices\n\n### State Parameter Validation\n\nAlways validate the state parameter to prevent CSRF attacks:\n\n```typescript\nimport { randomBytes } from \"crypto\";\n\n// Generate secure random state\nconst state = randomBytes(32).toString(\"base64url\");\n\nconst authUrl = new URL(\"https://oauth.example.com/authorize\");\nauthUrl.searchParams.set(\"client_id\", CLIENT_ID);\nauthUrl.searchParams.set(\"redirect_uri\", \"http://localhost:3000/callback\");\nauthUrl.searchParams.set(\"state\", state);\nauthUrl.searchParams.set(\"scope\", \"read write\");\n\nconst result = await getAuthCode(authUrl.toString());\n\n// Validate state matches\nif (result.state !== state) {\n throw new Error(\"State mismatch - possible CSRF attack!\");\n}\n\n// Safe to use authorization code\nconsole.log(\"Valid authorization code:\", result.code);\n```\n\n### PKCE Implementation\n\nImplement Proof Key for Code Exchange for public clients:\n\n```typescript\nimport { createHash, randomBytes } from \"crypto\";\n\n// Generate PKCE challenge\nconst verifier = randomBytes(32).toString(\"base64url\");\nconst challenge = createHash(\"sha256\").update(verifier).digest(\"base64url\");\n\n// Include challenge in authorization request\nconst authUrl = new URL(\"https://oauth.example.com/authorize\");\nauthUrl.searchParams.set(\"code_challenge\", challenge);\nauthUrl.searchParams.set(\"code_challenge_method\", \"S256\");\n// ... other parameters\n\nconst result = await getAuthCode(authUrl.toString());\n\n// Include verifier when exchanging code\nconst tokenResponse = await fetch(\"https://oauth.example.com/token\", {\n method: \"POST\",\n body: new URLSearchParams({\n grant_type: \"authorization_code\",\n code: result.code,\n code_verifier: verifier, // Include PKCE verifier\n client_id: CLIENT_ID,\n redirect_uri: \"http://localhost:3000/callback\",\n }),\n});\n```\n\n## Complete Examples\n\n### GitHub OAuth Integration\n\nFull example with error handling and token exchange:\n\n```typescript\nimport { getAuthCode, OAuthError } from \"oauth-callback\";\n\nasync function authenticateWithGitHub() {\n const CLIENT_ID = process.env.GITHUB_CLIENT_ID;\n const CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET;\n\n // Build authorization URL with all parameters\n const authUrl = new URL(\"https://github.com/login/oauth/authorize\");\n authUrl.searchParams.set(\"client_id\", CLIENT_ID);\n authUrl.searchParams.set(\"redirect_uri\", \"http://localhost:3000/callback\");\n authUrl.searchParams.set(\"scope\", \"user:email repo\");\n authUrl.searchParams.set(\"state\", crypto.randomUUID());\n\n try {\n // Get authorization code\n console.log(\"Opening browser for GitHub authorization...\");\n const result = await getAuthCode({\n authorizationUrl: authUrl.toString(),\n timeout: 60000,\n successHtml: \"

✅ GitHub authorization successful!

\",\n });\n\n // Exchange code for access token\n console.log(\"Exchanging code for access token...\");\n const tokenResponse = await fetch(\n \"https://github.com/login/oauth/access_token\",\n {\n method: \"POST\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n client_id: CLIENT_ID,\n client_secret: CLIENT_SECRET,\n code: result.code,\n }),\n },\n );\n\n const tokens = await tokenResponse.json();\n\n if (tokens.error) {\n throw new Error(`Token exchange failed: ${tokens.error_description}`);\n }\n\n // Use access token to get user info\n const userResponse = await fetch(\"https://api.github.com/user\", {\n headers: {\n Authorization: `Bearer ${tokens.access_token}`,\n Accept: \"application/vnd.github.v3+json\",\n },\n });\n\n const user = await userResponse.json();\n console.log(`Authenticated as: ${user.login}`);\n\n return tokens.access_token;\n } catch (error) {\n if (error instanceof OAuthError) {\n console.error(\"GitHub authorization failed:\", error.error_description);\n } else {\n console.error(\"Authentication error:\", error.message);\n }\n throw error;\n }\n}\n```\n\n### Multi-Provider Support\n\nHandle multiple OAuth providers with a unified interface:\n\n```typescript\ntype Provider = \"github\" | \"google\" | \"microsoft\";\n\nasync function authenticate(provider: Provider): Promise {\n const configs = {\n github: {\n authUrl: \"https://github.com/login/oauth/authorize\",\n tokenUrl: \"https://github.com/login/oauth/access_token\",\n scope: \"user:email\",\n },\n google: {\n authUrl: \"https://accounts.google.com/o/oauth2/v2/auth\",\n tokenUrl: \"https://oauth2.googleapis.com/token\",\n scope: \"openid email profile\",\n },\n microsoft: {\n authUrl: \"https://login.microsoftonline.com/common/oauth2/v2.0/authorize\",\n tokenUrl: \"https://login.microsoftonline.com/common/oauth2/v2.0/token\",\n scope: \"user.read\",\n },\n };\n\n const config = configs[provider];\n const authUrl = new URL(config.authUrl);\n\n // Add provider-specific parameters\n authUrl.searchParams.set(\n \"client_id\",\n process.env[`${provider.toUpperCase()}_CLIENT_ID`],\n );\n authUrl.searchParams.set(\"redirect_uri\", \"http://localhost:3000/callback\");\n authUrl.searchParams.set(\"scope\", config.scope);\n authUrl.searchParams.set(\"response_type\", \"code\");\n authUrl.searchParams.set(\"state\", crypto.randomUUID());\n\n if (provider === \"google\") {\n authUrl.searchParams.set(\"access_type\", \"offline\");\n authUrl.searchParams.set(\"prompt\", \"consent\");\n }\n\n const result = await getAuthCode({\n authorizationUrl: authUrl.toString(),\n timeout: 90000,\n onRequest: (req) => {\n console.log(`[${provider}] ${req.method} ${new URL(req.url).pathname}`);\n },\n });\n\n return result.code;\n}\n```\n\n## Testing\n\n### Unit Testing\n\nMock the OAuth flow for testing:\n\n```typescript\nimport { getAuthCode } from \"oauth-callback\";\nimport { describe, it, expect } from \"vitest\";\n\ndescribe(\"OAuth Flow\", () => {\n it(\"should capture authorization code\", async () => {\n // Start mock OAuth server\n const mockServer = createMockOAuthServer();\n await mockServer.start();\n\n const result = await getAuthCode({\n authorizationUrl: `http://localhost:${mockServer.port}/authorize`,\n port: 3001,\n // No launch callback - tests simulate OAuth redirect\n timeout: 5000,\n });\n\n expect(result.code).toBe(\"test_auth_code\");\n expect(result.state).toBe(\"test_state\");\n\n await mockServer.stop();\n });\n\n it(\"should handle OAuth errors\", async () => {\n const mockServer = createMockOAuthServer({\n error: \"access_denied\",\n });\n await mockServer.start();\n\n await expect(\n getAuthCode({\n authorizationUrl: `http://localhost:${mockServer.port}/authorize`,\n // No launch - test simulates OAuth redirect\n }),\n ).rejects.toThrow(OAuthError);\n\n await mockServer.stop();\n });\n});\n```\n\n## Migration Guide\n\n### From v1.x to v2.x\n\n```typescript\n// v1.x (old)\nconst code = await captureAuthCode(url, 3000);\n\n// v2.x (new)\nconst result = await getAuthCode({\n authorizationUrl: url,\n port: 3000,\n});\nconst code = result.code;\n```\n\n## Related APIs\n\n* [`OAuthError`](/api/oauth-error) - OAuth-specific error class\n* [`browserAuth`](/api/browser-auth) - MCP SDK integration provider\n* [`TokenStore`](/api/storage-providers) - Token storage interface\n\n---\n\n---\nurl: /oauth-callback/getting-started.md\ndescription: >-\n Quick start guide to implement OAuth 2.0 authorization code flow in your CLI\n tools, desktop apps, and MCP clients using oauth-callback.\n---\n\n# Getting Started {#top}\n\nThis guide will walk you through adding OAuth authentication to your application in just a few minutes. Whether you're building a CLI tool, desktop app, or MCP client, **OAuth Callback** handles the complexity of receiving authorization codes via localhost callbacks.\n\n## Prerequisites\n\nBefore you begin, ensure you have:\n\n* **Runtime**: Node.js 18+, Deno, or Bun installed\n* **OAuth App**: Registered with your OAuth provider (unless using Dynamic Client Registration)\n* **Redirect URI**: Set to `http://localhost:3000/callback` in your OAuth app settings\n\n## Installation\n\nInstall the package using your preferred package manager:\n\n::: code-group\n\n```bash [Bun]\nbun add oauth-callback open\n```\n\n```bash [npm]\nnpm install oauth-callback open\n```\n\n```bash [pnpm]\npnpm add oauth-callback open\n```\n\n```bash [Yarn]\nyarn add oauth-callback open\n```\n\n:::\n\n> **Note:** The `open` package is optional but recommended for launching the browser. Omit it for headless environments.\n\n## Basic Usage\n\nThe simplest way to capture an OAuth authorization code is with the `getAuthCode()` function:\n\n```typescript\nimport open from \"open\";\nimport { getAuthCode } from \"oauth-callback\";\n\n// Construct your OAuth authorization URL\nconst authUrl =\n \"https://github.com/login/oauth/authorize?\" +\n new URLSearchParams({\n client_id: \"your_client_id\",\n redirect_uri: \"http://localhost:3000/callback\",\n scope: \"user:email\",\n state: crypto.randomUUID(), // For CSRF protection\n });\n\n// Get the authorization code (launch: open opens the browser)\nconst result = await getAuthCode({ authorizationUrl: authUrl, launch: open });\n\nconsole.log(\"Authorization code:\", result.code);\nconsole.log(\"State:\", result.state);\n```\n\nThat's it! The library will:\n\n1. Start a local HTTP server on port 3000\n2. Open the user's browser to the authorization URL\n3. Capture the callback with the authorization code\n4. Return the code and automatically shut down the server\n\n## Step-by-Step Implementation\n\nLet's build a complete OAuth flow for a CLI application:\n\n### Step 1: Register Your OAuth Application\n\nFirst, register your application with your OAuth provider:\n\n::: details GitHub OAuth Setup\n\n1. Go to **Settings** → **Developer settings** → **OAuth Apps**\n2. Click **New OAuth App**\n3. Fill in:\n * **Application name**: Your app name\n * **Homepage URL**: Your website or GitHub repo\n * **Authorization callback URL**: `http://localhost:3000/callback`\n4. Save and copy your **Client ID** and **Client Secret**\n :::\n\n::: details Google OAuth Setup\n\n1. Go to [Google Cloud Console](https://console.cloud.google.com/)\n2. Create or select a project\n3. Enable the necessary APIs\n4. Go to **APIs & Services** → **Credentials**\n5. Click **Create Credentials** → **OAuth client ID**\n6. Choose **Desktop app** as application type\n7. Add `http://localhost:3000/callback` to authorized redirect URIs\n8. Copy your **Client ID** and **Client Secret**\n :::\n\n### Step 2: Implement the Authorization Flow\n\nCreate a file `auth.ts` with your OAuth implementation:\n\n```typescript\nimport open from \"open\";\nimport { getAuthCode, OAuthError } from \"oauth-callback\";\n\nasync function authenticate() {\n // Generate state for CSRF protection\n const state = crypto.randomUUID();\n\n // Build authorization URL\n const authUrl = new URL(\"https://github.com/login/oauth/authorize\");\n authUrl.searchParams.set(\"client_id\", process.env.GITHUB_CLIENT_ID!);\n authUrl.searchParams.set(\"redirect_uri\", \"http://localhost:3000/callback\");\n authUrl.searchParams.set(\"scope\", \"user:email\");\n authUrl.searchParams.set(\"state\", state);\n\n try {\n // Get authorization code\n console.log(\"Opening browser for authentication...\");\n const result = await getAuthCode({\n authorizationUrl: authUrl.toString(),\n launch: open,\n });\n\n // Validate state\n if (result.state !== state) {\n throw new Error(\"State mismatch - possible CSRF attack\");\n }\n\n console.log(\"✅ Authorization successful!\");\n return result.code;\n } catch (error) {\n if (error instanceof OAuthError) {\n console.error(\"❌ OAuth error:\", error.error_description || error.error);\n } else {\n console.error(\"❌ Unexpected error:\", error);\n }\n throw error;\n }\n}\n```\n\n### Step 3: Exchange Code for Access Token\n\nAfter getting the authorization code, exchange it for an access token:\n\n```typescript\nasync function exchangeCodeForToken(code: string) {\n const response = await fetch(\"https://github.com/login/oauth/access_token\", {\n method: \"POST\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n client_id: process.env.GITHUB_CLIENT_ID,\n client_secret: process.env.GITHUB_CLIENT_SECRET,\n code: code,\n }),\n });\n\n if (!response.ok) {\n throw new Error(`Token exchange failed: ${response.statusText}`);\n }\n\n const data = await response.json();\n\n if (data.error) {\n throw new Error(`OAuth error: ${data.error_description || data.error}`);\n }\n\n return data.access_token;\n}\n```\n\n### Step 4: Use the Access Token\n\nNow you can use the access token to make authenticated API requests:\n\n```typescript\nasync function getUserInfo(accessToken: string) {\n const response = await fetch(\"https://api.github.com/user\", {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: \"application/vnd.github.v3+json\",\n },\n });\n\n if (!response.ok) {\n throw new Error(`API request failed: ${response.statusText}`);\n }\n\n return response.json();\n}\n\n// Complete flow\nasync function main() {\n const code = await authenticate();\n const token = await exchangeCodeForToken(code);\n const user = await getUserInfo(token);\n\n console.log(`Hello, ${user.name}! 👋`);\n console.log(`Email: ${user.email}`);\n}\n\nmain().catch(console.error);\n```\n\n## MCP SDK Integration\n\nFor Model Context Protocol applications, use the `browserAuth()` provider for seamless integration:\n\n### Quick Setup\n\n```typescript\nimport open from \"open\";\nimport { browserAuth, inMemoryStore } from \"oauth-callback/mcp\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { StreamableHTTPClientTransport } from \"@modelcontextprotocol/sdk/client/streamableHttp.js\";\n\n// Create OAuth provider for MCP\nconst authProvider = browserAuth({\n launch: open,\n store: inMemoryStore(), // Or fileStore() for persistence\n scope: \"read write\",\n});\n\n// Connect to MCP server with OAuth\nconst transport = new StreamableHTTPClientTransport(\n new URL(\"https://mcp.notion.com/mcp\"),\n { authProvider },\n);\n\nconst client = new Client(\n { name: \"my-app\", version: \"1.0.0\" },\n { capabilities: {} },\n);\n\nawait client.connect(transport);\n```\n\n### Token Storage Options\n\nChoose between ephemeral and persistent token storage:\n\n::: code-group\n\n```typescript [Ephemeral (Memory)]\nimport open from \"open\";\nimport { browserAuth, inMemoryStore } from \"oauth-callback/mcp\";\n\n// Tokens are lost when the process exits\nconst authProvider = browserAuth({\n launch: open,\n store: inMemoryStore(),\n});\n```\n\n```typescript [Persistent (File)]\nimport open from \"open\";\nimport { browserAuth, fileStore } from \"oauth-callback/mcp\";\n\n// Tokens persist across sessions\nconst authProvider = browserAuth({\n launch: open,\n store: fileStore(), // Saves to ~/.mcp/tokens.json\n});\n\n// Or specify custom location\nconst customAuth = browserAuth({\n launch: open,\n store: fileStore(\"/path/to/tokens.json\"),\n});\n```\n\n:::\n\n### Pre-configured Credentials\n\nIf you have pre-registered OAuth credentials:\n\n```typescript\nimport open from \"open\";\n\nconst authProvider = browserAuth({\n clientId: \"your-client-id\",\n clientSecret: \"your-client-secret\",\n scope: \"read write\",\n launch: open,\n store: fileStore(),\n storeKey: \"my-app\", // Namespace for multiple apps\n});\n```\n\n## Advanced Configuration\n\n### Custom Port and Timeout\n\nConfigure the callback server port and timeout:\n\n```typescript\nconst result = await getAuthCode({\n authorizationUrl: authUrl,\n port: 8080, // Use port 8080 instead of 3000\n timeout: 60000, // 60 second timeout (default: 30s)\n hostname: \"127.0.0.1\", // Bind to specific IP\n});\n```\n\n### Custom HTML Templates\n\nCustomize the success and error pages shown to users:\n\n```typescript\nconst result = await getAuthCode({\n authorizationUrl: authUrl,\n successHtml: `\n \n \n

✅ Authorization Successful!

\n

You can now close this window and return to the application.

\n \n \n `,\n errorHtml: `\n \n \n

❌ Authorization Failed

\n

Error: {{error_description}}

\n

Please try again or contact support.

\n \n \n `,\n});\n```\n\n### Request Logging\n\nAdd logging for debugging OAuth flows:\n\n```typescript\nconst result = await getAuthCode({\n authorizationUrl: authUrl,\n onRequest: (req) => {\n console.log(`[OAuth] ${req.method} ${req.url}`);\n console.log(\"[OAuth] Headers:\", Object.fromEntries(req.headers));\n },\n});\n```\n\n### Programmatic Cancellation\n\nSupport user cancellation with AbortSignal:\n\n```typescript\nconst controller = new AbortController();\n\n// Cancel after 10 seconds\nsetTimeout(() => controller.abort(), 10000);\n\n// Or cancel on user input\nprocess.on(\"SIGINT\", () => {\n console.log(\"\\nCancelling OAuth flow...\");\n controller.abort();\n});\n\ntry {\n const result = await getAuthCode({\n authorizationUrl: authUrl,\n signal: controller.signal,\n });\n} catch (error) {\n if (error.message === \"Operation aborted\") {\n console.log(\"OAuth flow was cancelled\");\n }\n}\n```\n\n## Error Handling\n\nProper error handling ensures a good user experience:\n\n```typescript\nimport open from \"open\";\nimport { getAuthCode, OAuthError } from \"oauth-callback\";\n\ntry {\n const result = await getAuthCode({ authorizationUrl: authUrl, launch: open });\n // Success path\n} catch (error) {\n if (error instanceof OAuthError) {\n // OAuth-specific errors from the provider\n switch (error.error) {\n case \"access_denied\":\n console.error(\"User denied access\");\n break;\n case \"invalid_scope\":\n console.error(\"Invalid scope requested\");\n break;\n case \"server_error\":\n console.error(\"Authorization server error\");\n break;\n default:\n console.error(`OAuth error: ${error.error_description || error.error}`);\n }\n } else if (error.message === \"Timeout waiting for callback\") {\n console.error(\"Authorization timed out - please try again\");\n } else if (error.message === \"Operation aborted\") {\n console.error(\"Authorization was cancelled\");\n } else {\n console.error(\"Unexpected error:\", error);\n }\n}\n```\n\n## Security Best Practices\n\n### Always Use State Parameter\n\nProtect against CSRF attacks with a state parameter:\n\n```typescript\nconst state = crypto.randomUUID();\n\nconst authUrl = `https://example.com/authorize?state=${state}&...`;\nconst result = await getAuthCode(authUrl);\n\nif (result.state !== state) {\n throw new Error(\"State mismatch - possible CSRF attack\");\n}\n```\n\n### Implement PKCE for Public Clients\n\nFor enhanced security, implement Proof Key for Code Exchange:\n\n```typescript\nimport { createHash, randomBytes } from \"node:crypto\";\n\n// Generate PKCE challenge\nconst verifier = randomBytes(32).toString(\"base64url\");\nconst challenge = createHash(\"sha256\").update(verifier).digest(\"base64url\");\n\n// Include in authorization request\nconst authUrl = new URL(\"https://example.com/authorize\");\nauthUrl.searchParams.set(\"code_challenge\", challenge);\nauthUrl.searchParams.set(\"code_challenge_method\", \"S256\");\n// ... other parameters\n\nconst result = await getAuthCode(authUrl.toString());\n\n// Include verifier in token exchange\nconst tokenResponse = await fetch(tokenUrl, {\n method: \"POST\",\n body: new URLSearchParams({\n code: result.code,\n code_verifier: verifier,\n // ... other parameters\n }),\n});\n```\n\n### Secure Token Storage\n\nChoose appropriate token storage based on your security requirements:\n\n* **Use `inMemoryStore()`** for maximum security (tokens lost on restart)\n* **Use `fileStore()`** only when persistence is required\n* **Never commit tokens** to version control\n* **Consider encryption** for file-based storage in production\n\n## Testing Your Implementation\n\n### Local Testing with Demo\n\nTest the library without real OAuth credentials:\n\n```bash\n# Run interactive demo\nbun run example:demo\n\n# The demo includes a mock OAuth server for testing\n```\n\n### Testing with Real Providers\n\n::: code-group\n\n```bash [GitHub]\n# Set credentials in .env file\nGITHUB_CLIENT_ID=your_client_id\nGITHUB_CLIENT_SECRET=your_client_secret\n\n# Run example\nbun run example:github\n```\n\n```bash [Notion MCP]\n# No credentials needed - uses Dynamic Client Registration\nbun run example:notion\n```\n\n:::\n\n## Troubleshooting\n\n### Common Issues and Solutions\n\n::: details Port Already in Use\nIf port 3000 is already in use:\n\n```typescript\nconst result = await getAuthCode({\n authorizationUrl: authUrl,\n port: 8080, // Use a different port\n});\n```\n\nAlso update your OAuth app's redirect URI to match.\n:::\n\n::: details Browser Doesn't Open\nIf you're in a headless environment or the browser doesn't open:\n\n```typescript\n// Headless mode - print URL for manual opening\nconsole.log(`Please open: ${authUrl}`);\nconst result = await getAuthCode({ port: 3000, timeout: 120000 });\n```\n\n:::\n\n::: details Firewall Warnings\nOn first run, your OS firewall may show a warning. Allow connections for:\n\n* **localhost** only\n* The specific port you're using (default: 3000)\n :::\n\n::: details Token Refresh Errors\nFor MCP apps with token refresh issues:\n\n```typescript\nimport open from \"open\";\n\nconst authProvider = browserAuth({\n launch: open,\n store: fileStore(), // Use persistent storage\n authTimeout: 300000, // Increase timeout to 5 minutes\n});\n```\n\n:::\n\n## Getting Help\n\nNeed assistance? Here are your options:\n\n* 📝 [GitHub Issues](https://github.com/kriasoft/oauth-callback/issues) - Report bugs or request features\n* 💬 [GitHub Discussions](https://github.com/kriasoft/oauth-callback/discussions) - Ask questions and share ideas\n* 💬 [Discord Community](https://discord.gg/zQQXyqvs5x) - Join our Discord server for real-time help and discussions\n* 📚 [Stack Overflow](https://stackoverflow.com/questions/tagged/oauth-callback) - Search or ask questions with the `oauth-callback` tag\n\nHappy coding! 🚀\n\n---\n\n---\nurl: /oauth-callback/examples/linear.md\ndescription: >-\n Integrate with Linear's Model Context Protocol server using OAuth Callback for\n seamless issue tracking and project management automation.\n---\n\n# Linear MCP Example\n\nThis example demonstrates how to integrate with Linear's Model Context Protocol (MCP) server using OAuth Callback's `browserAuth()` provider. Linear is a modern issue tracking and project management tool designed for high-performance teams. Through MCP integration, you can programmatically manage issues, projects, cycles, and more.\n\n## Overview\n\nThe Linear MCP integration enables powerful project management automation:\n\n* **Issue Management** - Create, update, and track issues programmatically\n* **Project Tracking** - Monitor project progress and milestones\n* **Cycle Management** - Work with sprints and development cycles\n* **Team Collaboration** - Access team data and workflows\n* **Real-time Updates** - Subscribe to changes via MCP resources\n\n## Prerequisites\n\nBefore starting with Linear MCP integration:\n\n* **Runtime Environment** - Bun, Node.js 18+, or Deno installed\n* **Linear Account** - Active Linear workspace with API access\n* **OAuth Application** - Linear OAuth app configured (or use DCR if supported)\n* **Port Availability** - Port 3000 (or custom) for OAuth callback\n* **Browser Access** - Default browser for authorization flow\n\n## Installation\n\nInstall the required dependencies:\n\n::: code-group\n\n```bash [Bun]\nbun add oauth-callback @modelcontextprotocol/sdk\n```\n\n```bash [npm]\nnpm install oauth-callback @modelcontextprotocol/sdk\n```\n\n```bash [pnpm]\npnpm add oauth-callback @modelcontextprotocol/sdk\n```\n\n:::\n\n## Linear OAuth Setup\n\n### Creating a Linear OAuth Application\n\n1. Navigate to [Linear Settings > API](https://linear.app/settings/api)\n2. Click \"Create new OAuth application\"\n3. Configure your application:\n * **Application name**: Your app name\n * **Redirect URI**: `http://localhost:3000/callback`\n * **Scopes**: Select required permissions (read, write, admin)\n4. Save your credentials:\n * Client ID\n * Client Secret\n\n### Required Scopes\n\nSelect scopes based on your needs:\n\n| Scope | Description |\n| ----------------- | ------------------------------------------ |\n| `read` | Read access to issues, projects, and teams |\n| `write` | Create and modify issues and comments |\n| `admin` | Manage team settings and workflows |\n| `issues:create` | Create new issues |\n| `issues:update` | Update existing issues |\n| `comments:create` | Add comments to issues |\n\n## Basic Implementation\n\n### Simple Linear Connection\n\nHere's a basic example connecting to Linear's MCP server:\n\n```typescript\nimport open from \"open\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { StreamableHTTPClientTransport } from \"@modelcontextprotocol/sdk/client/streamableHttp.js\";\nimport { browserAuth, fileStore } from \"oauth-callback/mcp\";\n\nasync function connectToLinear() {\n console.log(\"🚀 Connecting to Linear MCP Server\\n\");\n\n // Linear MCP endpoint (hypothetical - check Linear docs)\n const serverUrl = new URL(\"https://mcp.linear.app\");\n\n // Create OAuth provider with credentials\n const authProvider = browserAuth({\n launch: open,\n clientId: process.env.LINEAR_CLIENT_ID,\n clientSecret: process.env.LINEAR_CLIENT_SECRET,\n scope: \"read write issues:create issues:update\",\n port: 3000,\n store: fileStore(\"~/.mcp/linear-tokens.json\"),\n onRequest(req) {\n console.log(`[OAuth] ${new URL(req.url).pathname}`);\n },\n });\n\n try {\n // Create MCP transport\n const transport = new StreamableHTTPClientTransport(serverUrl, {\n authProvider,\n });\n\n // Initialize MCP client\n const client = new Client(\n { name: \"linear-automation\", version: \"1.0.0\" },\n { capabilities: {} },\n );\n\n // Connect to Linear\n await client.connect(transport);\n console.log(\"✅ Connected to Linear MCP!\");\n\n // List available capabilities\n const tools = await client.listTools();\n console.log(\"\\n📝 Available tools:\", tools);\n\n await client.close();\n } catch (error) {\n console.error(\"❌ Connection failed:\", error);\n }\n}\n\nconnectToLinear();\n```\n\n## OAuth Flow Details\n\n### Authorization Flow Diagram\n\n```mermaid\nsequenceDiagram\n participant App as Your Application\n participant OAuth as OAuth Callback\n participant Browser\n participant Linear as Linear OAuth\n participant MCP as Linear MCP\n\n App->>OAuth: Initialize browserAuth\n App->>MCP: Connect to Linear MCP\n MCP-->>App: 401 Unauthorized\n\n Note over OAuth,Browser: OAuth Authorization\n OAuth->>Browser: Open Linear OAuth URL\n Browser->>Linear: Request authorization\n Linear->>Browser: Show consent screen\n Browser->>Linear: User approves\n Linear->>Browser: Redirect to localhost:3000\n Browser->>OAuth: GET /callback?code=xxx\n OAuth->>App: Capture auth code\n\n Note over App,Linear: Token Exchange\n App->>Linear: POST /oauth/token\n Linear-->>App: Access & refresh tokens\n OAuth->>OAuth: Store tokens\n\n App->>MCP: Reconnect with token\n MCP-->>App: 200 OK + capabilities\n```\n\n### Configuration Options\n\nConfigure the OAuth provider for Linear:\n\n```typescript\nconst authProvider = browserAuth({\n // Browser launch callback\n launch: open,\n\n // OAuth credentials\n clientId: process.env.LINEAR_CLIENT_ID,\n clientSecret: process.env.LINEAR_CLIENT_SECRET,\n\n // Required permissions\n scope: \"read write issues:create issues:update comments:create\",\n\n // Server configuration\n port: 3000,\n hostname: \"localhost\",\n callbackPath: \"/callback\",\n\n // Token storage\n store: fileStore(\"~/.mcp/linear.json\"),\n storeKey: \"linear-production\",\n\n // Timeouts\n authTimeout: 300000, // 5 minutes\n\n // Debugging\n onRequest(req) {\n const url = new URL(req.url);\n console.log(`[${new Date().toISOString()}] ${req.method} ${url.pathname}`);\n },\n});\n```\n\n## Working with Linear MCP\n\n### Issue Management\n\nCreate and manage Linear issues through MCP:\n\n```typescript\n// Create a new issue\nconst newIssue = await client.callTool(\"create_issue\", {\n title: \"Fix authentication bug\",\n description: \"Users unable to login with SSO\",\n teamId: \"ENG\",\n priority: 1, // Urgent\n labelIds: [\"bug\", \"authentication\"],\n assigneeId: \"user_123\",\n});\n\nconsole.log(\"Created issue:\", newIssue.identifier);\n\n// Update issue status\nawait client.callTool(\"update_issue\", {\n issueId: newIssue.id,\n stateId: \"in_progress\",\n});\n\n// Add a comment\nawait client.callTool(\"add_comment\", {\n issueId: newIssue.id,\n body: \"Started investigation - found root cause in SSO handler\",\n});\n\n// Search for issues\nconst searchResults = await client.callTool(\"search_issues\", {\n query: \"authentication bug\",\n teamId: \"ENG\",\n state: [\"todo\", \"in_progress\"],\n limit: 10,\n});\n```\n\n### Project Management\n\nWork with Linear projects and milestones:\n\n```typescript\n// Get project details\nconst project = await client.callTool(\"get_project\", {\n projectId: \"PROJ-123\",\n});\n\n// Update project progress\nawait client.callTool(\"update_project\", {\n projectId: \"PROJ-123\",\n progress: 0.75, // 75% complete\n status: \"on_track\",\n});\n\n// List project issues\nconst projectIssues = await client.callTool(\"list_project_issues\", {\n projectId: \"PROJ-123\",\n includeArchived: false,\n});\n\n// Create milestone\nconst milestone = await client.callTool(\"create_milestone\", {\n name: \"v2.0 Release\",\n targetDate: \"2024-06-01\",\n projectId: \"PROJ-123\",\n});\n```\n\n### Cycle Management\n\nManage development cycles (sprints):\n\n```typescript\n// Get current cycle\nconst currentCycle = await client.callTool(\"get_current_cycle\", {\n teamId: \"ENG\",\n});\n\n// List cycle issues\nconst cycleIssues = await client.callTool(\"list_cycle_issues\", {\n cycleId: currentCycle.id,\n});\n\n// Move issue to next cycle\nawait client.callTool(\"update_issue\", {\n issueId: \"ISS-456\",\n cycleId: currentCycle.nextCycle.id,\n});\n\n// Get cycle analytics\nconst analytics = await client.callTool(\"get_cycle_analytics\", {\n cycleId: currentCycle.id,\n});\n\nconsole.log(\"Cycle completion:\", analytics.completionRate);\nconsole.log(\"Issues completed:\", analytics.completedCount);\n```\n\n### Resource Subscriptions\n\nSubscribe to Linear resources for real-time updates:\n\n```typescript\n// Subscribe to team updates\nawait client.subscribeToResource({\n uri: \"linear://team/ENG\",\n});\n\n// Subscribe to project changes\nawait client.subscribeToResource({\n uri: \"linear://project/PROJ-123\",\n});\n\n// Handle resource updates\nclient.on(\"resource_updated\", (resource) => {\n console.log(\"Resource updated:\", resource.uri);\n\n if (resource.uri.startsWith(\"linear://issue/\")) {\n console.log(\"Issue changed:\", resource.data);\n }\n});\n```\n\n## Advanced Patterns\n\n### Custom Linear Client Class\n\nCreate a reusable Linear MCP client:\n\n```typescript\nimport open from \"open\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { StreamableHTTPClientTransport } from \"@modelcontextprotocol/sdk/client/streamableHttp.js\";\nimport { browserAuth, fileStore } from \"oauth-callback/mcp\";\n\nclass LinearMCPClient {\n private client?: Client;\n private authProvider: any;\n\n constructor(options?: {\n clientId?: string;\n clientSecret?: string;\n storePath?: string;\n }) {\n this.authProvider = browserAuth({\n launch: open,\n clientId: options?.clientId || process.env.LINEAR_CLIENT_ID,\n clientSecret: options?.clientSecret || process.env.LINEAR_CLIENT_SECRET,\n scope: \"read write issues:create issues:update\",\n store: fileStore(options?.storePath || \"~/.mcp/linear.json\"),\n port: 3000,\n authTimeout: 300000,\n });\n }\n\n async connect(): Promise {\n const serverUrl = new URL(\"https://mcp.linear.app\");\n\n const transport = new StreamableHTTPClientTransport(serverUrl, {\n authProvider: this.authProvider,\n });\n\n this.client = new Client(\n { name: \"linear-client\", version: \"1.0.0\" },\n { capabilities: {} },\n );\n\n await this.client.connect(transport);\n console.log(\"✅ Connected to Linear MCP\");\n }\n\n async createIssue(params: {\n title: string;\n description?: string;\n teamId: string;\n priority?: number;\n labels?: string[];\n }): Promise {\n if (!this.client) throw new Error(\"Not connected\");\n\n return await this.client.callTool(\"create_issue\", {\n title: params.title,\n description: params.description,\n teamId: params.teamId,\n priority: params.priority || 3,\n labelIds: params.labels || [],\n });\n }\n\n async searchIssues(query: string, teamId?: string): Promise {\n if (!this.client) throw new Error(\"Not connected\");\n\n return await this.client.callTool(\"search_issues\", {\n query,\n teamId,\n limit: 20,\n });\n }\n\n async updateIssueStatus(issueId: string, status: string): Promise {\n if (!this.client) throw new Error(\"Not connected\");\n\n const states = {\n todo: \"state_todo_id\",\n in_progress: \"state_in_progress_id\",\n done: \"state_done_id\",\n cancelled: \"state_cancelled_id\",\n };\n\n await this.client.callTool(\"update_issue\", {\n issueId,\n stateId: states[status] || status,\n });\n }\n\n async addComment(issueId: string, comment: string): Promise {\n if (!this.client) throw new Error(\"Not connected\");\n\n return await this.client.callTool(\"add_comment\", {\n issueId,\n body: comment,\n });\n }\n\n async disconnect(): Promise {\n if (this.client) {\n await this.client.close();\n this.client = undefined;\n }\n }\n}\n\n// Usage\nconst linear = new LinearMCPClient();\nawait linear.connect();\n\nconst issue = await linear.createIssue({\n title: \"Implement OAuth integration\",\n description: \"Add OAuth support for third-party services\",\n teamId: \"ENG\",\n priority: 2,\n labels: [\"feature\", \"authentication\"],\n});\n\nawait linear.updateIssueStatus(issue.id, \"in_progress\");\nawait linear.addComment(issue.id, \"Started implementation\");\nawait linear.disconnect();\n```\n\n### Automation Workflows\n\nBuild powerful automations with Linear MCP:\n\n```typescript\n// Auto-triage incoming issues\nasync function autoTriageIssues(client: Client) {\n // Get untriaged issues\n const untriaged = await client.callTool(\"search_issues\", {\n query: \"no:assignee no:priority\",\n state: [\"todo\"],\n limit: 50,\n });\n\n for (const issue of untriaged.issues) {\n // Analyze issue content\n const keywords =\n issue.title.toLowerCase() + \" \" + issue.description.toLowerCase();\n\n // Auto-assign based on keywords\n let assignee = null;\n let priority = 3; // Default: Medium\n\n if (keywords.includes(\"crash\") || keywords.includes(\"down\")) {\n priority = 1; // Urgent\n assignee = \"oncall_engineer\";\n } else if (\n keywords.includes(\"security\") ||\n keywords.includes(\"vulnerability\")\n ) {\n priority = 1;\n assignee = \"security_team\";\n } else if (keywords.includes(\"performance\") || keywords.includes(\"slow\")) {\n priority = 2; // High\n assignee = \"performance_team\";\n }\n\n // Update issue\n await client.callTool(\"update_issue\", {\n issueId: issue.id,\n priority,\n assigneeId: assignee,\n labelIds: [\"auto-triaged\"],\n });\n\n console.log(\n `Triaged: ${issue.identifier} -> P${priority} ${assignee || \"unassigned\"}`,\n );\n }\n}\n\n// Sync Linear issues with external systems\nasync function syncWithJira(client: Client, jiraClient: any) {\n // Get recent Linear issues\n const recentIssues = await client.callTool(\"search_issues\", {\n createdAfter: new Date(Date.now() - 86400000).toISOString(), // Last 24h\n limit: 100,\n });\n\n for (const issue of recentIssues.issues) {\n // Check if already synced\n if (issue.metadata?.jiraKey) continue;\n\n // Create in Jira\n const jiraIssue = await jiraClient.createIssue({\n summary: issue.title,\n description: issue.description,\n issueType: \"Task\",\n project: \"PROJ\",\n });\n\n // Update Linear with Jira reference\n await client.callTool(\"update_issue\", {\n issueId: issue.id,\n metadata: {\n jiraKey: jiraIssue.key,\n syncedAt: new Date().toISOString(),\n },\n });\n\n // Add sync comment\n await client.callTool(\"add_comment\", {\n issueId: issue.id,\n body: `🔄 Synced to Jira: [${jiraIssue.key}](https://jira.example.com/browse/${jiraIssue.key})`,\n });\n }\n}\n```\n\n### Batch Operations\n\nEfficiently handle multiple operations:\n\n```typescript\nclass LinearBatchProcessor {\n constructor(private client: Client) {}\n\n async batchCreateIssues(\n issues: Array<{\n title: string;\n description?: string;\n teamId: string;\n }>,\n ): Promise {\n const results = [];\n\n // Process in parallel with concurrency limit\n const batchSize = 5;\n for (let i = 0; i < issues.length; i += batchSize) {\n const batch = issues.slice(i, i + batchSize);\n const promises = batch.map((issue) =>\n this.client.callTool(\"create_issue\", issue),\n );\n\n const batchResults = await Promise.all(promises);\n results.push(...batchResults);\n\n console.log(\n `Created batch ${i / batchSize + 1}: ${batchResults.length} issues`,\n );\n }\n\n return results;\n }\n\n async bulkUpdatePriority(\n issueIds: string[],\n priority: number,\n ): Promise {\n const promises = issueIds.map((id) =>\n this.client.callTool(\"update_issue\", {\n issueId: id,\n priority,\n }),\n );\n\n await Promise.all(promises);\n console.log(`Updated priority for ${issueIds.length} issues`);\n }\n\n async archiveCompletedIssues(\n teamId: string,\n olderThanDays = 30,\n ): Promise {\n const cutoffDate = new Date();\n cutoffDate.setDate(cutoffDate.getDate() - olderThanDays);\n\n const completed = await this.client.callTool(\"search_issues\", {\n teamId,\n state: [\"done\", \"cancelled\"],\n completedBefore: cutoffDate.toISOString(),\n limit: 100,\n });\n\n for (const issue of completed.issues) {\n await this.client.callTool(\"archive_issue\", {\n issueId: issue.id,\n });\n }\n\n return completed.issues.length;\n }\n}\n```\n\n## Error Handling\n\n### Common Error Scenarios\n\nHandle Linear-specific errors gracefully:\n\n```typescript\nasync function robustLinearOperation(\n client: Client,\n operation: () => Promise,\n) {\n const maxRetries = 3;\n let lastError;\n\n for (let attempt = 1; attempt <= maxRetries; attempt++) {\n try {\n return await operation();\n } catch (error: any) {\n lastError = error;\n\n // Handle specific Linear errors\n if (error.message.includes(\"RATE_LIMITED\")) {\n // Rate limit - exponential backoff\n const delay = Math.pow(2, attempt) * 1000;\n console.log(`Rate limited. Waiting ${delay}ms...`);\n await new Promise((resolve) => setTimeout(resolve, delay));\n continue;\n }\n\n if (error.message.includes(\"INVALID_TEAM_ID\")) {\n throw new Error(\n \"Invalid team ID. Check your Linear workspace settings.\",\n );\n }\n\n if (error.message.includes(\"INSUFFICIENT_PERMISSIONS\")) {\n throw new Error(\n \"Missing permissions. Request additional OAuth scopes.\",\n );\n }\n\n if (error.message.includes(\"RESOURCE_NOT_FOUND\")) {\n throw new Error(\"Linear resource not found. It may have been deleted.\");\n }\n\n // Network errors - retry\n if (\n error.message.includes(\"ECONNRESET\") ||\n error.message.includes(\"ETIMEDOUT\")\n ) {\n console.log(`Network error on attempt ${attempt}. Retrying...`);\n continue;\n }\n\n // Unknown error - don't retry\n throw error;\n }\n }\n\n throw lastError;\n}\n\n// Usage\nconst result = await robustLinearOperation(client, async () => {\n return await client.callTool(\"create_issue\", {\n title: \"New feature request\",\n teamId: \"ENG\",\n });\n});\n```\n\n## Security Best Practices\n\n### Token Management\n\nSecure your Linear OAuth tokens:\n\n```typescript\nimport { browserAuth, fileStore } from \"oauth-callback/mcp\";\nimport { createCipheriv, createDecipheriv, randomBytes, scrypt } from \"crypto\";\nimport { promisify } from \"util\";\n\n// Encrypted token storage\nclass EncryptedLinearStore {\n private key: Buffer;\n private store = fileStore(\"~/.mcp/linear-encrypted.json\");\n\n async init(password: string) {\n const salt = randomBytes(16);\n this.key = (await promisify(scrypt)(password, salt, 32)) as Buffer;\n }\n\n async get(key: string): Promise {\n const encrypted = await this.store.get(key);\n if (!encrypted) return null;\n\n // Decrypt tokens\n return this.decrypt(encrypted);\n }\n\n async set(key: string, tokens: any): Promise {\n // Encrypt before storing\n const encrypted = this.encrypt(tokens);\n await this.store.set(key, encrypted);\n }\n\n private encrypt(data: any): string {\n const iv = randomBytes(16);\n const cipher = createCipheriv(\"aes-256-gcm\", this.key, iv);\n // ... encryption logic\n return encrypted;\n }\n\n private decrypt(encrypted: string): any {\n // ... decryption logic\n return decrypted;\n }\n}\n```\n\n### Environment Configuration\n\nUse environment variables for sensitive data:\n\n```bash\n# .env file (never commit!)\nLINEAR_CLIENT_ID=lin_oauth_client_xxx\nLINEAR_CLIENT_SECRET=lin_oauth_secret_xxx\nLINEAR_WORKSPACE_ID=workspace_123\nLINEAR_TEAM_ID=team_eng\n```\n\n```typescript\n// Load configuration\nimport open from \"open\";\nimport { config } from \"dotenv\";\nconfig();\n\nconst authProvider = browserAuth({\n launch: open,\n clientId: process.env.LINEAR_CLIENT_ID!,\n clientSecret: process.env.LINEAR_CLIENT_SECRET!,\n scope: \"read write\",\n store: fileStore(),\n});\n```\n\n## Testing\n\n### Mock Linear MCP Server\n\nTest your integration without hitting Linear's API:\n\n```typescript\nimport { createServer } from \"http\";\n\nclass MockLinearMCPServer {\n private server: any;\n private issues = new Map();\n\n async start(port = 4000) {\n this.server = createServer((req, res) => {\n // Mock MCP endpoints\n if (req.url === \"/capabilities\") {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n tools: [\n { name: \"create_issue\", description: \"Create issue\" },\n { name: \"update_issue\", description: \"Update issue\" },\n { name: \"search_issues\", description: \"Search issues\" },\n ],\n }),\n );\n }\n // ... other endpoints\n });\n\n await new Promise((resolve) => {\n this.server.listen(port, resolve);\n });\n }\n\n async stop() {\n await new Promise((resolve) => this.server.close(resolve));\n }\n}\n\n// Test usage\ndescribe(\"Linear MCP Integration\", () => {\n let mockServer: MockLinearMCPServer;\n\n beforeAll(async () => {\n mockServer = new MockLinearMCPServer();\n await mockServer.start();\n });\n\n afterAll(async () => {\n await mockServer.stop();\n });\n\n it(\"should create an issue\", async () => {\n // Test implementation\n });\n});\n```\n\n## Troubleshooting\n\n### Common Issues\n\n::: details OAuth authorization fails\n\nCheck your Linear OAuth app configuration:\n\n```typescript\n// Verify redirect URI matches\nconst authProvider = browserAuth({\n launch: open,\n clientId: \"...\",\n clientSecret: \"...\",\n port: 3000, // Must match redirect URI port\n callbackPath: \"/callback\", // Must match redirect URI path\n});\n```\n\n:::\n\n::: details Rate limiting errors\n\nImplement rate limit handling:\n\n```typescript\nclass RateLimitedClient {\n private requestCount = 0;\n private resetTime = Date.now() + 60000;\n\n async callTool(name: string, params: any) {\n // Check rate limit\n if (Date.now() > this.resetTime) {\n this.requestCount = 0;\n this.resetTime = Date.now() + 60000;\n }\n\n if (this.requestCount >= 100) {\n const waitTime = this.resetTime - Date.now();\n await new Promise((resolve) => setTimeout(resolve, waitTime));\n this.requestCount = 0;\n }\n\n this.requestCount++;\n return await this.client.callTool(name, params);\n }\n}\n```\n\n:::\n\n::: details Token refresh fails\n\nHandle token refresh errors:\n\n```typescript\nconst authProvider = browserAuth({\n launch: open,\n store: fileStore(),\n onTokenRefreshError: async (error) => {\n console.error(\"Token refresh failed:\", error);\n // Clear invalid tokens\n await authProvider.invalidateCredentials(\"tokens\");\n // Trigger re-authentication\n throw new Error(\"Re-authentication required\");\n },\n});\n```\n\n:::\n\n## Related Resources\n\n* [Linear API Documentation](https://developers.linear.app/docs/graphql/working-with-the-graphql-api) - Official Linear API reference\n* [browserAuth API](/api/browser-auth) - OAuth provider documentation\n* [MCP SDK Documentation](https://modelcontextprotocol.io/docs) - Model Context Protocol reference\n\n---\n\n---\nurl: /oauth-callback/markdown-examples.md\n---\n# Markdown Extension Examples\n\nThis page demonstrates some of the built-in markdown extensions provided by VitePress.\n\n## Syntax Highlighting\n\nVitePress provides Syntax Highlighting powered by [Shiki](https://github.com/shikijs/shiki), with additional features like line-highlighting:\n\n**Input**\n\n````md\n```js{4}\nexport default {\n data () {\n return {\n msg: 'Highlighted!'\n }\n }\n}\n```\n````\n\n**Output**\n\n```js{4}\nexport default {\n data () {\n return {\n msg: 'Highlighted!'\n }\n }\n}\n```\n\n## Custom Containers\n\n**Input**\n\n```md\n::: info\nThis is an info box.\n:::\n\n::: tip\nThis is a tip.\n:::\n\n::: warning\nThis is a warning.\n:::\n\n::: danger\nThis is a dangerous warning.\n:::\n\n::: details\nThis is a details block.\n:::\n```\n\n**Output**\n\n::: info\nThis is an info box.\n:::\n\n::: tip\nThis is a tip.\n:::\n\n::: warning\nThis is a warning.\n:::\n\n::: danger\nThis is a dangerous warning.\n:::\n\n::: details\nThis is a details block.\n:::\n\n## More\n\nCheck out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown).\n\n---\n\n---\nurl: /oauth-callback/examples/notion.md\ndescription: >-\n Connect to Notion's Model Context Protocol server using OAuth Callback with\n Dynamic Client Registration - no pre-configured OAuth app required.\n---\n\n# Notion MCP Example\n\nThis example demonstrates how to connect to Notion's Model Context Protocol (MCP) server using OAuth Callback's `browserAuth()` provider with Dynamic Client Registration (DCR). Unlike traditional OAuth flows that require pre-registering an OAuth application, this example shows how to automatically register and authenticate with Notion's authorization server.\n\n## Overview\n\nThe Notion MCP integration showcases several advanced features:\n\n* **Dynamic Client Registration (RFC 7591)** - Automatic OAuth client registration\n* **Model Context Protocol Integration** - Seamless MCP SDK authentication\n* **Browser-Based Authorization** - Automatic browser opening for user consent\n* **Token Management** - Automatic token storage and retrieval\n* **Zero Configuration** - No client ID or secret required\n\n## Prerequisites\n\nBefore running this example, ensure you have:\n\n* **Bun, Node.js 18+, or Deno** installed\n* **Port 3000** available for the OAuth callback server\n* **Default browser** configured for opening authorization URLs\n* **Internet connection** to reach Notion's servers\n\n## Installation\n\nInstall the required dependencies:\n\n::: code-group\n\n```bash [Bun]\nbun add oauth-callback @modelcontextprotocol/sdk\n```\n\n```bash [npm]\nnpm install oauth-callback @modelcontextprotocol/sdk\n```\n\n```bash [pnpm]\npnpm add oauth-callback @modelcontextprotocol/sdk\n```\n\n:::\n\n## Quick Start\n\n### Running the Example\n\nThe simplest way to run the Notion MCP example:\n\n```bash\n# Clone the repository\ngit clone https://github.com/kriasoft/oauth-callback.git\ncd oauth-callback\n\n# Install dependencies\nbun install\n\n# Run the Notion example\nbun run example:notion\n```\n\nThe example will:\n\n1. Start a local OAuth callback server on port 3000\n2. Open your browser to Notion's authorization page\n3. Capture the authorization code after you approve\n4. Exchange the code for access tokens\n5. Connect to Notion's MCP server\n6. Display available tools and resources\n\n## Complete Example Code\n\nHere's the full implementation demonstrating Notion MCP integration:\n\n```typescript\n#!/usr/bin/env bun\nimport open from \"open\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { StreamableHTTPClientTransport } from \"@modelcontextprotocol/sdk/client/streamableHttp.js\";\nimport { browserAuth, inMemoryStore } from \"oauth-callback/mcp\";\n\nasync function connectToNotion() {\n console.log(\"🚀 Starting OAuth flow with Notion MCP Server\\n\");\n\n const serverUrl = new URL(\"https://mcp.notion.com/mcp\");\n\n // Create OAuth provider - no client_id or client_secret needed!\n const authProvider = browserAuth({\n launch: open,\n port: 3000,\n scope: \"read write\",\n store: inMemoryStore(), // Use fileStore() for persistence\n onRequest(req) {\n const url = new URL(req.url);\n console.log(`📨 OAuth: ${req.method} ${url.pathname}`);\n },\n });\n\n try {\n // Create MCP transport with OAuth provider\n const transport = new StreamableHTTPClientTransport(serverUrl, {\n authProvider,\n });\n\n // Create MCP client\n const client = new Client(\n { name: \"notion-example\", version: \"1.0.0\" },\n { capabilities: {} },\n );\n\n // Connect triggers OAuth flow if needed\n await client.connect(transport);\n console.log(\"✅ Connected to Notion MCP server!\");\n\n // List available tools\n const tools = await client.listTools();\n console.log(\"\\n📝 Available tools:\");\n for (const tool of tools.tools || []) {\n console.log(` - ${tool.name}: ${tool.description}`);\n }\n\n // List available resources\n const resources = await client.listResources();\n console.log(\"\\n📂 Available resources:\");\n for (const resource of resources.resources || []) {\n console.log(` - ${resource.uri}: ${resource.name}`);\n }\n\n await client.close();\n } catch (error) {\n console.error(\"❌ Connection failed:\", error);\n }\n}\n\nconnectToNotion();\n```\n\n## How It Works\n\n### OAuth Flow Sequence\n\n```mermaid\nsequenceDiagram\n participant App as Your App\n participant OAuth as OAuth Callback\n participant Browser\n participant Notion as Notion Auth\n participant MCP as Notion MCP\n\n App->>OAuth: Create browserAuth provider\n App->>MCP: Connect via transport\n MCP-->>App: 401 Unauthorized\n\n Note over App,OAuth: Dynamic Client Registration\n App->>Notion: POST /oauth/register\n Notion-->>App: client_id, client_secret\n OAuth->>OAuth: Store credentials\n\n Note over OAuth,Browser: Authorization Flow\n OAuth->>Browser: Open authorization URL\n Browser->>Notion: User authorizes\n Notion->>Browser: Redirect to localhost:3000\n Browser->>OAuth: GET /callback?code=xxx\n OAuth->>App: Return auth code\n\n Note over App,MCP: Token Exchange\n App->>Notion: POST /oauth/token\n Notion-->>App: access_token, refresh_token\n OAuth->>OAuth: Store tokens\n\n App->>MCP: Connect with token\n MCP-->>App: 200 OK + capabilities\n```\n\n### Dynamic Client Registration\n\nUnlike traditional OAuth, Notion's MCP server supports Dynamic Client Registration (RFC 7591):\n\n1. **No Pre-Registration** - You don't need to manually register an OAuth app\n2. **Automatic Registration** - The client registers itself on first use\n3. **Credential Persistence** - Client credentials are stored for reuse\n4. **Simplified Distribution** - Ship apps without OAuth setup instructions\n\n## Key Features\n\n### Browser-Based Authorization\n\nThe `browserAuth()` provider handles the complete OAuth flow:\n\n```typescript\nimport open from \"open\";\n\nconst authProvider = browserAuth({\n launch: open, // Opens browser for authorization\n port: 3000, // Callback server port\n scope: \"read write\", // Requested permissions\n store: inMemoryStore(), // Token storage\n onRequest(req) {\n // Request logging\n console.log(`OAuth: ${req.url}`);\n },\n});\n```\n\n### Token Storage Options\n\nChoose between ephemeral and persistent storage:\n\n::: code-group\n\n```typescript [Ephemeral Storage]\n// Tokens lost on restart (more secure)\nconst authProvider = browserAuth({\n launch: open,\n store: inMemoryStore(),\n});\n```\n\n```typescript [Persistent Storage]\n// Tokens saved to disk (convenient)\nimport { fileStore } from \"oauth-callback/mcp\";\n\nconst authProvider = browserAuth({\n launch: open,\n store: fileStore(), // Default: ~/.mcp/tokens.json\n});\n```\n\n```typescript [Custom Location]\n// Specify custom file path\nconst authProvider = browserAuth({\n launch: open,\n store: fileStore(\"~/my-app/notion-tokens.json\"),\n});\n```\n\n:::\n\n### Error Handling\n\nProperly handle authentication failures:\n\n```typescript\ntry {\n await client.connect(transport);\n} catch (error) {\n if (error.message.includes(\"Unauthorized\")) {\n console.log(\"Authorization required - check browser\");\n } else if (error.message.includes(\"access_denied\")) {\n console.log(\"User cancelled authorization\");\n } else {\n console.error(\"Connection failed:\", error);\n }\n}\n```\n\n## Working with Notion MCP\n\n### Available Tools\n\nOnce connected, you can use Notion's MCP tools:\n\n```typescript\n// Search for content\nconst searchResults = await client.callTool(\"search_objects\", {\n query: \"meeting notes\",\n limit: 10,\n});\n\n// Create a new page\nconst newPage = await client.callTool(\"create_page\", {\n title: \"My New Page\",\n content: \"Page content here\",\n});\n\n// Update existing content\nconst updated = await client.callTool(\"update_page\", {\n page_id: \"page-123\",\n content: \"Updated content\",\n});\n```\n\n### Available Resources\n\nAccess Notion resources through MCP:\n\n```typescript\n// List all resources\nconst resources = await client.listResources();\n\n// Read a specific resource\nconst pageContent = await client.readResource({\n uri: \"notion://page/page-123\",\n});\n\n// Subscribe to changes\nawait client.subscribeToResource({\n uri: \"notion://database/db-456\",\n});\n```\n\n## Advanced Configuration\n\n### Custom Success Pages\n\nProvide branded callback pages:\n\n```typescript\nconst authProvider = browserAuth({\n successHtml: `\n \n \n \n Notion Connected!\n \n \n \n
\n

✨ Connected to Notion!

\n

You can close this window and return to your app.

\n
\n \n \n `,\n});\n```\n\n### Request Logging\n\nDebug OAuth flow with detailed logging:\n\n```typescript\nimport open from \"open\";\n\nconst authProvider = browserAuth({\n launch: open,\n onRequest(req) {\n const url = new URL(req.url);\n const timestamp = new Date().toISOString();\n\n console.log(`[${timestamp}] OAuth Request`);\n console.log(` Method: ${req.method}`);\n console.log(` Path: ${url.pathname}`);\n\n if (url.pathname === \"/callback\") {\n console.log(` Code: ${url.searchParams.get(\"code\")}`);\n console.log(` State: ${url.searchParams.get(\"state\")}`);\n }\n },\n});\n```\n\n### Multi-Account Support\n\nSupport multiple Notion accounts:\n\n```typescript\nimport open from \"open\";\n\nfunction createNotionAuth(accountName: string) {\n return browserAuth({\n launch: open,\n store: fileStore(`~/.mcp/notion-${accountName}.json`),\n storeKey: `notion-${accountName}`,\n port: 3000 + Math.floor(Math.random() * 1000), // Random port\n });\n}\n\n// Use different accounts\nconst personalAuth = createNotionAuth(\"personal\");\nconst workAuth = createNotionAuth(\"work\");\n```\n\n## Troubleshooting\n\n### Common Issues and Solutions\n\n::: details Browser doesn't open automatically\n\nIf you're in a headless environment:\n\n```typescript\nconst authProvider = browserAuth({\n launch: () => {}, // Noop - disable browser opening\n});\n\n// Manually instruct user\nconsole.log(\"Please open this URL in your browser:\");\nconsole.log(authorizationUrl);\n```\n\n:::\n\n::: details Port 3000 is already in use\n\nUse a different port for the callback server:\n\n```typescript\nconst authProvider = browserAuth({\n port: 8080, // Use alternative port\n});\n```\n\n:::\n\n::: details Tokens not persisting\n\nEnsure you're using file storage, not in-memory:\n\n```typescript\nimport open from \"open\";\nimport { fileStore } from \"oauth-callback/mcp\";\n\nconst authProvider = browserAuth({\n launch: open,\n store: fileStore(), // ✅ Persistent storage\n // store: inMemoryStore() // ❌ Lost on restart\n});\n```\n\n:::\n\n::: details Authorization fails repeatedly\n\nClear stored credentials and try again:\n\n```typescript\n// Clear all stored data\nawait authProvider.invalidateCredentials(\"all\");\n\n// Or clear specific data\nawait authProvider.invalidateCredentials(\"tokens\");\nawait authProvider.invalidateCredentials(\"client\");\n```\n\n:::\n\n## Security Considerations\n\n### Best Practices\n\n1. **Use Ephemeral Storage for Sensitive Data**\n\n ```typescript\n import open from \"open\";\n\n // Tokens are never written to disk\n const authProvider = browserAuth({\n launch: open,\n store: inMemoryStore(),\n });\n ```\n\n2. **Validate State Parameter**\n * The library automatically generates and validates state parameters\n * Prevents CSRF attacks during authorization\n\n3. **PKCE Protection**\n * Enabled by default for enhanced security\n * Prevents authorization code interception\n\n4. **Secure File Permissions**\n * File storage uses mode 0600 (owner read/write only)\n * Tokens are protected from other users on the system\n\n### Token Security\n\n::: warning\nNever commit tokens to version control:\n\n```bash\n# Add to .gitignore\n~/.mcp/\n*.json\ntokens.json\n```\n\n:::\n\n## Complete Working Example\n\nFor a production-ready implementation with full error handling:\n\n```typescript\nimport open from \"open\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { StreamableHTTPClientTransport } from \"@modelcontextprotocol/sdk/client/streamableHttp.js\";\nimport { browserAuth, fileStore } from \"oauth-callback/mcp\";\n\nclass NotionMCPClient {\n private client?: Client;\n private authProvider: any;\n\n constructor() {\n this.authProvider = browserAuth({\n launch: open,\n port: 3000,\n scope: \"read write\",\n store: fileStore(\"~/.mcp/notion.json\"),\n authTimeout: 300000, // 5 minutes\n onRequest: this.logRequest.bind(this),\n });\n }\n\n private logRequest(req: Request) {\n const url = new URL(req.url);\n console.log(`[OAuth] ${req.method} ${url.pathname}`);\n }\n\n async connect(): Promise {\n const serverUrl = new URL(\"https://mcp.notion.com/mcp\");\n\n const transport = new StreamableHTTPClientTransport(serverUrl, {\n authProvider: this.authProvider,\n });\n\n this.client = new Client(\n { name: \"notion-client\", version: \"1.0.0\" },\n { capabilities: {} },\n );\n\n try {\n await this.client.connect(transport);\n console.log(\"✅ Connected to Notion MCP\");\n } catch (error: any) {\n if (error.message.includes(\"Unauthorized\")) {\n throw new Error(\"Authorization required. Please check your browser.\");\n }\n throw error;\n }\n }\n\n async search(query: string): Promise {\n if (!this.client) throw new Error(\"Not connected\");\n\n return await this.client.callTool(\"search_objects\", {\n query,\n limit: 10,\n });\n }\n\n async disconnect(): Promise {\n if (this.client) {\n await this.client.close();\n this.client = undefined;\n }\n }\n}\n\n// Usage\nasync function main() {\n const notion = new NotionMCPClient();\n\n try {\n await notion.connect();\n\n const results = await notion.search(\"project roadmap\");\n console.log(\"Search results:\", results);\n\n await notion.disconnect();\n } catch (error) {\n console.error(\"Error:\", error);\n }\n}\n\nmain();\n```\n\n## Related Resources\n\n* [browserAuth API Documentation](/api/browser-auth) - Complete API reference\n* [Storage Providers](/api/storage-providers) - Token storage options\n* [Core Concepts](/core-concepts) - OAuth and MCP architecture\n* [GitHub Example](https://github.com/kriasoft/oauth-callback/blob/main/examples/notion.ts) - Source code\n\n---\n\n---\nurl: /oauth-callback/api/oauth-error.md\ndescription: >-\n OAuth-specific error class for handling authorization failures and provider\n errors according to RFC 6749.\n---\n\n# OAuthError\n\nThe `OAuthError` class represents OAuth-specific errors that occur during the authorization flow. It extends the standard JavaScript `Error` class and provides structured access to OAuth error details as defined in [RFC 6749 Section 4.1.2.1](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1).\n\n## Class Definition\n\n```typescript\nclass OAuthError extends Error {\n error: string; // OAuth error code\n error_description?: string; // Human-readable description\n error_uri?: string; // URI with more information\n\n constructor(error: string, description?: string, uri?: string);\n}\n```\n\n## Properties\n\n| Property | Type | Description |\n| ------------------- | --------------------- | ------------------------------------------------ |\n| `name` | `string` | Always `\"OAuthError\"` for instanceof checks |\n| `error` | `string` | OAuth error code (e.g., `\"access_denied\"`) |\n| `error_description` | `string \\| undefined` | Human-readable error description |\n| `error_uri` | `string \\| undefined` | URI with additional error information |\n| `message` | `string` | Inherited from Error (description or error code) |\n| `stack` | `string` | Inherited stack trace from Error |\n\n## OAuth Error Codes\n\nAccording to [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1) and common provider extensions:\n\n### Standard OAuth 2.0 Error Codes\n\n| Error Code | Description | User Action Required |\n| --------------------------- | --------------------------------------------------- | --------------------------- |\n| `invalid_request` | Request is missing required parameters or malformed | Fix request parameters |\n| `unauthorized_client` | Client not authorized for this grant type | Check client configuration |\n| `access_denied` | User denied the authorization request | User must approve access |\n| `unsupported_response_type` | Response type not supported | Use supported response type |\n| `invalid_scope` | Requested scope is invalid or unknown | Request valid scopes |\n| `server_error` | Authorization server error (500) | Retry later |\n| `temporarily_unavailable` | Server temporarily overloaded (503) | Retry with backoff |\n\n### Common Provider Extensions\n\n| Error Code | Provider | Description |\n| ---------------------------- | ---------------- | ------------------------------ |\n| `consent_required` | Microsoft | User consent needed |\n| `interaction_required` | Microsoft | User interaction needed |\n| `login_required` | Google/Microsoft | User must authenticate |\n| `account_selection_required` | Microsoft | User must select account |\n| `invalid_client` | Various | Client authentication failed |\n| `invalid_grant` | Various | Grant or refresh token invalid |\n\n## Basic Usage\n\n### Catching OAuth Errors\n\n```typescript\nimport { getAuthCode, OAuthError } from \"oauth-callback\";\n\ntry {\n const result = await getAuthCode(authorizationUrl);\n console.log(\"Success! Code:\", result.code);\n} catch (error) {\n if (error instanceof OAuthError) {\n console.error(`OAuth error: ${error.error}`);\n if (error.error_description) {\n console.error(`Details: ${error.error_description}`);\n }\n if (error.error_uri) {\n console.error(`More info: ${error.error_uri}`);\n }\n } else {\n // Handle other errors (timeout, network, etc.)\n console.error(\"Unexpected error:\", error);\n }\n}\n```\n\n### Type Guard\n\n```typescript\nfunction isOAuthError(error: unknown): error is OAuthError {\n return error instanceof OAuthError;\n}\n\n// Usage\ncatch (error) {\n if (isOAuthError(error)) {\n // TypeScript knows error is OAuthError\n handleOAuthError(error.error, error.error_description);\n }\n}\n```\n\n## Error Handling Patterns\n\n### Comprehensive Error Handler\n\n```typescript\nimport { OAuthError } from \"oauth-callback\";\n\nasync function handleOAuthFlow(authUrl: string) {\n try {\n const result = await getAuthCode(authUrl);\n return result.code;\n } catch (error) {\n if (error instanceof OAuthError) {\n switch (error.error) {\n case \"access_denied\":\n // User cancelled - this is expected\n console.log(\"User cancelled authorization\");\n return null;\n\n case \"invalid_scope\":\n throw new Error(\n `Invalid permissions requested: ${error.error_description}`,\n );\n\n case \"server_error\":\n case \"temporarily_unavailable\":\n // Retry with exponential backoff\n console.warn(\"Server error, retrying...\");\n await delay(1000);\n return handleOAuthFlow(authUrl);\n\n case \"unauthorized_client\":\n throw new Error(\n \"Application not authorized. Please check OAuth app settings.\",\n );\n\n default:\n // Unknown OAuth error\n throw new Error(\n `OAuth authorization failed: ${error.error_description || error.error}`,\n );\n }\n }\n\n // Non-OAuth errors\n throw error;\n }\n}\n```\n\n### User-Friendly Error Messages\n\n```typescript\nfunction getErrorMessage(error: OAuthError): string {\n const messages: Record = {\n access_denied: \"You cancelled the authorization. Please try again when ready.\",\n invalid_scope: \"The requested permissions are not available.\",\n server_error: \"The authorization server encountered an error. Please try again.\",\n temporarily_unavailable: \"The service is temporarily unavailable. Please try again later.\",\n unauthorized_client: \"This application is not authorized. Please contact support.\",\n invalid_request: \"The authorization request was invalid. Please try again.\",\n consent_required: \"Please provide consent to continue.\",\n login_required: \"Please log in to continue.\",\n interaction_required: \"Additional interaction is required. Please complete the authorization in your browser.\"\n };\n\n return messages[error.error] ||\n error.error_description ||\n `Authorization failed: ${error.error}`;\n}\n\n// Usage\ncatch (error) {\n if (error instanceof OAuthError) {\n const userMessage = getErrorMessage(error);\n showUserNotification(userMessage);\n }\n}\n```\n\n### Retry Logic\n\n```typescript\nasync function authorizeWithRetry(\n authUrl: string,\n maxAttempts = 3,\n): Promise {\n let lastError: Error | undefined;\n\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n const result = await getAuthCode(authUrl);\n return result.code;\n } catch (error) {\n lastError = error as Error;\n\n if (error instanceof OAuthError) {\n // Don't retry user-actionable errors\n if (\n [\"access_denied\", \"invalid_scope\", \"unauthorized_client\"].includes(\n error.error,\n )\n ) {\n throw error;\n }\n\n // Retry server errors\n if ([\"server_error\", \"temporarily_unavailable\"].includes(error.error)) {\n const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);\n console.warn(`Attempt ${attempt} failed, retrying in ${delay}ms...`);\n await new Promise((resolve) => setTimeout(resolve, delay));\n continue;\n }\n }\n\n // Don't retry other errors\n throw error;\n }\n }\n\n throw lastError || new Error(\"Authorization failed after retries\");\n}\n```\n\n## Error Recovery Strategies\n\n### Graceful Degradation\n\n```typescript\nclass OAuthService {\n private cachedToken?: string;\n\n async getAccessToken(): Promise {\n try {\n // Try to get fresh token\n const code = await getAuthCode(this.authUrl);\n const token = await this.exchangeCodeForToken(code);\n this.cachedToken = token;\n return token;\n } catch (error) {\n if (error instanceof OAuthError) {\n if (error.error === \"access_denied\") {\n // User cancelled - try using cached token if available\n if (this.cachedToken) {\n console.log(\"Using cached token after user cancellation\");\n return this.cachedToken;\n }\n return null;\n }\n\n if (error.error === \"temporarily_unavailable\" && this.cachedToken) {\n // Service down - use cached token\n console.warn(\"OAuth service unavailable, using cached token\");\n return this.cachedToken;\n }\n }\n\n throw error;\n }\n }\n}\n```\n\n### Error Logging\n\n```typescript\nimport { OAuthError } from \"oauth-callback\";\n\nfunction logOAuthError(error: OAuthError, context: Record) {\n const errorLog = {\n timestamp: new Date().toISOString(),\n type: \"oauth_error\",\n error_code: error.error,\n error_description: error.error_description,\n error_uri: error.error_uri,\n context,\n stack: error.stack\n };\n\n // Send to logging service\n if ([\"server_error\", \"temporarily_unavailable\"].includes(error.error)) {\n console.error(\"OAuth provider error:\", errorLog);\n // Report to monitoring service\n reportToMonitoring(errorLog);\n } else {\n console.warn(\"OAuth user error:\", errorLog);\n // Track user analytics\n trackUserEvent(\"oauth_error\", { code: error.error });\n }\n}\n\n// Usage\ncatch (error) {\n if (error instanceof OAuthError) {\n logOAuthError(error, {\n provider: \"github\",\n client_id: CLIENT_ID,\n scopes: [\"user:email\", \"repo\"]\n });\n }\n}\n```\n\n## Testing OAuth Errors\n\n### Unit Testing\n\n```typescript\nimport { OAuthError } from \"oauth-callback\";\nimport { describe, it, expect } from \"vitest\";\n\ndescribe(\"OAuthError\", () => {\n it(\"should create error with all properties\", () => {\n const error = new OAuthError(\n \"invalid_scope\",\n \"The requested scope is invalid\",\n \"https://example.com/docs/scopes\",\n );\n\n expect(error).toBeInstanceOf(Error);\n expect(error).toBeInstanceOf(OAuthError);\n expect(error.name).toBe(\"OAuthError\");\n expect(error.error).toBe(\"invalid_scope\");\n expect(error.error_description).toBe(\"The requested scope is invalid\");\n expect(error.error_uri).toBe(\"https://example.com/docs/scopes\");\n expect(error.message).toBe(\"The requested scope is invalid\");\n });\n\n it(\"should use error code as message when description is missing\", () => {\n const error = new OAuthError(\"access_denied\");\n expect(error.message).toBe(\"access_denied\");\n });\n});\n```\n\n### Mock OAuth Errors\n\n```typescript\nimport { OAuthError } from \"oauth-callback\";\n\nclass MockOAuthProvider {\n private shouldFail: string | null = null;\n\n simulateError(errorCode: string) {\n this.shouldFail = errorCode;\n }\n\n async authorize(): Promise {\n if (this.shouldFail) {\n throw new OAuthError(\n this.shouldFail,\n this.getErrorDescription(this.shouldFail),\n \"https://example.com/oauth/errors\",\n );\n }\n return \"mock_auth_code\";\n }\n\n private getErrorDescription(code: string): string {\n const descriptions: Record = {\n access_denied: \"User denied access to the application\",\n invalid_scope: \"One or more scopes are invalid\",\n server_error: \"Authorization server encountered an error\",\n };\n return descriptions[code] || \"Unknown error\";\n }\n}\n\n// Usage in tests\ndescribe(\"OAuth Flow\", () => {\n it(\"should handle access_denied error\", async () => {\n const provider = new MockOAuthProvider();\n provider.simulateError(\"access_denied\");\n\n await expect(provider.authorize()).rejects.toThrow(OAuthError);\n await expect(provider.authorize()).rejects.toThrow(\"User denied access\");\n });\n});\n```\n\n## Integration with MCP\n\nWhen using with the MCP SDK through `browserAuth()`:\n\n```typescript\nimport { browserAuth } from \"oauth-callback/mcp\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\n\nasync function connectWithErrorHandling() {\n const authProvider = browserAuth({\n store: fileStore(),\n onRequest: (req) => {\n // Log OAuth flow for debugging\n console.log(`OAuth: ${req.url}`);\n },\n });\n\n const client = new Client(\n { name: \"my-app\", version: \"1.0.0\" },\n { capabilities: {} },\n );\n\n try {\n await client.connect(transport);\n } catch (error) {\n // OAuth errors from browserAuth are wrapped\n if (error.message?.includes(\"OAuthError\")) {\n // Extract OAuth error details\n const match = error.message.match(/OAuthError: (\\w+)/);\n if (match) {\n const errorCode = match[1];\n console.error(`OAuth failed with code: ${errorCode}`);\n\n if (errorCode === \"access_denied\") {\n console.log(\"User cancelled MCP authorization\");\n return null;\n }\n }\n }\n throw error;\n }\n}\n```\n\n## Error Flow Diagram\n\n```mermaid\nflowchart TD\n Start([User initiates OAuth]) --> Auth[Open authorization URL]\n Auth --> Decision{User action}\n\n Decision -->|Approves| Success[Return code]\n Decision -->|Denies| Denied[OAuthError: access_denied]\n Decision -->|Invalid scope| Scope[OAuthError: invalid_scope]\n Decision -->|Timeout| Timeout[TimeoutError]\n\n Auth --> ServerCheck{Server status}\n ServerCheck -->|Error 500| ServerErr[OAuthError: server_error]\n ServerCheck -->|Error 503| Unavailable[OAuthError: temporarily_unavailable]\n ServerCheck -->|Invalid client| Unauthorized[OAuthError: unauthorized_client]\n\n Denied --> Handle[Error Handler]\n Scope --> Handle\n ServerErr --> Retry{Retry?}\n Unavailable --> Retry\n Unauthorized --> Handle\n Timeout --> Handle\n\n Retry -->|Yes| Auth\n Retry -->|No| Handle\n\n Handle --> UserMessage[Show user message]\n Success --> Complete([Complete])\n\n style Denied fill:#f96\n style Scope fill:#f96\n style ServerErr fill:#fa6\n style Unavailable fill:#fa6\n style Unauthorized fill:#f96\n style Timeout fill:#f96\n style Success fill:#6f9\n```\n\n## Best Practices\n\n### 1. Always Check Error Type\n\n```typescript\ncatch (error) {\n if (error instanceof OAuthError) {\n // Handle OAuth-specific errors\n } else if (error.message === \"Timeout waiting for callback\") {\n // Handle timeout\n } else {\n // Handle unexpected errors\n }\n}\n```\n\n### 2. Log Errors Appropriately\n\n```typescript\nif (error instanceof OAuthError) {\n if (error.error === \"access_denied\") {\n // User action - info level\n console.info(\"User cancelled OAuth flow\");\n } else if (\n [\"server_error\", \"temporarily_unavailable\"].includes(error.error)\n ) {\n // Provider issue - error level\n console.error(\"OAuth provider error:\", error);\n } else {\n // Configuration issue - warning level\n console.warn(\"OAuth configuration error:\", error);\n }\n}\n```\n\n### 3. Provide Clear User Feedback\n\n```typescript\nfunction getUserMessage(error: OAuthError): string {\n // Prefer provider's description if available\n if (error.error_description) {\n return error.error_description;\n }\n\n // Fall back to generic messages\n return userFriendlyMessages[error.error] || \"Authorization failed\";\n}\n```\n\n### 4. Handle Errors at the Right Level\n\n```typescript\n// Low level - preserve error details\nasync function getOAuthCode(url: string) {\n const result = await getAuthCode(url);\n // Let OAuthError propagate up\n return result.code;\n}\n\n// High level - translate to user actions\nasync function authenticateUser() {\n try {\n const code = await getOAuthCode(authUrl);\n return { success: true, code };\n } catch (error) {\n if (error instanceof OAuthError && error.error === \"access_denied\") {\n return { success: false, cancelled: true };\n }\n return { success: false, error: error.message };\n }\n}\n```\n\n## Related APIs\n\n* [`getAuthCode`](/api/get-auth-code) - Main function that throws OAuthError\n* [`browserAuth`](/api/browser-auth) - MCP provider that handles OAuth errors\n* [`TimeoutError`](#timeouterror) - Related timeout error class\n\n## TimeoutError\n\nThe library also exports a `TimeoutError` class for timeout-specific failures:\n\n```typescript\nclass TimeoutError extends Error {\n name: \"TimeoutError\";\n constructor(message?: string);\n}\n```\n\nUsage:\n\n```typescript\ncatch (error) {\n if (error instanceof TimeoutError) {\n console.error(\"OAuth flow timed out\");\n // Suggest user tries again\n }\n}\n```\n\n---\n\n---\nurl: /oauth-callback/api-examples.md\n---\n\n# Runtime API Examples\n\nThis page demonstrates usage of some of the runtime APIs provided by VitePress.\n\nThe main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files:\n\n```md\n\n\n## Results\n\n### Theme Data\n\n
{{ theme }}
\n\n### Page Data\n\n
{{ page }}
\n\n### Page Frontmatter\n\n
{{ frontmatter }}
\n```\n\n## Results\n\n### Theme Data\n\n### Page Data\n\n### Page Frontmatter\n\n## More\n\nCheck out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata).\n\n---\n\n---\nurl: /oauth-callback/api/storage-providers.md\ndescription: >-\n Token storage interfaces and implementations for persisting OAuth tokens,\n client credentials, and session state.\n---\n\n# Storage Providers\n\nStorage providers in OAuth Callback manage the persistence of OAuth tokens, client credentials, and session state. The library provides flexible storage interfaces with built-in implementations for both ephemeral and persistent storage, plus the ability to create custom storage backends.\n\n## Storage Interfaces\n\nOAuth Callback defines two storage interfaces for different levels of OAuth state management:\n\n### TokenStore Interface\n\nThe basic `TokenStore` interface handles OAuth token persistence:\n\n```typescript\ninterface TokenStore {\n get(key: string): Promise;\n set(key: string, tokens: Tokens): Promise;\n delete(key: string): Promise;\n}\n```\n\n#### Tokens Type\n\n```typescript\ninterface Tokens {\n accessToken: string; // OAuth access token\n refreshToken?: string; // Optional refresh token\n expiresAt?: number; // Absolute expiry time (Unix ms)\n scope?: string; // Space-delimited granted scopes\n}\n```\n\n### OAuthStore Interface\n\nThe extended `OAuthStore` interface adds support for Dynamic Client Registration and PKCE verifier persistence:\n\n```typescript\nimport { OAuthStoreBrand } from \"oauth-callback/mcp\";\n\ninterface OAuthStore extends TokenStore {\n readonly [OAuthStoreBrand]: true; // Required brand for type detection\n\n getClient(key: string): Promise;\n setClient(key: string, client: ClientInfo): Promise;\n deleteClient(key: string): Promise;\n\n getCodeVerifier(key: string): Promise;\n setCodeVerifier(key: string, verifier: string): Promise;\n deleteCodeVerifier(key: string): Promise;\n}\n```\n\n#### ClientInfo Type\n\n```typescript\ninterface ClientInfo {\n clientId: string; // OAuth client ID\n clientSecret?: string; // OAuth client secret\n clientIdIssuedAt?: number; // When client was registered\n clientSecretExpiresAt?: number; // When secret expires\n}\n```\n\n## Built-in Storage Providers\n\n### inMemoryStore()\n\nEphemeral storage that keeps tokens in memory. Tokens are lost when the process exits.\n\n```typescript\nfunction inMemoryStore(): TokenStore;\n```\n\n#### Usage\n\n```typescript\nimport open from \"open\";\nimport { browserAuth, inMemoryStore } from \"oauth-callback/mcp\";\n\nconst authProvider = browserAuth({\n launch: open,\n store: inMemoryStore(),\n});\n```\n\n#### Characteristics\n\n| Feature | Description |\n| --------------- | ------------------------------------------ |\n| **Persistence** | None - data lost on process exit |\n| **Concurrency** | Thread-safe within process |\n| **Security** | Maximum - no disk persistence |\n| **Performance** | Fastest - no I/O operations |\n| **Use Cases** | Development, testing, short-lived sessions |\n\n#### Implementation Details\n\n```typescript\n// Internal implementation uses a Map\nconst store = new Map();\n\n// All operations are synchronous but return Promises\n// for interface consistency\n```\n\n### fileStore()\n\nPersistent storage that saves tokens to a JSON file on disk.\n\n```typescript\nfunction fileStore(filepath?: string): TokenStore;\n```\n\n#### Parameters\n\n| Parameter | Type | Default | Description |\n| ---------- | -------- | -------------------- | ---------------------------------- |\n| `filepath` | `string` | `~/.mcp/tokens.json` | Custom file path for token storage |\n\n#### Usage\n\n```typescript\nimport open from \"open\";\nimport { browserAuth, fileStore } from \"oauth-callback/mcp\";\n\n// Use default location (~/.mcp/tokens.json)\nconst authProvider = browserAuth({\n launch: open,\n store: fileStore(),\n});\n\n// Use custom location\nconst customAuth = browserAuth({\n launch: open,\n store: fileStore(\"/path/to/my-tokens.json\"),\n});\n\n// Environment-specific storage\nconst envAuth = browserAuth({\n launch: open,\n store: fileStore(`~/.myapp/${process.env.NODE_ENV}-tokens.json`),\n});\n```\n\n#### Characteristics\n\n| Feature | Description |\n| --------------- | ----------------------------------- |\n| **Persistence** | Survives process restarts |\n| **Concurrency** | ⚠️ Not safe across processes |\n| **Security** | File permissions (mode 0600) |\n| **Performance** | File I/O on each operation |\n| **Use Cases** | Desktop apps, long-running services |\n\n#### File Format\n\nThe file store saves tokens in JSON format:\n\n```json\n{\n \"mcp-tokens\": {\n \"accessToken\": \"eyJhbGciOiJSUzI1NiIs...\",\n \"refreshToken\": \"refresh_token_here\",\n \"expiresAt\": 1735689600000,\n \"scope\": \"read write\"\n },\n \"app-specific-key\": {\n \"accessToken\": \"another_token\",\n \"expiresAt\": 1735693200000\n }\n}\n```\n\n::: warning Concurrent Access\nThe file store is not safe for concurrent access across multiple processes. If you need multi-process support, implement a custom storage provider with proper locking mechanisms.\n:::\n\n## Storage Key Management\n\nStorage keys namespace tokens for different applications or environments:\n\n### Single Application\n\n```typescript\nimport open from \"open\";\n\nconst authProvider = browserAuth({\n launch: open,\n store: fileStore(),\n storeKey: \"my-app\", // Default: \"mcp-tokens\"\n});\n```\n\n### Multiple Applications\n\n```typescript\nimport open from \"open\";\n\n// App 1\nconst app1Auth = browserAuth({\n launch: open,\n store: fileStore(),\n storeKey: \"app1-tokens\",\n});\n\n// App 2 (same file, different key)\nconst app2Auth = browserAuth({\n launch: open,\n store: fileStore(),\n storeKey: \"app2-tokens\",\n});\n```\n\n### Environment Separation\n\n```typescript\nimport open from \"open\";\n\nconst authProvider = browserAuth({\n launch: open,\n store: fileStore(),\n storeKey: `${process.env.APP_NAME}-${process.env.NODE_ENV}`,\n});\n// Results in keys like: \"myapp-dev\", \"myapp-staging\", \"myapp-prod\"\n```\n\n## Custom Storage Implementations\n\nCreate custom storage providers by implementing the `TokenStore` or `OAuthStore` interface:\n\n### Basic Custom Storage\n\n#### Redis Storage Example\n\n```typescript\nimport { TokenStore, Tokens } from \"oauth-callback/mcp\";\nimport Redis from \"ioredis\";\n\nclass RedisTokenStore implements TokenStore {\n private redis: Redis;\n private prefix: string;\n\n constructor(redis: Redis, prefix = \"oauth:\") {\n this.redis = redis;\n this.prefix = prefix;\n }\n\n async get(key: string): Promise {\n const data = await this.redis.get(this.prefix + key);\n return data ? JSON.parse(data) : null;\n }\n\n async set(key: string, tokens: Tokens): Promise {\n const ttl = tokens.expiresAt\n ? Math.floor((tokens.expiresAt - Date.now()) / 1000)\n : undefined;\n\n if (ttl && ttl > 0) {\n await this.redis.setex(this.prefix + key, ttl, JSON.stringify(tokens));\n } else {\n await this.redis.set(this.prefix + key, JSON.stringify(tokens));\n }\n }\n\n async delete(key: string): Promise {\n await this.redis.del(this.prefix + key);\n }\n}\n\n// Usage\nimport open from \"open\";\nconst redis = new Redis();\nconst authProvider = browserAuth({\n launch: open,\n store: new RedisTokenStore(redis),\n});\n```\n\n#### SQLite Storage Example\n\n```typescript\nimport { TokenStore, Tokens } from \"oauth-callback/mcp\";\nimport Database from \"better-sqlite3\";\n\nclass SQLiteTokenStore implements TokenStore {\n private db: Database.Database;\n\n constructor(dbPath = \"./tokens.db\") {\n this.db = new Database(dbPath);\n this.init();\n }\n\n private init() {\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS tokens (\n key TEXT PRIMARY KEY,\n access_token TEXT NOT NULL,\n refresh_token TEXT,\n expires_at INTEGER,\n scope TEXT,\n updated_at INTEGER DEFAULT (unixepoch())\n )\n `);\n }\n\n async get(key: string): Promise {\n const row = this.db.prepare(\"SELECT * FROM tokens WHERE key = ?\").get(key);\n\n if (!row) return null;\n\n return {\n accessToken: row.access_token,\n refreshToken: row.refresh_token,\n expiresAt: row.expires_at,\n scope: row.scope,\n };\n }\n\n async set(key: string, tokens: Tokens): Promise {\n this.db\n .prepare(\n `\n INSERT OR REPLACE INTO tokens \n (key, access_token, refresh_token, expires_at, scope)\n VALUES (?, ?, ?, ?, ?)\n `,\n )\n .run(\n key,\n tokens.accessToken,\n tokens.refreshToken,\n tokens.expiresAt,\n tokens.scope,\n );\n }\n\n async delete(key: string): Promise {\n this.db.prepare(\"DELETE FROM tokens WHERE key = ?\").run(key);\n }\n}\n\n// Usage\nimport open from \"open\";\nconst authProvider = browserAuth({\n launch: open,\n store: new SQLiteTokenStore(\"./oauth-tokens.db\"),\n});\n```\n\n### Advanced Custom Storage\n\n#### Full OAuthStore Implementation\n\n```typescript\nimport {\n OAuthStoreBrand,\n type OAuthStore,\n type Tokens,\n type ClientInfo,\n} from \"oauth-callback/mcp\";\nimport { MongoClient, Db } from \"mongodb\";\n\nclass MongoOAuthStore implements OAuthStore {\n readonly [OAuthStoreBrand] = true as const;\n private db: Db;\n\n constructor(db: Db) {\n this.db = db;\n }\n\n // TokenStore methods\n async get(key: string): Promise {\n const doc = await this.db.collection(\"tokens\").findOne({ _id: key });\n return doc\n ? {\n accessToken: doc.accessToken,\n refreshToken: doc.refreshToken,\n expiresAt: doc.expiresAt,\n scope: doc.scope,\n }\n : null;\n }\n\n async set(key: string, tokens: Tokens): Promise {\n await this.db\n .collection(\"tokens\")\n .replaceOne({ _id: key }, { _id: key, ...tokens }, { upsert: true });\n }\n\n async delete(key: string): Promise {\n await this.db.collection(\"tokens\").deleteOne({ _id: key });\n }\n\n // Client registration methods\n async getClient(key: string): Promise {\n const doc = await this.db.collection(\"clients\").findOne({ _id: key });\n return doc\n ? {\n clientId: doc.clientId,\n clientSecret: doc.clientSecret,\n clientIdIssuedAt: doc.clientIdIssuedAt,\n clientSecretExpiresAt: doc.clientSecretExpiresAt,\n }\n : null;\n }\n\n async setClient(key: string, client: ClientInfo): Promise {\n await this.db\n .collection(\"clients\")\n .replaceOne({ _id: key }, { _id: key, ...client }, { upsert: true });\n }\n\n async deleteClient(key: string): Promise {\n await this.db.collection(\"clients\").deleteOne({ _id: key });\n }\n\n // PKCE verifier methods\n async getCodeVerifier(key: string): Promise {\n const doc = await this.db.collection(\"verifiers\").findOne({ _id: key });\n return doc?.verifier ?? null;\n }\n\n async setCodeVerifier(key: string, verifier: string): Promise {\n await this.db\n .collection(\"verifiers\")\n .replaceOne({ _id: key }, { _id: key, verifier }, { upsert: true });\n }\n\n async deleteCodeVerifier(key: string): Promise {\n await this.db.collection(\"verifiers\").deleteOne({ _id: key });\n }\n}\n\n// Usage\nimport open from \"open\";\nconst client = new MongoClient(\"mongodb://localhost:27017\");\nawait client.connect();\nconst db = client.db(\"oauth\");\n\nconst authProvider = browserAuth({\n launch: open,\n store: new MongoOAuthStore(db),\n});\n```\n\n## Storage Security\n\n### Encryption at Rest\n\nImplement encryption for sensitive token storage:\n\n```typescript\nimport { TokenStore, Tokens } from \"oauth-callback/mcp\";\nimport { createCipheriv, createDecipheriv, randomBytes, scrypt } from \"crypto\";\nimport { promisify } from \"util\";\n\nclass EncryptedTokenStore implements TokenStore {\n private store: TokenStore;\n private key: Buffer;\n\n constructor(store: TokenStore, password: string) {\n this.store = store;\n }\n\n async init(password: string) {\n // Derive key from password\n const salt = randomBytes(16);\n this.key = (await promisify(scrypt)(password, salt, 32)) as Buffer;\n }\n\n private encrypt(data: string): string {\n const iv = randomBytes(16);\n const cipher = createCipheriv(\"aes-256-gcm\", this.key, iv);\n\n let encrypted = cipher.update(data, \"utf8\", \"hex\");\n encrypted += cipher.final(\"hex\");\n\n const authTag = cipher.getAuthTag();\n\n return JSON.stringify({\n iv: iv.toString(\"hex\"),\n authTag: authTag.toString(\"hex\"),\n encrypted,\n });\n }\n\n private decrypt(encryptedData: string): string {\n const { iv, authTag, encrypted } = JSON.parse(encryptedData);\n\n const decipher = createDecipheriv(\n \"aes-256-gcm\",\n this.key,\n Buffer.from(iv, \"hex\"),\n );\n\n decipher.setAuthTag(Buffer.from(authTag, \"hex\"));\n\n let decrypted = decipher.update(encrypted, \"hex\", \"utf8\");\n decrypted += decipher.final(\"utf8\");\n\n return decrypted;\n }\n\n async get(key: string): Promise {\n const encrypted = await this.store.get(key);\n if (!encrypted) return null;\n\n try {\n const decrypted = this.decrypt(JSON.stringify(encrypted));\n return JSON.parse(decrypted);\n } catch {\n return null; // Decryption failed\n }\n }\n\n async set(key: string, tokens: Tokens): Promise {\n const encrypted = this.encrypt(JSON.stringify(tokens));\n await this.store.set(key, JSON.parse(encrypted));\n }\n\n async delete(key: string): Promise {\n await this.store.delete(key);\n }\n}\n\n// Usage\nimport open from \"open\";\nconst encryptedStore = new EncryptedTokenStore(\n fileStore(),\n process.env.ENCRYPTION_PASSWORD!,\n);\nawait encryptedStore.init(process.env.ENCRYPTION_PASSWORD!);\n\nconst authProvider = browserAuth({\n launch: open,\n store: encryptedStore,\n});\n```\n\n### Secure File Permissions\n\nWhen using file storage, ensure proper permissions:\n\n```typescript\nimport { chmod } from \"fs/promises\";\n\nclass SecureFileStore implements TokenStore {\n private store: TokenStore;\n private filepath: string;\n\n constructor(filepath: string) {\n this.filepath = filepath;\n this.store = fileStore(filepath);\n }\n\n async set(key: string, tokens: Tokens): Promise {\n await this.store.set(key, tokens);\n // Ensure file is only readable by owner\n await chmod(this.filepath, 0o600);\n }\n\n // ... other methods delegate to store\n}\n```\n\n## Storage Patterns\n\n### Multi-Tenant Storage\n\nSupport multiple tenants with isolated storage:\n\n```typescript\nclass TenantAwareStore implements TokenStore {\n private stores = new Map();\n\n getStore(tenantId: string): TokenStore {\n if (!this.stores.has(tenantId)) {\n this.stores.set(tenantId, fileStore(`~/.oauth/${tenantId}/tokens.json`));\n }\n return this.stores.get(tenantId)!;\n }\n\n async get(key: string): Promise {\n const [tenantId, tokenKey] = key.split(\":\");\n return this.getStore(tenantId).get(tokenKey);\n }\n\n async set(key: string, tokens: Tokens): Promise {\n const [tenantId, tokenKey] = key.split(\":\");\n return this.getStore(tenantId).set(tokenKey, tokens);\n }\n\n // ... other methods\n}\n\n// Usage\nimport open from \"open\";\nconst authProvider = browserAuth({\n launch: open,\n store: new TenantAwareStore(),\n storeKey: `${tenantId}:${appName}`,\n});\n```\n\n### Cached Storage\n\nAdd caching layer for performance:\n\n```typescript\nclass CachedTokenStore implements TokenStore {\n private cache = new Map();\n private store: TokenStore;\n private ttl: number;\n\n constructor(store: TokenStore, ttlSeconds = 300) {\n this.store = store;\n this.ttl = ttlSeconds * 1000;\n }\n\n async get(key: string): Promise {\n // Check cache first\n const cached = this.cache.get(key);\n if (cached && Date.now() < cached.expires) {\n return cached.tokens;\n }\n\n // Load from store\n const tokens = await this.store.get(key);\n if (tokens) {\n this.cache.set(key, {\n tokens,\n expires: Date.now() + this.ttl,\n });\n }\n\n return tokens;\n }\n\n async set(key: string, tokens: Tokens): Promise {\n // Update both cache and store\n this.cache.set(key, {\n tokens,\n expires: Date.now() + this.ttl,\n });\n await this.store.set(key, tokens);\n }\n\n async delete(key: string): Promise {\n this.cache.delete(key);\n await this.store.delete(key);\n }\n}\n\n// Usage\nimport open from \"open\";\nconst authProvider = browserAuth({\n launch: open,\n store: new CachedTokenStore(fileStore(), 600), // 10 min cache\n});\n```\n\n## Testing Storage Providers\n\n### Mock Storage for Tests\n\n```typescript\nimport { TokenStore, Tokens } from \"oauth-callback/mcp\";\n\nclass MockTokenStore implements TokenStore {\n private data = new Map();\n public getCalls: string[] = [];\n public setCalls: Array<{ key: string; tokens: Tokens }> = [];\n\n async get(key: string): Promise {\n this.getCalls.push(key);\n return this.data.get(key) ?? null;\n }\n\n async set(key: string, tokens: Tokens): Promise {\n this.setCalls.push({ key, tokens });\n this.data.set(key, tokens);\n }\n\n async delete(key: string): Promise {\n this.data.delete(key);\n }\n\n // Test helper methods\n reset() {\n this.data.clear();\n this.getCalls = [];\n this.setCalls = [];\n }\n\n setTestData(key: string, tokens: Tokens) {\n this.data.set(key, tokens);\n }\n}\n\n// Usage in tests\ndescribe(\"OAuth Flow\", () => {\n let mockStore: MockTokenStore;\n\n beforeEach(() => {\n mockStore = new MockTokenStore();\n mockStore.setTestData(\"test-key\", {\n accessToken: \"test-token\",\n expiresAt: Date.now() + 3600000,\n });\n });\n\n it(\"should use stored tokens\", async () => {\n const authProvider = browserAuth({\n launch: () => {}, // Noop for tests\n store: mockStore,\n storeKey: \"test-key\",\n });\n\n // Test your OAuth flow\n expect(mockStore.getCalls).toContain(\"test-key\");\n });\n});\n```\n\n## Migration Strategies\n\n### Migrating Storage Backends\n\n```typescript\nasync function migrateStorage(\n source: TokenStore,\n target: TokenStore,\n keys?: string[],\n) {\n // If no keys specified, migrate all (if source supports listing)\n const keysToMigrate = keys || [\"mcp-tokens\"]; // Default key\n\n for (const key of keysToMigrate) {\n const tokens = await source.get(key);\n if (tokens) {\n await target.set(key, tokens);\n console.log(`Migrated tokens for key: ${key}`);\n }\n }\n\n console.log(\"Migration complete\");\n}\n\n// Example: Migrate from file to Redis\nconst fileStorage = fileStore();\nconst redisStorage = new RedisTokenStore(redis);\n\nawait migrateStorage(fileStorage, redisStorage);\n```\n\n### Upgrading Token Format\n\n```typescript\nclass LegacyAdapter implements TokenStore {\n private store: TokenStore;\n\n constructor(store: TokenStore) {\n this.store = store;\n }\n\n async get(key: string): Promise {\n const data = await this.store.get(key);\n if (!data) return null;\n\n // Handle legacy format\n if (\"access_token\" in (data as any)) {\n return {\n accessToken: (data as any).access_token,\n refreshToken: (data as any).refresh_token,\n expiresAt: (data as any).expires_at,\n scope: (data as any).scope,\n };\n }\n\n return data;\n }\n\n // ... other methods\n}\n```\n\n## Best Practices\n\n### Choosing a Storage Provider\n\n| Scenario | Recommended Storage | Reason |\n| ---------------------- | --------------------- | --------------------- |\n| Development/Testing | `inMemoryStore()` | No persistence needed |\n| CLI Tools (single-use) | `inMemoryStore()` | Security, simplicity |\n| Desktop Apps | `fileStore()` | User convenience |\n| Server Applications | Custom (Redis/DB) | Scalability, sharing |\n| High Security | `inMemoryStore()` | No disk persistence |\n| Multi-tenant | Custom implementation | Isolation required |\n\n### Storage Key Conventions\n\n```typescript\n// Use hierarchical keys for organization\nconst key = `${organization}:${application}:${environment}`;\n\n// Examples:\n(\"acme:billing-app:production\");\n(\"acme:billing-app:staging\");\n(\"personal:cli-tool:default\");\n```\n\n### Error Handling\n\nAlways handle storage failures gracefully:\n\n```typescript\nclass ResilientTokenStore implements TokenStore {\n private primary: TokenStore;\n private fallback: TokenStore;\n\n constructor(primary: TokenStore, fallback: TokenStore) {\n this.primary = primary;\n this.fallback = fallback;\n }\n\n async get(key: string): Promise {\n try {\n return await this.primary.get(key);\n } catch (error) {\n console.warn(\"Primary storage failed, using fallback:\", error);\n return await this.fallback.get(key);\n }\n }\n\n async set(key: string, tokens: Tokens): Promise {\n try {\n await this.primary.set(key, tokens);\n // Also update fallback for consistency\n await this.fallback.set(key, tokens).catch(() => {});\n } catch (error) {\n console.warn(\"Primary storage failed, using fallback:\", error);\n await this.fallback.set(key, tokens);\n }\n }\n\n // ... other methods\n}\n\n// Usage: Redis with file fallback\nimport open from \"open\";\nconst authProvider = browserAuth({\n launch: open,\n store: new ResilientTokenStore(new RedisTokenStore(redis), fileStore()),\n});\n```\n\n## Related APIs\n\n* [`browserAuth`](/api/browser-auth) - OAuth provider using storage\n* [`TokenStore` Types](/api/types#tokenstore) - TypeScript interfaces\n* [`OAuthError`](/api/oauth-error) - Error handling\n\n---\n\n---\nurl: /oauth-callback/api/types.md\ndescription: >-\n Complete reference for all TypeScript types, interfaces, and type definitions\n in the oauth-callback library.\n---\n\n# TypeScript Types\n\nOAuth Callback is fully typed with TypeScript, providing comprehensive type safety and excellent IDE support. This page documents all exported types and interfaces available in the library.\n\n## Type Organization\n\n```mermaid\nflowchart TB\n subgraph \"Core Types\"\n GetAuthCodeOptions\n CallbackResult\n ServerOptions\n CallbackServer\n end\n\n subgraph \"Error Types\"\n OAuthError\n TimeoutError\n end\n\n subgraph \"Storage Types\"\n TokenStore\n OAuthStore\n Tokens\n ClientInfo\n end\n\n subgraph \"MCP Types\"\n BrowserAuthOptions\n OAuthClientProvider[\"OAuthClientProvider (MCP SDK)\"]\n end\n\n GetAuthCodeOptions --> CallbackResult\n ServerOptions --> CallbackServer\n BrowserAuthOptions --> TokenStore\n BrowserAuthOptions --> OAuthStore\n OAuthStore --> Tokens\n OAuthStore --> ClientInfo\n```\n\n## Core Types\n\n### GetAuthCodeOptions\n\nConfiguration options for the `getAuthCode` function.\n\n```typescript\ninterface GetAuthCodeOptions {\n authorizationUrl: string; // OAuth authorization URL\n port?: number; // Server port (default: 3000)\n hostname?: string; // Hostname (default: \"localhost\")\n callbackPath?: string; // Callback path (default: \"/callback\")\n timeout?: number; // Timeout in ms (default: 30000)\n launch?: (url: string) => unknown; // Optional URL launcher\n successHtml?: string; // Custom success HTML\n errorHtml?: string; // Custom error HTML template\n signal?: AbortSignal; // Cancellation signal\n onRequest?: (req: Request) => void; // Request callback\n}\n```\n\n#### Usage Example\n\n```typescript\nimport type { GetAuthCodeOptions } from \"oauth-callback\";\n\nconst options: GetAuthCodeOptions = {\n authorizationUrl: \"https://oauth.example.com/authorize?...\",\n port: 8080,\n timeout: 60000,\n successHtml: \"

Success!

\",\n errorHtml: \"

Error: {{error_description}}

\",\n onRequest: (req) => console.log(`Request: ${req.url}`),\n};\n\nconst result = await getAuthCode(options);\n```\n\n### CallbackResult\n\nResult object returned from OAuth callback containing authorization code or error details.\n\n```typescript\ninterface CallbackResult {\n code?: string; // Authorization code\n state?: string; // State parameter for CSRF\n error?: string; // OAuth error code\n error_description?: string; // Error description\n error_uri?: string; // Error info URI\n [key: string]: string | undefined; // Additional params\n}\n```\n\n#### Usage Example\n\n```typescript\nimport type { CallbackResult } from \"oauth-callback\";\n\nfunction handleCallback(result: CallbackResult) {\n if (result.error) {\n console.error(`OAuth error: ${result.error}`);\n if (result.error_description) {\n console.error(`Details: ${result.error_description}`);\n }\n return;\n }\n\n if (result.code) {\n console.log(`Authorization code: ${result.code}`);\n\n // Validate state for CSRF protection\n if (result.state !== expectedState) {\n throw new Error(\"State mismatch - possible CSRF attack\");\n }\n\n // Exchange code for tokens\n exchangeCodeForTokens(result.code);\n }\n}\n```\n\n### ServerOptions\n\nConfiguration options for the OAuth callback server.\n\n```typescript\ninterface ServerOptions {\n port: number; // Port to bind to\n hostname?: string; // Hostname (default: \"localhost\")\n successHtml?: string; // Custom success HTML\n errorHtml?: string; // Error HTML template\n signal?: AbortSignal; // Cancellation signal\n onRequest?: (req: Request) => void; // Request callback\n}\n```\n\n#### Usage Example\n\n```typescript\nimport type { ServerOptions } from \"oauth-callback\";\n\nconst serverOptions: ServerOptions = {\n port: 3000,\n hostname: \"127.0.0.1\",\n successHtml: `\n \n \n

Authorization successful!

\n \n \n \n `,\n onRequest: (req) => {\n const url = new URL(req.url);\n console.log(`[${req.method}] ${url.pathname}`);\n },\n};\n```\n\n### CallbackServer\n\nInterface for OAuth callback server implementations across different runtimes.\n\n```typescript\ninterface CallbackServer {\n start(options: ServerOptions): Promise;\n waitForCallback(path: string, timeout: number): Promise;\n stop(): Promise;\n}\n```\n\n#### Implementation Example\n\n```typescript\nimport type {\n CallbackServer,\n ServerOptions,\n CallbackResult,\n} from \"oauth-callback\";\n\nclass CustomCallbackServer implements CallbackServer {\n private server?: HttpServer;\n\n async start(options: ServerOptions): Promise {\n // Start HTTP server\n this.server = await createServer(options);\n }\n\n async waitForCallback(\n path: string,\n timeout: number,\n ): Promise {\n // Wait for OAuth callback\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n reject(new Error(\"Timeout waiting for callback\"));\n }, timeout);\n\n this.server.on(\"request\", (req) => {\n if (req.url.startsWith(path)) {\n clearTimeout(timer);\n const params = parseQueryParams(req.url);\n resolve(params);\n }\n });\n });\n }\n\n async stop(): Promise {\n // Stop server\n await this.server?.close();\n }\n}\n```\n\n## Storage Types\n\n### TokenStore\n\nBasic interface for OAuth token storage.\n\n```typescript\ninterface TokenStore {\n get(key: string): Promise;\n set(key: string, tokens: Tokens): Promise;\n delete(key: string): Promise;\n}\n```\n\n### Tokens\n\nOAuth token data structure.\n\n```typescript\ninterface Tokens {\n accessToken: string; // OAuth access token\n refreshToken?: string; // Optional refresh token\n expiresAt?: number; // Absolute expiry (Unix ms)\n scope?: string; // Space-delimited scopes\n}\n```\n\n#### Usage Example\n\n```typescript\nimport type { Tokens, TokenStore } from \"oauth-callback/mcp\";\n\nclass CustomTokenStore implements TokenStore {\n private storage = new Map();\n\n async get(key: string): Promise {\n return this.storage.get(key) ?? null;\n }\n\n async set(key: string, tokens: Tokens): Promise {\n // Check if token is expired\n if (tokens.expiresAt && Date.now() >= tokens.expiresAt) {\n console.warn(\"Storing expired token\");\n }\n this.storage.set(key, tokens);\n }\n\n async delete(key: string): Promise {\n this.storage.delete(key);\n }\n}\n```\n\n### OAuthStore\n\nExtended storage interface with Dynamic Client Registration and PKCE verifier persistence.\n\n```typescript\nimport { OAuthStoreBrand } from \"oauth-callback/mcp\";\n\ninterface OAuthStore extends TokenStore {\n readonly [OAuthStoreBrand]: true; // Required brand for type detection\n\n getClient(key: string): Promise;\n setClient(key: string, client: ClientInfo): Promise;\n deleteClient(key: string): Promise;\n\n getCodeVerifier(key: string): Promise;\n setCodeVerifier(key: string, verifier: string): Promise;\n deleteCodeVerifier(key: string): Promise;\n}\n```\n\n### ClientInfo\n\nDynamic client registration data.\n\n```typescript\ninterface ClientInfo {\n clientId: string; // OAuth client ID\n clientSecret?: string; // Client secret\n clientIdIssuedAt?: number; // Registration time\n clientSecretExpiresAt?: number; // Secret expiry\n}\n```\n\n#### Complete Storage Example\n\n```typescript\nimport {\n OAuthStoreBrand,\n type OAuthStore,\n type Tokens,\n type ClientInfo,\n} from \"oauth-callback/mcp\";\n\nclass DatabaseOAuthStore implements OAuthStore {\n readonly [OAuthStoreBrand] = true as const;\n\n constructor(private db: Database) {}\n\n // TokenStore methods\n async get(key: string): Promise {\n return await this.db.tokens.findOne({ key });\n }\n\n async set(key: string, tokens: Tokens): Promise {\n await this.db.tokens.upsert({ key }, tokens);\n }\n\n async delete(key: string): Promise {\n await this.db.tokens.delete({ key });\n }\n\n // Client registration methods\n async getClient(key: string): Promise {\n return await this.db.clients.findOne({ key });\n }\n\n async setClient(key: string, client: ClientInfo): Promise {\n await this.db.clients.upsert({ key }, client);\n }\n\n async deleteClient(key: string): Promise {\n await this.db.clients.delete({ key });\n }\n\n // PKCE verifier methods\n async getCodeVerifier(key: string): Promise {\n const doc = await this.db.verifiers.findOne({ key });\n return doc?.verifier ?? null;\n }\n\n async setCodeVerifier(key: string, verifier: string): Promise {\n await this.db.verifiers.upsert({ key }, { verifier });\n }\n\n async deleteCodeVerifier(key: string): Promise {\n await this.db.verifiers.delete({ key });\n }\n}\n```\n\n## MCP Types\n\n### BrowserAuthOptions\n\nConfiguration for browser-based OAuth flows with MCP servers.\n\n```typescript\ninterface BrowserAuthOptions {\n // OAuth credentials\n clientId?: string; // Pre-registered client ID\n clientSecret?: string; // Pre-registered secret\n scope?: string; // OAuth scopes\n\n // Server configuration\n port?: number; // Callback port (default: 3000)\n hostname?: string; // Hostname (default: \"localhost\")\n callbackPath?: string; // Path (default: \"/callback\")\n\n // Storage\n store?: TokenStore; // Token storage\n storeKey?: string; // Storage key for token isolation\n\n // Behavior\n launch?: (url: string) => unknown; // URL launcher\n authTimeout?: number; // Timeout ms (default: 300000)\n\n // UI\n successHtml?: string; // Success page HTML\n errorHtml?: string; // Error page template\n\n // Debugging\n onRequest?: (req: Request) => void; // Request logger\n}\n```\n\n#### Usage Example\n\n```typescript\nimport type { BrowserAuthOptions } from \"oauth-callback/mcp\";\nimport { browserAuth, fileStore } from \"oauth-callback/mcp\";\n\nconst options: BrowserAuthOptions = {\n // Dynamic Client Registration - no credentials needed\n scope: \"read write\",\n\n // Custom server configuration\n port: 8080,\n hostname: \"127.0.0.1\",\n\n // Persistent storage\n store: fileStore(\"~/.myapp/tokens.json\"),\n storeKey: \"production\",\n\n // Timeout\n authTimeout: 600000, // 10 minutes\n\n // Custom UI\n successHtml: \"

Success!

\",\n\n // Debugging\n onRequest: (req) => {\n console.log(`[OAuth] ${new URL(req.url).pathname}`);\n },\n};\n\nconst authProvider = browserAuth(options);\n```\n\n## Error Types\n\n### OAuthError\n\nOAuth-specific error class.\n\n```typescript\nclass OAuthError extends Error {\n name: \"OAuthError\";\n error: string; // OAuth error code\n error_description?: string; // Human-readable description\n error_uri?: string; // Info URI\n\n constructor(error: string, description?: string, uri?: string);\n}\n```\n\n### TimeoutError\n\nTimeout-specific error class.\n\n```typescript\nclass TimeoutError extends Error {\n name: \"TimeoutError\";\n constructor(message?: string);\n}\n```\n\n#### Error Handling Example\n\n```typescript\nimport { OAuthError, TimeoutError } from \"oauth-callback\";\nimport type { CallbackResult } from \"oauth-callback\";\n\nfunction handleAuthResult(result: CallbackResult) {\n // Check for OAuth errors in result\n if (result.error) {\n throw new OAuthError(\n result.error,\n result.error_description,\n result.error_uri,\n );\n }\n\n if (!result.code) {\n throw new Error(\"No authorization code received\");\n }\n\n return result.code;\n}\n\n// Usage with proper error handling\ntry {\n const code = await getAuthCode(authUrl);\n} catch (error) {\n if (error instanceof OAuthError) {\n console.error(`OAuth error: ${error.error}`);\n } else if (error instanceof TimeoutError) {\n console.error(\"Authorization timed out\");\n } else {\n console.error(\"Unexpected error:\", error);\n }\n}\n```\n\n## Type Guards\n\nUseful type guard functions for runtime type checking:\n\n```typescript\nimport type { Tokens, ClientInfo } from \"oauth-callback/mcp\";\n\n// Check if object is Tokens\nfunction isTokens(obj: unknown): obj is Tokens {\n return (\n typeof obj === \"object\" &&\n obj !== null &&\n \"accessToken\" in obj &&\n typeof (obj as any).accessToken === \"string\"\n );\n}\n\n// Check if object is ClientInfo\nfunction isClientInfo(obj: unknown): obj is ClientInfo {\n return (\n typeof obj === \"object\" &&\n obj !== null &&\n \"clientId\" in obj &&\n typeof (obj as any).clientId === \"string\"\n );\n}\n\n// Check if error is OAuthError\nfunction isOAuthError(error: unknown): error is OAuthError {\n return error instanceof OAuthError;\n}\n\n// Usage\nconst stored = await store.get(\"key\");\nif (stored && isTokens(stored)) {\n console.log(\"Valid tokens:\", stored.accessToken);\n}\n```\n\n## Generic Type Patterns\n\n### Result Type Pattern\n\n```typescript\ntype Result =\n | { success: true; data: T }\n | { success: false; error: E };\n\nasync function safeGetAuthCode(\n url: string,\n): Promise> {\n try {\n const result = await getAuthCode(url);\n return { success: true, data: result };\n } catch (error) {\n if (error instanceof OAuthError) {\n return { success: false, error };\n }\n return { success: false, error: error as Error };\n }\n}\n\n// Usage\nconst result = await safeGetAuthCode(authUrl);\nif (result.success) {\n console.log(\"Code:\", result.data.code);\n} else {\n console.error(\"Error:\", result.error.message);\n}\n```\n\n### Storage Adapter Pattern\n\n```typescript\ntype StorageAdapter = {\n load(): Promise;\n save(data: T): Promise;\n remove(): Promise;\n};\n\nfunction createStorageAdapter(\n store: TokenStore,\n key: string,\n): StorageAdapter {\n return {\n async load() {\n const data = await store.get(key);\n return data as T | null;\n },\n async save(data: T) {\n await store.set(key, data as any);\n },\n async remove() {\n await store.delete(key);\n },\n };\n}\n```\n\n## Type Exports\n\n### Main Package Exports\n\n```typescript\n// From \"oauth-callback\"\nexport type {\n GetAuthCodeOptions,\n CallbackResult,\n CallbackServer,\n ServerOptions,\n};\n\nexport { getAuthCode, OAuthError, inMemoryStore, fileStore };\n```\n\n### MCP Sub-Package Exports\n\n```typescript\n// From \"oauth-callback/mcp\"\nexport type { BrowserAuthOptions, TokenStore, OAuthStore, Tokens, ClientInfo };\n\nexport { browserAuth, inMemoryStore, fileStore };\n```\n\n### Namespace Export\n\n```typescript\n// Also available via namespace\nimport { mcp } from \"oauth-callback\";\n\n// All MCP types and functions under mcp namespace\nconst authProvider = mcp.browserAuth({\n store: mcp.fileStore(),\n});\n```\n\n## TypeScript Configuration\n\nFor optimal type support, use these TypeScript settings:\n\n```json\n{\n \"compilerOptions\": {\n \"target\": \"ES2020\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"strict\": true,\n \"esModuleInterop\": true,\n \"skipLibCheck\": true,\n \"forceConsistentCasingInFileNames\": true,\n \"types\": [\"node\", \"bun-types\"]\n }\n}\n```\n\n## Type Versioning\n\nThe library follows semantic versioning for types:\n\n* **Major version**: Breaking type changes\n* **Minor version**: New types or optional properties\n* **Patch version**: Type fixes that don't break compatibility\n\n## Related Documentation\n\n* [`getAuthCode`](/api/get-auth-code) - Main function using these types\n* [`browserAuth`](/api/browser-auth) - MCP provider using these types\n* [`Storage Providers`](/api/storage-providers) - Storage type implementations\n* [`OAuthError`](/api/oauth-error) - Error type documentation\n\n---\n\n---\nurl: /oauth-callback/what-is-oauth-callback.md\ndescription: >-\n Learn how OAuth Callback simplifies OAuth 2.0 authorization code capture for\n CLI tools, desktop apps, and MCP clients using localhost callbacks.\n---\n\n# What is OAuth Callback? {#top}\n\nAn OAuth callback is the mechanism that allows OAuth 2.0 authorization servers to return authorization codes to your application after user authentication. For native applications like CLI tools, desktop apps, and MCP clients, receiving this callback requires spinning up a temporary HTTP server on localhost — a process that **OAuth Callback** makes trivially simple with just one function call.\n\n**OAuth Callback** is designed for developers building CLI tools, desktop applications, automation scripts, and Model Context Protocol (MCP) clients that need to capture OAuth authorization codes via a localhost callback. Whether you're automating workflows across services (Notion, Linear, GitHub), building developer tools, or creating MCP-enabled applications, **OAuth Callback** handles the complexity of the loopback redirect flow recommended by [RFC 8252](https://www.rfc-editor.org/rfc/rfc8252.html) while providing modern features like Dynamic Client Registration and flexible token storage — all with support for Node.js 18+, Deno, and Bun.\n\n## Understanding OAuth Callbacks\n\n### What is a Callback URL in OAuth 2.0?\n\nIn the OAuth 2.0 authorization code flow, the callback URL (also called redirect URI) is where the authorization server sends the user's browser after they approve or deny your application's access request. This URL receives critical information:\n\n* **On success**: An authorization `code` parameter that your app exchanges for access tokens\n* **On failure**: An `error` parameter describing what went wrong\n* **Security parameters**: The `state` value for CSRF protection\n\nFor web applications, this callback is typically a route on your server. But for native applications without a public web server, you need a different approach.\n\n### The Loopback Redirect Pattern\n\nNative applications (CLIs, desktop apps) can't expose public URLs for callbacks. Instead, they use the **loopback interface** — a temporary HTTP server on `http://localhost` or `http://127.0.0.1`. This pattern, standardized in [RFC 8252](https://www.rfc-editor.org/rfc/rfc8252.html) (OAuth 2.0 for Native Apps), provides several benefits:\n\n* **No public exposure**: The callback server only accepts local connections\n* **Dynamic ports**: Apps can use any available port (e.g., 3000, 8080)\n* **Automatic cleanup**: The server shuts down immediately after receiving the callback\n* **Universal support**: Works across all platforms without special permissions\n\nHere's how the flow works:\n\n```mermaid\nsequenceDiagram\n participant App as CLI/Desktop App\n participant Browser as User's Browser\n participant Auth as Authorization Server\n participant Local as localhost:3000\n\n App->>Local: Start HTTP server\n App->>Browser: Open authorization URL\n Browser->>Auth: Request authorization\n Auth->>Browser: Show consent screen\n Browser->>Auth: User approves\n Auth->>Browser: Redirect to localhost:3000/callback?code=xyz\n Browser->>Local: GET /callback?code=xyz\n Local->>App: Capture code\n App->>Local: Shutdown server\n Local->>Browser: Show success page\n```\n\n### Security Best Practices\n\nOAuth 2.0 for native apps requires additional security measures:\n\n**Proof Key for Code Exchange (PKCE)** - [RFC 7636](https://www.rfc-editor.org/rfc/rfc7636.html) mandates using PKCE for public clients (those without client secrets). PKCE prevents authorization code interception attacks by binding the code exchange to a cryptographic challenge.\n\n```mermaid\nsequenceDiagram\n participant App as Native App\n participant Browser\n participant Auth as Auth Server\n\n Note over App: Generate code_verifier\n Note over App: Hash to create code_challenge\n\n App->>Browser: Open /authorize?code_challenge=...\n Browser->>Auth: Authorization request with challenge\n Auth->>Auth: Store challenge\n Auth->>Browser: Redirect with code\n Browser->>App: Return code\n\n App->>Auth: POST /token with code + verifier\n Auth->>Auth: Verify: hash(verifier) == challenge\n Auth->>App: Return access token\n```\n\n**State Parameter** - A random value that prevents CSRF attacks. Your app generates this value, includes it in the authorization request, and validates it in the callback.\n\n**Dynamic Client Registration (DCR)** - [RFC 7591](https://www.rfc-editor.org/rfc/rfc7591.html) allows apps to register OAuth clients on-the-fly without pre-configuration. This is particularly useful for MCP servers where users shouldn't need to manually register OAuth applications.\n\n## How OAuth Callback Solves It\n\nOAuth Callback eliminates the boilerplate of implementing loopback redirects. Instead of manually managing servers, ports, and browser launches, you get a complete solution in one function call.\n\n### Core Functionality\n\nThe library handles the entire OAuth callback flow:\n\n1. **Starts a localhost HTTP server** on your specified port\n2. **Opens the user's browser** to the authorization URL\n3. **Captures the callback** with the authorization code\n4. **Returns the result** as a clean JavaScript object\n5. **Shuts down the server** automatically\n\n### Zero Configuration Example\n\nHere's a complete OAuth flow in just 6 lines:\n\n```typescript {3-5}\nimport { getAuthCode } from \"oauth-callback\";\n\nconst result = await getAuthCode(\n \"https://github.com/login/oauth/authorize?client_id=xxx&redirect_uri=http://localhost:3000/callback\",\n);\n\nconsole.log(\"Authorization code:\", result.code);\n```\n\nThat's it. No server setup, no browser management, no cleanup code.\n\n### Cross-Runtime Support\n\nOAuth Callback uses modern Web Standards APIs (Request, Response, URL) that work identically across:\n\n* **Node.js 18+** - Native fetch and Web Streams support\n* **Deno** - First-class Web Standards implementation\n* **Bun** - High-performance runtime with built-in APIs\n\nThis means your OAuth code is portable across runtimes without modifications.\n\n### MCP Integration\n\nFor Model Context Protocol applications, OAuth Callback provides the `browserAuth()` provider that integrates seamlessly with the MCP SDK:\n\n::: code-group\n\n```typescript [MCP with OAuth Callback]\nimport { browserAuth, fileStore } from \"oauth-callback/mcp\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { StreamableHTTPClientTransport } from \"@modelcontextprotocol/sdk/client/streamableHttp.js\";\n\nconst authProvider = browserAuth({\n store: fileStore(), // Persist tokens to ~/.mcp/tokens.json\n scope: \"read write\",\n});\n\n// Use directly with MCP transports\nconst transport = new StreamableHTTPClientTransport(\n new URL(\"https://mcp.example.com\"),\n { authProvider },\n);\n```\n\n```typescript [Namespace Import]\nimport { mcp } from \"oauth-callback\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\n\nconst authProvider = mcp.browserAuth({\n store: mcp.fileStore(),\n scope: \"read write\",\n});\n```\n\n:::\n\n::: tip MCP Integration Features\nThe MCP integration handles:\n\n* **Dynamic Client Registration** when supported by the server\n* **Token persistence** with `fileStore()` or ephemeral `inMemoryStore()`\n* **Automatic re-authentication** when tokens expire\n* **Multiple app namespace support** via `storeKey` option\n :::\n\n## When to Use OAuth Callback\n\n### Perfect For\n\nOAuth Callback is ideal when:\n\n* **You control the user's machine** - CLI tools, desktop apps, development tools\n* **You can open a browser** - The user has a default browser configured\n* **You need quick setup** - No server infrastructure or complex configuration\n* **You're building MCP clients** - Direct integration with Model Context Protocol SDK\n\n### Consider Alternatives When\n\n**Device Authorization Flow** ([RFC 8628](https://www.rfc-editor.org/rfc/rfc8628.html)) might be better if:\n\n* **No browser access** - SSH sessions, headless servers, CI/CD environments\n* **Remote terminals** - The auth happens on a different device than the app\n* **Input-constrained devices** - Smart TVs, IoT devices without keyboards\n\nThe Device Flow shows a code to the user that they enter on another device, eliminating the need for a browser on the same machine. However, it requires OAuth provider support and a more complex user experience.\n\n```mermaid\nflowchart TD\n subgraph \"OAuth Callback Flow\"\n A1[CLI App] -->|Opens browser| B1[Auth Page]\n B1 -->|User authorizes| C1[localhost:3000/callback]\n C1 -->|Returns code| A1\n A1 -->|Exchange code| D1[Access Token]\n end\n\n subgraph \"Device Authorization Flow\"\n A2[CLI App] -->|Request device code| B2[Auth Server]\n B2 -->|Returns code + URL| A2\n A2 -->|Display to user| C2[\"User Code: ABCD-1234\"]\n C2 -->|User enters on phone/laptop| D2[Auth Page]\n D2 -->|User authorizes| E2[Auth Server]\n A2 -->|Poll for token| E2\n E2 -->|Returns token| F2[Access Token]\n end\n```\n\n## Security Considerations\n\nOAuth Callback implements security best practices by default:\n\n::: info Security Features\n\n* **Localhost-only binding** - The callback server only accepts connections from `127.0.0.1` or `::1`, preventing remote access attempts.\n* **Automatic cleanup** - The HTTP server shuts down immediately after receiving the callback, minimizing the attack surface window.\n* **No persistent state** - Server is ephemeral and leaves no traces after completion.\n\n:::\n\n::: warning Always Validate State\nAlways validate the `state` parameter returned in the callback matches what you sent:\n\n```typescript {4-7}\nconst state = crypto.randomUUID();\nconst authUrl = `https://example.com/authorize?state=${state}&...`;\n\nconst result = await getAuthCode(authUrl);\nif (result.state !== state) {\n throw new Error(\"State mismatch - possible CSRF attack\");\n}\n```\n\n:::\n\n::: details Proof Key for Code Exchange (PKCE) Implementation\nFor public clients, implement Proof Key for Code Exchange as required by [RFC 7636](https://www.rfc-editor.org/rfc/rfc7636.html):\n\n```typescript {3-4,7,10}\nimport { createHash, randomBytes } from \"crypto\";\n\nconst verifier = randomBytes(32).toString(\"base64url\");\nconst challenge = createHash(\"sha256\").update(verifier).digest(\"base64url\");\n\n// Include challenge in authorization request\nconst authUrl = `https://example.com/authorize?code_challenge=${challenge}&code_challenge_method=S256&...`;\n\n// Include verifier in token exchange\nconst tokenResponse = await fetch(tokenUrl, {\n method: \"POST\",\n body: new URLSearchParams({\n code: result.code,\n code_verifier: verifier,\n // ... other parameters\n }),\n});\n```\n\n:::\n\n**Token storage choices** - Choose storage based on your security requirements:\n\n::: code-group\n\n```typescript [Ephemeral Storage]\n// Tokens lost on restart (more secure)\nimport { browserAuth, inMemoryStore } from \"oauth-callback/mcp\";\n\nconst authProvider = browserAuth({\n store: inMemoryStore(),\n});\n```\n\n```typescript [Persistent Storage]\n// Tokens saved to disk (convenient)\nimport { browserAuth, fileStore } from \"oauth-callback/mcp\";\n\nconst authProvider = browserAuth({\n store: fileStore(\"~/.myapp/tokens.json\"),\n});\n```\n\n:::\n\n## Requirements and Registration\n\n### Prerequisites\n\n::: details System Requirements\n\n| Requirement | Details |\n| --------------------- | ---------------------------------------------------------------- |\n| **Runtime** | Node.js 18+, Deno, or Bun |\n| **OAuth Application** | Register your app with the OAuth provider |\n| **Redirect URI** | Configure `http://localhost:3000/callback` (or your chosen port) |\n| **Browser** | User must have a default browser configured |\n| **Permissions** | Ability to bind to localhost ports |\n\n:::\n\n### Standard OAuth Registration\n\nMost OAuth providers require pre-registering your application:\n\n1. Create an OAuth app in the provider's developer console\n2. Set redirect URI to `http://localhost:3000/callback`\n3. Copy your client ID (and secret if provided)\n4. Use credentials in your code\n\n### Dynamic Client Registration for MCP\n\nSome MCP servers support Dynamic Client Registration ([RFC 7591](https://www.rfc-editor.org/rfc/rfc7591.html)), eliminating pre-registration:\n\n::: tip No Pre-Registration Required\n\n```typescript {1}\n// No client_id or client_secret needed!\nconst authProvider = browserAuth({\n scope: \"read write\",\n store: fileStore(),\n});\n```\n\n:::\n\nThe Notion MCP example demonstrates this — the server automatically registers your client on first use. This greatly simplifies distribution of MCP-enabled tools.\n\n```mermaid\nflowchart LR\n subgraph \"Traditional OAuth Setup\"\n A[Developer] -->|1 Register app| B[OAuth Provider]\n B -->|2 Get client_id| A\n A -->|3 Embed client_id in app| D[CLI App]\n A -->|4 Distribute app| C[User]\n C -->|5 Runs app to authenticate| D\n end\n\n subgraph \"Dynamic Client Registration\"\n E[User] -->|1 Run app| F[CLI App]\n F -->|2 Auto-register| G[OAuth Provider]\n G -->|3 Return credentials| F\n F -->|4 Complete auth| H[Ready to use!]\n end\n\n style H fill:#3498DB\n```\n", "dir"=>"/", "name"=>"llms-full.txt", "path"=>"llms-full.txt"}
### Page Frontmatter

```

## Results

### Theme Data

### Page Data

### Page Frontmatter

## More

Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata).

---

---
url: /oauth-callback/api/storage-providers.md
description: >-
  Token storage interfaces and implementations for persisting OAuth tokens,
  client credentials, and session state.
---

# Storage Providers

Storage providers in OAuth Callback manage the persistence of OAuth tokens, client credentials, and session state. The library provides flexible storage interfaces with built-in implementations for both ephemeral and persistent storage, plus the ability to create custom storage backends.

## Storage Interfaces

OAuth Callback defines two storage interfaces for different levels of OAuth state management:

### TokenStore Interface

The basic `TokenStore` interface handles OAuth token persistence:

```typescript
interface TokenStore {
  get(key: string): Promise;
  set(key: string, tokens: Tokens): Promise;
  delete(key: string): Promise;
}
```

#### Tokens Type

```typescript
interface Tokens {
  accessToken: string; // OAuth access token
  refreshToken?: string; // Optional refresh token
  expiresAt?: number; // Absolute expiry time (Unix ms)
  scope?: string; // Space-delimited granted scopes
}
```

### OAuthStore Interface

The extended `OAuthStore` interface adds support for Dynamic Client Registration and PKCE verifier persistence:

```typescript
import { OAuthStoreBrand } from "oauth-callback/mcp";

interface OAuthStore extends TokenStore {
  readonly [OAuthStoreBrand]: true; // Required brand for type detection

  getClient(key: string): Promise;
  setClient(key: string, client: ClientInfo): Promise;
  deleteClient(key: string): Promise;

  getCodeVerifier(key: string): Promise;
  setCodeVerifier(key: string, verifier: string): Promise;
  deleteCodeVerifier(key: string): Promise;
}
```

#### ClientInfo Type

```typescript
interface ClientInfo {
  clientId: string; // OAuth client ID
  clientSecret?: string; // OAuth client secret
  clientIdIssuedAt?: number; // When client was registered
  clientSecretExpiresAt?: number; // When secret expires
}
```

## Built-in Storage Providers

### inMemoryStore()

Ephemeral storage that keeps tokens in memory. Tokens are lost when the process exits.

```typescript
function inMemoryStore(): TokenStore;
```

#### Usage

```typescript
import open from "open";
import { browserAuth, inMemoryStore } from "oauth-callback/mcp";

const authProvider = browserAuth({
  launch: open,
  store: inMemoryStore(),
});
```

#### Characteristics

| Feature         | Description                                |
| --------------- | ------------------------------------------ |
| **Persistence** | None - data lost on process exit           |
| **Concurrency** | Thread-safe within process                 |
| **Security**    | Maximum - no disk persistence              |
| **Performance** | Fastest - no I/O operations                |
| **Use Cases**   | Development, testing, short-lived sessions |

#### Implementation Details

```typescript
// Internal implementation uses a Map
const store = new Map();

// All operations are synchronous but return Promises
// for interface consistency
```

### fileStore()

Persistent storage that saves tokens to a JSON file on disk.

```typescript
function fileStore(filepath?: string): TokenStore;
```

#### Parameters

| Parameter  | Type     | Default              | Description                        |
| ---------- | -------- | -------------------- | ---------------------------------- |
| `filepath` | `string` | `~/.mcp/tokens.json` | Custom file path for token storage |

#### Usage

```typescript
import open from "open";
import { browserAuth, fileStore } from "oauth-callback/mcp";

// Use default location (~/.mcp/tokens.json)
const authProvider = browserAuth({
  launch: open,
  store: fileStore(),
});

// Use custom location
const customAuth = browserAuth({
  launch: open,
  store: fileStore("/path/to/my-tokens.json"),
});

// Environment-specific storage
const envAuth = browserAuth({
  launch: open,
  store: fileStore(`~/.myapp/${process.env.NODE_ENV}-tokens.json`),
});
```

#### Characteristics

| Feature         | Description                         |
| --------------- | ----------------------------------- |
| **Persistence** | Survives process restarts           |
| **Concurrency** | ⚠️ Not safe across processes        |
| **Security**    | File permissions (mode 0600)        |
| **Performance** | File I/O on each operation          |
| **Use Cases**   | Desktop apps, long-running services |

#### File Format

The file store saves tokens in JSON format:

```json
{
  "mcp-tokens": {
    "accessToken": "eyJhbGciOiJSUzI1NiIs...",
    "refreshToken": "refresh_token_here",
    "expiresAt": 1735689600000,
    "scope": "read write"
  },
  "app-specific-key": {
    "accessToken": "another_token",
    "expiresAt": 1735693200000
  }
}
```

::: warning Concurrent Access
The file store is not safe for concurrent access across multiple processes. If you need multi-process support, implement a custom storage provider with proper locking mechanisms.
:::

## Storage Key Management

Storage keys namespace tokens for different applications or environments:

### Single Application

```typescript
import open from "open";

const authProvider = browserAuth({
  launch: open,
  store: fileStore(),
  storeKey: "my-app", // Default: "mcp-tokens"
});
```

### Multiple Applications

```typescript
import open from "open";

// App 1
const app1Auth = browserAuth({
  launch: open,
  store: fileStore(),
  storeKey: "app1-tokens",
});

// App 2 (same file, different key)
const app2Auth = browserAuth({
  launch: open,
  store: fileStore(),
  storeKey: "app2-tokens",
});
```

### Environment Separation

```typescript
import open from "open";

const authProvider = browserAuth({
  launch: open,
  store: fileStore(),
  storeKey: `${process.env.APP_NAME}-${process.env.NODE_ENV}`,
});
// Results in keys like: "myapp-dev", "myapp-staging", "myapp-prod"
```

## Custom Storage Implementations

Create custom storage providers by implementing the `TokenStore` or `OAuthStore` interface:

### Basic Custom Storage

#### Redis Storage Example

```typescript
import { TokenStore, Tokens } from "oauth-callback/mcp";
import Redis from "ioredis";

class RedisTokenStore implements TokenStore {
  private redis: Redis;
  private prefix: string;

  constructor(redis: Redis, prefix = "oauth:") {
    this.redis = redis;
    this.prefix = prefix;
  }

  async get(key: string): Promise {
    const data = await this.redis.get(this.prefix + key);
    return data ? JSON.parse(data) : null;
  }

  async set(key: string, tokens: Tokens): Promise {
    const ttl = tokens.expiresAt
      ? Math.floor((tokens.expiresAt - Date.now()) / 1000)
      : undefined;

    if (ttl && ttl > 0) {
      await this.redis.setex(this.prefix + key, ttl, JSON.stringify(tokens));
    } else {
      await this.redis.set(this.prefix + key, JSON.stringify(tokens));
    }
  }

  async delete(key: string): Promise {
    await this.redis.del(this.prefix + key);
  }
}

// Usage
import open from "open";
const redis = new Redis();
const authProvider = browserAuth({
  launch: open,
  store: new RedisTokenStore(redis),
});
```

#### SQLite Storage Example

```typescript
import { TokenStore, Tokens } from "oauth-callback/mcp";
import Database from "better-sqlite3";

class SQLiteTokenStore implements TokenStore {
  private db: Database.Database;

  constructor(dbPath = "./tokens.db") {
    this.db = new Database(dbPath);
    this.init();
  }

  private init() {
    this.db.exec(`
      CREATE TABLE IF NOT EXISTS tokens (
        key TEXT PRIMARY KEY,
        access_token TEXT NOT NULL,
        refresh_token TEXT,
        expires_at INTEGER,
        scope TEXT,
        updated_at INTEGER DEFAULT (unixepoch())
      )
    `);
  }

  async get(key: string): Promise {
    const row = this.db.prepare("SELECT * FROM tokens WHERE key = ?").get(key);

    if (!row) return null;

    return {
      accessToken: row.access_token,
      refreshToken: row.refresh_token,
      expiresAt: row.expires_at,
      scope: row.scope,
    };
  }

  async set(key: string, tokens: Tokens): Promise {
    this.db
      .prepare(
        `
      INSERT OR REPLACE INTO tokens 
      (key, access_token, refresh_token, expires_at, scope)
      VALUES (?, ?, ?, ?, ?)
    `,
      )
      .run(
        key,
        tokens.accessToken,
        tokens.refreshToken,
        tokens.expiresAt,
        tokens.scope,
      );
  }

  async delete(key: string): Promise {
    this.db.prepare("DELETE FROM tokens WHERE key = ?").run(key);
  }
}

// Usage
import open from "open";
const authProvider = browserAuth({
  launch: open,
  store: new SQLiteTokenStore("./oauth-tokens.db"),
});
```

### Advanced Custom Storage

#### Full OAuthStore Implementation

```typescript
import {
  OAuthStoreBrand,
  type OAuthStore,
  type Tokens,
  type ClientInfo,
} from "oauth-callback/mcp";
import { MongoClient, Db } from "mongodb";

class MongoOAuthStore implements OAuthStore {
  readonly [OAuthStoreBrand] = true as const;
  private db: Db;

  constructor(db: Db) {
    this.db = db;
  }

  // TokenStore methods
  async get(key: string): Promise {
    const doc = await this.db.collection("tokens").findOne({ _id: key });
    return doc
      ? {
          accessToken: doc.accessToken,
          refreshToken: doc.refreshToken,
          expiresAt: doc.expiresAt,
          scope: doc.scope,
        }
      : null;
  }

  async set(key: string, tokens: Tokens): Promise {
    await this.db
      .collection("tokens")
      .replaceOne({ _id: key }, { _id: key, ...tokens }, { upsert: true });
  }

  async delete(key: string): Promise {
    await this.db.collection("tokens").deleteOne({ _id: key });
  }

  // Client registration methods
  async getClient(key: string): Promise {
    const doc = await this.db.collection("clients").findOne({ _id: key });
    return doc
      ? {
          clientId: doc.clientId,
          clientSecret: doc.clientSecret,
          clientIdIssuedAt: doc.clientIdIssuedAt,
          clientSecretExpiresAt: doc.clientSecretExpiresAt,
        }
      : null;
  }

  async setClient(key: string, client: ClientInfo): Promise {
    await this.db
      .collection("clients")
      .replaceOne({ _id: key }, { _id: key, ...client }, { upsert: true });
  }

  async deleteClient(key: string): Promise {
    await this.db.collection("clients").deleteOne({ _id: key });
  }

  // PKCE verifier methods
  async getCodeVerifier(key: string): Promise {
    const doc = await this.db.collection("verifiers").findOne({ _id: key });
    return doc?.verifier ?? null;
  }

  async setCodeVerifier(key: string, verifier: string): Promise {
    await this.db
      .collection("verifiers")
      .replaceOne({ _id: key }, { _id: key, verifier }, { upsert: true });
  }

  async deleteCodeVerifier(key: string): Promise {
    await this.db.collection("verifiers").deleteOne({ _id: key });
  }
}

// Usage
import open from "open";
const client = new MongoClient("mongodb://localhost:27017");
await client.connect();
const db = client.db("oauth");

const authProvider = browserAuth({
  launch: open,
  store: new MongoOAuthStore(db),
});
```

## Storage Security

### Encryption at Rest

Implement encryption for sensitive token storage:

```typescript
import { TokenStore, Tokens } from "oauth-callback/mcp";
import { createCipheriv, createDecipheriv, randomBytes, scrypt } from "crypto";
import { promisify } from "util";

class EncryptedTokenStore implements TokenStore {
  private store: TokenStore;
  private key: Buffer;

  constructor(store: TokenStore, password: string) {
    this.store = store;
  }

  async init(password: string) {
    // Derive key from password
    const salt = randomBytes(16);
    this.key = (await promisify(scrypt)(password, salt, 32)) as Buffer;
  }

  private encrypt(data: string): string {
    const iv = randomBytes(16);
    const cipher = createCipheriv("aes-256-gcm", this.key, iv);

    let encrypted = cipher.update(data, "utf8", "hex");
    encrypted += cipher.final("hex");

    const authTag = cipher.getAuthTag();

    return JSON.stringify({
      iv: iv.toString("hex"),
      authTag: authTag.toString("hex"),
      encrypted,
    });
  }

  private decrypt(encryptedData: string): string {
    const { iv, authTag, encrypted } = JSON.parse(encryptedData);

    const decipher = createDecipheriv(
      "aes-256-gcm",
      this.key,
      Buffer.from(iv, "hex"),
    );

    decipher.setAuthTag(Buffer.from(authTag, "hex"));

    let decrypted = decipher.update(encrypted, "hex", "utf8");
    decrypted += decipher.final("utf8");

    return decrypted;
  }

  async get(key: string): Promise {
    const encrypted = await this.store.get(key);
    if (!encrypted) return null;

    try {
      const decrypted = this.decrypt(JSON.stringify(encrypted));
      return JSON.parse(decrypted);
    } catch {
      return null; // Decryption failed
    }
  }

  async set(key: string, tokens: Tokens): Promise {
    const encrypted = this.encrypt(JSON.stringify(tokens));
    await this.store.set(key, JSON.parse(encrypted));
  }

  async delete(key: string): Promise {
    await this.store.delete(key);
  }
}

// Usage
import open from "open";
const encryptedStore = new EncryptedTokenStore(
  fileStore(),
  process.env.ENCRYPTION_PASSWORD!,
);
await encryptedStore.init(process.env.ENCRYPTION_PASSWORD!);

const authProvider = browserAuth({
  launch: open,
  store: encryptedStore,
});
```

### Secure File Permissions

When using file storage, ensure proper permissions:

```typescript
import { chmod } from "fs/promises";

class SecureFileStore implements TokenStore {
  private store: TokenStore;
  private filepath: string;

  constructor(filepath: string) {
    this.filepath = filepath;
    this.store = fileStore(filepath);
  }

  async set(key: string, tokens: Tokens): Promise {
    await this.store.set(key, tokens);
    // Ensure file is only readable by owner
    await chmod(this.filepath, 0o600);
  }

  // ... other methods delegate to store
}
```

## Storage Patterns

### Multi-Tenant Storage

Support multiple tenants with isolated storage:

```typescript
class TenantAwareStore implements TokenStore {
  private stores = new Map();

  getStore(tenantId: string): TokenStore {
    if (!this.stores.has(tenantId)) {
      this.stores.set(tenantId, fileStore(`~/.oauth/${tenantId}/tokens.json`));
    }
    return this.stores.get(tenantId)!;
  }

  async get(key: string): Promise {
    const [tenantId, tokenKey] = key.split(":");
    return this.getStore(tenantId).get(tokenKey);
  }

  async set(key: string, tokens: Tokens): Promise {
    const [tenantId, tokenKey] = key.split(":");
    return this.getStore(tenantId).set(tokenKey, tokens);
  }

  // ... other methods
}

// Usage
import open from "open";
const authProvider = browserAuth({
  launch: open,
  store: new TenantAwareStore(),
  storeKey: `${tenantId}:${appName}`,
});
```

### Cached Storage

Add caching layer for performance:

```typescript
class CachedTokenStore implements TokenStore {
  private cache = new Map();
  private store: TokenStore;
  private ttl: number;

  constructor(store: TokenStore, ttlSeconds = 300) {
    this.store = store;
    this.ttl = ttlSeconds * 1000;
  }

  async get(key: string): Promise {
    // Check cache first
    const cached = this.cache.get(key);
    if (cached && Date.now() < cached.expires) {
      return cached.tokens;
    }

    // Load from store
    const tokens = await this.store.get(key);
    if (tokens) {
      this.cache.set(key, {
        tokens,
        expires: Date.now() + this.ttl,
      });
    }

    return tokens;
  }

  async set(key: string, tokens: Tokens): Promise {
    // Update both cache and store
    this.cache.set(key, {
      tokens,
      expires: Date.now() + this.ttl,
    });
    await this.store.set(key, tokens);
  }

  async delete(key: string): Promise {
    this.cache.delete(key);
    await this.store.delete(key);
  }
}

// Usage
import open from "open";
const authProvider = browserAuth({
  launch: open,
  store: new CachedTokenStore(fileStore(), 600), // 10 min cache
});
```

## Testing Storage Providers

### Mock Storage for Tests

```typescript
import { TokenStore, Tokens } from "oauth-callback/mcp";

class MockTokenStore implements TokenStore {
  private data = new Map();
  public getCalls: string[] = [];
  public setCalls: Array<{ key: string; tokens: Tokens }> = [];

  async get(key: string): Promise {
    this.getCalls.push(key);
    return this.data.get(key) ?? null;
  }

  async set(key: string, tokens: Tokens): Promise {
    this.setCalls.push({ key, tokens });
    this.data.set(key, tokens);
  }

  async delete(key: string): Promise {
    this.data.delete(key);
  }

  // Test helper methods
  reset() {
    this.data.clear();
    this.getCalls = [];
    this.setCalls = [];
  }

  setTestData(key: string, tokens: Tokens) {
    this.data.set(key, tokens);
  }
}

// Usage in tests
describe("OAuth Flow", () => {
  let mockStore: MockTokenStore;

  beforeEach(() => {
    mockStore = new MockTokenStore();
    mockStore.setTestData("test-key", {
      accessToken: "test-token",
      expiresAt: Date.now() + 3600000,
    });
  });

  it("should use stored tokens", async () => {
    const authProvider = browserAuth({
      launch: () => {}, // Noop for tests
      store: mockStore,
      storeKey: "test-key",
    });

    // Test your OAuth flow
    expect(mockStore.getCalls).toContain("test-key");
  });
});
```

## Migration Strategies

### Migrating Storage Backends

```typescript
async function migrateStorage(
  source: TokenStore,
  target: TokenStore,
  keys?: string[],
) {
  // If no keys specified, migrate all (if source supports listing)
  const keysToMigrate = keys || ["mcp-tokens"]; // Default key

  for (const key of keysToMigrate) {
    const tokens = await source.get(key);
    if (tokens) {
      await target.set(key, tokens);
      console.log(`Migrated tokens for key: ${key}`);
    }
  }

  console.log("Migration complete");
}

// Example: Migrate from file to Redis
const fileStorage = fileStore();
const redisStorage = new RedisTokenStore(redis);

await migrateStorage(fileStorage, redisStorage);
```

### Upgrading Token Format

```typescript
class LegacyAdapter implements TokenStore {
  private store: TokenStore;

  constructor(store: TokenStore) {
    this.store = store;
  }

  async get(key: string): Promise {
    const data = await this.store.get(key);
    if (!data) return null;

    // Handle legacy format
    if ("access_token" in (data as any)) {
      return {
        accessToken: (data as any).access_token,
        refreshToken: (data as any).refresh_token,
        expiresAt: (data as any).expires_at,
        scope: (data as any).scope,
      };
    }

    return data;
  }

  // ... other methods
}
```

## Best Practices

### Choosing a Storage Provider

| Scenario               | Recommended Storage   | Reason                |
| ---------------------- | --------------------- | --------------------- |
| Development/Testing    | `inMemoryStore()`     | No persistence needed |
| CLI Tools (single-use) | `inMemoryStore()`     | Security, simplicity  |
| Desktop Apps           | `fileStore()`         | User convenience      |
| Server Applications    | Custom (Redis/DB)     | Scalability, sharing  |
| High Security          | `inMemoryStore()`     | No disk persistence   |
| Multi-tenant           | Custom implementation | Isolation required    |

### Storage Key Conventions

```typescript
// Use hierarchical keys for organization
const key = `${organization}:${application}:${environment}`;

// Examples:
("acme:billing-app:production");
("acme:billing-app:staging");
("personal:cli-tool:default");
```

### Error Handling

Always handle storage failures gracefully:

```typescript
class ResilientTokenStore implements TokenStore {
  private primary: TokenStore;
  private fallback: TokenStore;

  constructor(primary: TokenStore, fallback: TokenStore) {
    this.primary = primary;
    this.fallback = fallback;
  }

  async get(key: string): Promise {
    try {
      return await this.primary.get(key);
    } catch (error) {
      console.warn("Primary storage failed, using fallback:", error);
      return await this.fallback.get(key);
    }
  }

  async set(key: string, tokens: Tokens): Promise {
    try {
      await this.primary.set(key, tokens);
      // Also update fallback for consistency
      await this.fallback.set(key, tokens).catch(() => {});
    } catch (error) {
      console.warn("Primary storage failed, using fallback:", error);
      await this.fallback.set(key, tokens);
    }
  }

  // ... other methods
}

// Usage: Redis with file fallback
import open from "open";
const authProvider = browserAuth({
  launch: open,
  store: new ResilientTokenStore(new RedisTokenStore(redis), fileStore()),
});
```

## Related APIs

* [`browserAuth`](/api/browser-auth) - OAuth provider using storage
* [`TokenStore` Types](/api/types#tokenstore) - TypeScript interfaces
* [`OAuthError`](/api/oauth-error) - Error handling

---

---
url: /oauth-callback/api/types.md
description: >-
  Complete reference for all TypeScript types, interfaces, and type definitions
  in the oauth-callback library.
---

# TypeScript Types

OAuth Callback is fully typed with TypeScript, providing comprehensive type safety and excellent IDE support. This page documents all exported types and interfaces available in the library.

## Type Organization

```mermaid
flowchart TB
    subgraph "Core Types"
        GetAuthCodeOptions
        CallbackResult
        ServerOptions
        CallbackServer
    end

    subgraph "Error Types"
        OAuthError
        TimeoutError
    end

    subgraph "Storage Types"
        TokenStore
        OAuthStore
        Tokens
        ClientInfo
    end

    subgraph "MCP Types"
        BrowserAuthOptions
        OAuthClientProvider["OAuthClientProvider (MCP SDK)"]
    end

    GetAuthCodeOptions --> CallbackResult
    ServerOptions --> CallbackServer
    BrowserAuthOptions --> TokenStore
    BrowserAuthOptions --> OAuthStore
    OAuthStore --> Tokens
    OAuthStore --> ClientInfo
```

## Core Types

### GetAuthCodeOptions

Configuration options for the `getAuthCode` function.

```typescript
interface GetAuthCodeOptions {
  authorizationUrl: string; // OAuth authorization URL
  port?: number; // Server port (default: 3000)
  hostname?: string; // Hostname (default: "localhost")
  callbackPath?: string; // Callback path (default: "/callback")
  timeout?: number; // Timeout in ms (default: 30000)
  launch?: (url: string) => unknown; // Optional URL launcher
  successHtml?: string; // Custom success HTML
  errorHtml?: string; // Custom error HTML template
  signal?: AbortSignal; // Cancellation signal
  onRequest?: (req: Request) => void; // Request callback
}
```

#### Usage Example

```typescript
import type { GetAuthCodeOptions } from "oauth-callback";

const options: GetAuthCodeOptions = {
  authorizationUrl: "https://oauth.example.com/authorize?...",
  port: 8080,
  timeout: 60000,
  successHtml: "

Success!

", errorHtml: "

Error:

", onRequest: (req) => console.log(`Request: ${req.url}`), }; const result = await getAuthCode(options); ``` ### CallbackResult Result object returned from OAuth callback containing authorization code or error details. ```typescript interface CallbackResult { code?: string; // Authorization code state?: string; // State parameter for CSRF error?: string; // OAuth error code error_description?: string; // Error description error_uri?: string; // Error info URI [key: string]: string | undefined; // Additional params } ``` #### Usage Example ```typescript import type { CallbackResult } from "oauth-callback"; function handleCallback(result: CallbackResult) { if (result.error) { console.error(`OAuth error: ${result.error}`); if (result.error_description) { console.error(`Details: ${result.error_description}`); } return; } if (result.code) { console.log(`Authorization code: ${result.code}`); // Validate state for CSRF protection if (result.state !== expectedState) { throw new Error("State mismatch - possible CSRF attack"); } // Exchange code for tokens exchangeCodeForTokens(result.code); } } ``` ### ServerOptions Configuration options for the OAuth callback server. ```typescript interface ServerOptions { port: number; // Port to bind to hostname?: string; // Hostname (default: "localhost") successHtml?: string; // Custom success HTML errorHtml?: string; // Error HTML template signal?: AbortSignal; // Cancellation signal onRequest?: (req: Request) => void; // Request callback } ``` #### Usage Example ```typescript import type { ServerOptions } from "oauth-callback"; const serverOptions: ServerOptions = { port: 3000, hostname: "127.0.0.1", successHtml: `

Authorization successful!

`, onRequest: (req) => { const url = new URL(req.url); console.log(`[${req.method}] ${url.pathname}`); }, }; ``` ### CallbackServer Interface for OAuth callback server implementations across different runtimes. ```typescript interface CallbackServer { start(options: ServerOptions): Promise; waitForCallback(path: string, timeout: number): Promise; stop(): Promise; } ``` #### Implementation Example ```typescript import type { CallbackServer, ServerOptions, CallbackResult, } from "oauth-callback"; class CustomCallbackServer implements CallbackServer { private server?: HttpServer; async start(options: ServerOptions): Promise { // Start HTTP server this.server = await createServer(options); } async waitForCallback( path: string, timeout: number, ): Promise { // Wait for OAuth callback return new Promise((resolve, reject) => { const timer = setTimeout(() => { reject(new Error("Timeout waiting for callback")); }, timeout); this.server.on("request", (req) => { if (req.url.startsWith(path)) { clearTimeout(timer); const params = parseQueryParams(req.url); resolve(params); } }); }); } async stop(): Promise { // Stop server await this.server?.close(); } } ``` ## Storage Types ### TokenStore Basic interface for OAuth token storage. ```typescript interface TokenStore { get(key: string): Promise; set(key: string, tokens: Tokens): Promise; delete(key: string): Promise; } ``` ### Tokens OAuth token data structure. ```typescript interface Tokens { accessToken: string; // OAuth access token refreshToken?: string; // Optional refresh token expiresAt?: number; // Absolute expiry (Unix ms) scope?: string; // Space-delimited scopes } ``` #### Usage Example ```typescript import type { Tokens, TokenStore } from "oauth-callback/mcp"; class CustomTokenStore implements TokenStore { private storage = new Map(); async get(key: string): Promise { return this.storage.get(key) ?? null; } async set(key: string, tokens: Tokens): Promise { // Check if token is expired if (tokens.expiresAt && Date.now() >= tokens.expiresAt) { console.warn("Storing expired token"); } this.storage.set(key, tokens); } async delete(key: string): Promise { this.storage.delete(key); } } ``` ### OAuthStore Extended storage interface with Dynamic Client Registration and PKCE verifier persistence. ```typescript import { OAuthStoreBrand } from "oauth-callback/mcp"; interface OAuthStore extends TokenStore { readonly [OAuthStoreBrand]: true; // Required brand for type detection getClient(key: string): Promise; setClient(key: string, client: ClientInfo): Promise; deleteClient(key: string): Promise; getCodeVerifier(key: string): Promise; setCodeVerifier(key: string, verifier: string): Promise; deleteCodeVerifier(key: string): Promise; } ``` ### ClientInfo Dynamic client registration data. ```typescript interface ClientInfo { clientId: string; // OAuth client ID clientSecret?: string; // Client secret clientIdIssuedAt?: number; // Registration time clientSecretExpiresAt?: number; // Secret expiry } ``` #### Complete Storage Example ```typescript import { OAuthStoreBrand, type OAuthStore, type Tokens, type ClientInfo, } from "oauth-callback/mcp"; class DatabaseOAuthStore implements OAuthStore { readonly [OAuthStoreBrand] = true as const; constructor(private db: Database) {} // TokenStore methods async get(key: string): Promise { return await this.db.tokens.findOne({ key }); } async set(key: string, tokens: Tokens): Promise { await this.db.tokens.upsert({ key }, tokens); } async delete(key: string): Promise { await this.db.tokens.delete({ key }); } // Client registration methods async getClient(key: string): Promise { return await this.db.clients.findOne({ key }); } async setClient(key: string, client: ClientInfo): Promise { await this.db.clients.upsert({ key }, client); } async deleteClient(key: string): Promise { await this.db.clients.delete({ key }); } // PKCE verifier methods async getCodeVerifier(key: string): Promise { const doc = await this.db.verifiers.findOne({ key }); return doc?.verifier ?? null; } async setCodeVerifier(key: string, verifier: string): Promise { await this.db.verifiers.upsert({ key }, { verifier }); } async deleteCodeVerifier(key: string): Promise { await this.db.verifiers.delete({ key }); } } ``` ## MCP Types ### BrowserAuthOptions Configuration for browser-based OAuth flows with MCP servers. ```typescript interface BrowserAuthOptions { // OAuth credentials clientId?: string; // Pre-registered client ID clientSecret?: string; // Pre-registered secret scope?: string; // OAuth scopes // Server configuration port?: number; // Callback port (default: 3000) hostname?: string; // Hostname (default: "localhost") callbackPath?: string; // Path (default: "/callback") // Storage store?: TokenStore; // Token storage storeKey?: string; // Storage key for token isolation // Behavior launch?: (url: string) => unknown; // URL launcher authTimeout?: number; // Timeout ms (default: 300000) // UI successHtml?: string; // Success page HTML errorHtml?: string; // Error page template // Debugging onRequest?: (req: Request) => void; // Request logger } ``` #### Usage Example ```typescript import type { BrowserAuthOptions } from "oauth-callback/mcp"; import { browserAuth, fileStore } from "oauth-callback/mcp"; const options: BrowserAuthOptions = { // Dynamic Client Registration - no credentials needed scope: "read write", // Custom server configuration port: 8080, hostname: "127.0.0.1", // Persistent storage store: fileStore("~/.myapp/tokens.json"), storeKey: "production", // Timeout authTimeout: 600000, // 10 minutes // Custom UI successHtml: "

Success!

", // Debugging onRequest: (req) => { console.log(`[OAuth] ${new URL(req.url).pathname}`); }, }; const authProvider = browserAuth(options); ``` ## Error Types ### OAuthError OAuth-specific error class. ```typescript class OAuthError extends Error { name: "OAuthError"; error: string; // OAuth error code error_description?: string; // Human-readable description error_uri?: string; // Info URI constructor(error: string, description?: string, uri?: string); } ``` ### TimeoutError Timeout-specific error class. ```typescript class TimeoutError extends Error { name: "TimeoutError"; constructor(message?: string); } ``` #### Error Handling Example ```typescript import { OAuthError, TimeoutError } from "oauth-callback"; import type { CallbackResult } from "oauth-callback"; function handleAuthResult(result: CallbackResult) { // Check for OAuth errors in result if (result.error) { throw new OAuthError( result.error, result.error_description, result.error_uri, ); } if (!result.code) { throw new Error("No authorization code received"); } return result.code; } // Usage with proper error handling try { const code = await getAuthCode(authUrl); } catch (error) { if (error instanceof OAuthError) { console.error(`OAuth error: ${error.error}`); } else if (error instanceof TimeoutError) { console.error("Authorization timed out"); } else { console.error("Unexpected error:", error); } } ``` ## Type Guards Useful type guard functions for runtime type checking: ```typescript import type { Tokens, ClientInfo } from "oauth-callback/mcp"; // Check if object is Tokens function isTokens(obj: unknown): obj is Tokens { return ( typeof obj === "object" && obj !== null && "accessToken" in obj && typeof (obj as any).accessToken === "string" ); } // Check if object is ClientInfo function isClientInfo(obj: unknown): obj is ClientInfo { return ( typeof obj === "object" && obj !== null && "clientId" in obj && typeof (obj as any).clientId === "string" ); } // Check if error is OAuthError function isOAuthError(error: unknown): error is OAuthError { return error instanceof OAuthError; } // Usage const stored = await store.get("key"); if (stored && isTokens(stored)) { console.log("Valid tokens:", stored.accessToken); } ``` ## Generic Type Patterns ### Result Type Pattern ```typescript type Result = | { success: true; data: T } | { success: false; error: E }; async function safeGetAuthCode( url: string, ): Promise> { try { const result = await getAuthCode(url); return { success: true, data: result }; } catch (error) { if (error instanceof OAuthError) { return { success: false, error }; } return { success: false, error: error as Error }; } } // Usage const result = await safeGetAuthCode(authUrl); if (result.success) { console.log("Code:", result.data.code); } else { console.error("Error:", result.error.message); } ``` ### Storage Adapter Pattern ```typescript type StorageAdapter = { load(): Promise; save(data: T): Promise; remove(): Promise; }; function createStorageAdapter( store: TokenStore, key: string, ): StorageAdapter { return { async load() { const data = await store.get(key); return data as T | null; }, async save(data: T) { await store.set(key, data as any); }, async remove() { await store.delete(key); }, }; } ``` ## Type Exports ### Main Package Exports ```typescript // From "oauth-callback" export type { GetAuthCodeOptions, CallbackResult, CallbackServer, ServerOptions, }; export { getAuthCode, OAuthError, inMemoryStore, fileStore }; ``` ### MCP Sub-Package Exports ```typescript // From "oauth-callback/mcp" export type { BrowserAuthOptions, TokenStore, OAuthStore, Tokens, ClientInfo }; export { browserAuth, inMemoryStore, fileStore }; ``` ### Namespace Export ```typescript // Also available via namespace import { mcp } from "oauth-callback"; // All MCP types and functions under mcp namespace const authProvider = mcp.browserAuth({ store: mcp.fileStore(), }); ``` ## TypeScript Configuration For optimal type support, use these TypeScript settings: ```json { "compilerOptions": { "target": "ES2020", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "types": ["node", "bun-types"] } } ``` ## Type Versioning The library follows semantic versioning for types: * **Major version**: Breaking type changes * **Minor version**: New types or optional properties * **Patch version**: Type fixes that don't break compatibility ## Related Documentation * [`getAuthCode`](/api/get-auth-code) - Main function using these types * [`browserAuth`](/api/browser-auth) - MCP provider using these types * [`Storage Providers`](/api/storage-providers) - Storage type implementations * [`OAuthError`](/api/oauth-error) - Error type documentation --- --- url: /oauth-callback/what-is-oauth-callback.md description: >- Learn how OAuth Callback simplifies OAuth 2.0 authorization code capture for CLI tools, desktop apps, and MCP clients using localhost callbacks. --- # What is OAuth Callback? {#top} An OAuth callback is the mechanism that allows OAuth 2.0 authorization servers to return authorization codes to your application after user authentication. For native applications like CLI tools, desktop apps, and MCP clients, receiving this callback requires spinning up a temporary HTTP server on localhost — a process that **OAuth Callback** makes trivially simple with just one function call. **OAuth Callback** is designed for developers building CLI tools, desktop applications, automation scripts, and Model Context Protocol (MCP) clients that need to capture OAuth authorization codes via a localhost callback. Whether you're automating workflows across services (Notion, Linear, GitHub), building developer tools, or creating MCP-enabled applications, **OAuth Callback** handles the complexity of the loopback redirect flow recommended by [RFC 8252](https://www.rfc-editor.org/rfc/rfc8252.html) while providing modern features like Dynamic Client Registration and flexible token storage — all with support for Node.js 18+, Deno, and Bun. ## Understanding OAuth Callbacks ### What is a Callback URL in OAuth 2.0? In the OAuth 2.0 authorization code flow, the callback URL (also called redirect URI) is where the authorization server sends the user's browser after they approve or deny your application's access request. This URL receives critical information: * **On success**: An authorization `code` parameter that your app exchanges for access tokens * **On failure**: An `error` parameter describing what went wrong * **Security parameters**: The `state` value for CSRF protection For web applications, this callback is typically a route on your server. But for native applications without a public web server, you need a different approach. ### The Loopback Redirect Pattern Native applications (CLIs, desktop apps) can't expose public URLs for callbacks. Instead, they use the **loopback interface** — a temporary HTTP server on `http://localhost` or `http://127.0.0.1`. This pattern, standardized in [RFC 8252](https://www.rfc-editor.org/rfc/rfc8252.html) (OAuth 2.0 for Native Apps), provides several benefits: * **No public exposure**: The callback server only accepts local connections * **Dynamic ports**: Apps can use any available port (e.g., 3000, 8080) * **Automatic cleanup**: The server shuts down immediately after receiving the callback * **Universal support**: Works across all platforms without special permissions Here's how the flow works: ```mermaid sequenceDiagram participant App as CLI/Desktop App participant Browser as User's Browser participant Auth as Authorization Server participant Local as localhost:3000 App->>Local: Start HTTP server App->>Browser: Open authorization URL Browser->>Auth: Request authorization Auth->>Browser: Show consent screen Browser->>Auth: User approves Auth->>Browser: Redirect to localhost:3000/callback?code=xyz Browser->>Local: GET /callback?code=xyz Local->>App: Capture code App->>Local: Shutdown server Local->>Browser: Show success page ``` ### Security Best Practices OAuth 2.0 for native apps requires additional security measures: **Proof Key for Code Exchange (PKCE)** - [RFC 7636](https://www.rfc-editor.org/rfc/rfc7636.html) mandates using PKCE for public clients (those without client secrets). PKCE prevents authorization code interception attacks by binding the code exchange to a cryptographic challenge. ```mermaid sequenceDiagram participant App as Native App participant Browser participant Auth as Auth Server Note over App: Generate code_verifier Note over App: Hash to create code_challenge App->>Browser: Open /authorize?code_challenge=... Browser->>Auth: Authorization request with challenge Auth->>Auth: Store challenge Auth->>Browser: Redirect with code Browser->>App: Return code App->>Auth: POST /token with code + verifier Auth->>Auth: Verify: hash(verifier) == challenge Auth->>App: Return access token ``` **State Parameter** - A random value that prevents CSRF attacks. Your app generates this value, includes it in the authorization request, and validates it in the callback. **Dynamic Client Registration (DCR)** - [RFC 7591](https://www.rfc-editor.org/rfc/rfc7591.html) allows apps to register OAuth clients on-the-fly without pre-configuration. This is particularly useful for MCP servers where users shouldn't need to manually register OAuth applications. ## How OAuth Callback Solves It OAuth Callback eliminates the boilerplate of implementing loopback redirects. Instead of manually managing servers, ports, and browser launches, you get a complete solution in one function call. ### Core Functionality The library handles the entire OAuth callback flow: 1. **Starts a localhost HTTP server** on your specified port 2. **Opens the user's browser** to the authorization URL 3. **Captures the callback** with the authorization code 4. **Returns the result** as a clean JavaScript object 5. **Shuts down the server** automatically ### Zero Configuration Example Here's a complete OAuth flow in just 6 lines: ```typescript {3-5} import { getAuthCode } from "oauth-callback"; const result = await getAuthCode( "https://github.com/login/oauth/authorize?client_id=xxx&redirect_uri=http://localhost:3000/callback", ); console.log("Authorization code:", result.code); ``` That's it. No server setup, no browser management, no cleanup code. ### Cross-Runtime Support OAuth Callback uses modern Web Standards APIs (Request, Response, URL) that work identically across: * **Node.js 18+** - Native fetch and Web Streams support * **Deno** - First-class Web Standards implementation * **Bun** - High-performance runtime with built-in APIs This means your OAuth code is portable across runtimes without modifications. ### MCP Integration For Model Context Protocol applications, OAuth Callback provides the `browserAuth()` provider that integrates seamlessly with the MCP SDK: ::: code-group ```typescript [MCP with OAuth Callback] import { browserAuth, fileStore } from "oauth-callback/mcp"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const authProvider = browserAuth({ store: fileStore(), // Persist tokens to ~/.mcp/tokens.json scope: "read write", }); // Use directly with MCP transports const transport = new StreamableHTTPClientTransport( new URL("https://mcp.example.com"), { authProvider }, ); ``` ```typescript [Namespace Import] import { mcp } from "oauth-callback"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; const authProvider = mcp.browserAuth({ store: mcp.fileStore(), scope: "read write", }); ``` ::: ::: tip MCP Integration Features The MCP integration handles: * **Dynamic Client Registration** when supported by the server * **Token persistence** with `fileStore()` or ephemeral `inMemoryStore()` * **Automatic re-authentication** when tokens expire * **Multiple app namespace support** via `storeKey` option ::: ## When to Use OAuth Callback ### Perfect For OAuth Callback is ideal when: * **You control the user's machine** - CLI tools, desktop apps, development tools * **You can open a browser** - The user has a default browser configured * **You need quick setup** - No server infrastructure or complex configuration * **You're building MCP clients** - Direct integration with Model Context Protocol SDK ### Consider Alternatives When **Device Authorization Flow** ([RFC 8628](https://www.rfc-editor.org/rfc/rfc8628.html)) might be better if: * **No browser access** - SSH sessions, headless servers, CI/CD environments * **Remote terminals** - The auth happens on a different device than the app * **Input-constrained devices** - Smart TVs, IoT devices without keyboards The Device Flow shows a code to the user that they enter on another device, eliminating the need for a browser on the same machine. However, it requires OAuth provider support and a more complex user experience. ```mermaid flowchart TD subgraph "OAuth Callback Flow" A1[CLI App] -->|Opens browser| B1[Auth Page] B1 -->|User authorizes| C1[localhost:3000/callback] C1 -->|Returns code| A1 A1 -->|Exchange code| D1[Access Token] end subgraph "Device Authorization Flow" A2[CLI App] -->|Request device code| B2[Auth Server] B2 -->|Returns code + URL| A2 A2 -->|Display to user| C2["User Code: ABCD-1234"] C2 -->|User enters on phone/laptop| D2[Auth Page] D2 -->|User authorizes| E2[Auth Server] A2 -->|Poll for token| E2 E2 -->|Returns token| F2[Access Token] end ``` ## Security Considerations OAuth Callback implements security best practices by default: ::: info Security Features * **Localhost-only binding** - The callback server only accepts connections from `127.0.0.1` or `::1`, preventing remote access attempts. * **Automatic cleanup** - The HTTP server shuts down immediately after receiving the callback, minimizing the attack surface window. * **No persistent state** - Server is ephemeral and leaves no traces after completion. ::: ::: warning Always Validate State Always validate the `state` parameter returned in the callback matches what you sent: ```typescript {4-7} const state = crypto.randomUUID(); const authUrl = `https://example.com/authorize?state=${state}&...`; const result = await getAuthCode(authUrl); if (result.state !== state) { throw new Error("State mismatch - possible CSRF attack"); } ``` ::: ::: details Proof Key for Code Exchange (PKCE) Implementation For public clients, implement Proof Key for Code Exchange as required by [RFC 7636](https://www.rfc-editor.org/rfc/rfc7636.html): ```typescript {3-4,7,10} import { createHash, randomBytes } from "crypto"; const verifier = randomBytes(32).toString("base64url"); const challenge = createHash("sha256").update(verifier).digest("base64url"); // Include challenge in authorization request const authUrl = `https://example.com/authorize?code_challenge=${challenge}&code_challenge_method=S256&...`; // Include verifier in token exchange const tokenResponse = await fetch(tokenUrl, { method: "POST", body: new URLSearchParams({ code: result.code, code_verifier: verifier, // ... other parameters }), }); ``` ::: **Token storage choices** - Choose storage based on your security requirements: ::: code-group ```typescript [Ephemeral Storage] // Tokens lost on restart (more secure) import { browserAuth, inMemoryStore } from "oauth-callback/mcp"; const authProvider = browserAuth({ store: inMemoryStore(), }); ``` ```typescript [Persistent Storage] // Tokens saved to disk (convenient) import { browserAuth, fileStore } from "oauth-callback/mcp"; const authProvider = browserAuth({ store: fileStore("~/.myapp/tokens.json"), }); ``` ::: ## Requirements and Registration ### Prerequisites ::: details System Requirements | Requirement | Details | | --------------------- | ---------------------------------------------------------------- | | **Runtime** | Node.js 18+, Deno, or Bun | | **OAuth Application** | Register your app with the OAuth provider | | **Redirect URI** | Configure `http://localhost:3000/callback` (or your chosen port) | | **Browser** | User must have a default browser configured | | **Permissions** | Ability to bind to localhost ports | ::: ### Standard OAuth Registration Most OAuth providers require pre-registering your application: 1. Create an OAuth app in the provider's developer console 2. Set redirect URI to `http://localhost:3000/callback` 3. Copy your client ID (and secret if provided) 4. Use credentials in your code ### Dynamic Client Registration for MCP Some MCP servers support Dynamic Client Registration ([RFC 7591](https://www.rfc-editor.org/rfc/rfc7591.html)), eliminating pre-registration: ::: tip No Pre-Registration Required ```typescript {1} // No client_id or client_secret needed! const authProvider = browserAuth({ scope: "read write", store: fileStore(), }); ``` ::: The Notion MCP example demonstrates this — the server automatically registers your client on first use. This greatly simplifies distribution of MCP-enabled tools. ```mermaid flowchart LR subgraph "Traditional OAuth Setup" A[Developer] -->|1 Register app| B[OAuth Provider] B -->|2 Get client_id| A A -->|3 Embed client_id in app| D[CLI App] A -->|4 Distribute app| C[User] C -->|5 Runs app to authenticate| D end subgraph "Dynamic Client Registration" E[User] -->|1 Run app| F[CLI App] F -->|2 Auto-register| G[OAuth Provider] G -->|3 Return credentials| F F -->|4 Complete auth| H[Ready to use!] end style H fill:#3498DB ```