Getting Started β
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/callbackin your OAuth app settings
Installation β
Install the package using your preferred package manager:
bun add oauth-callbacknpm install oauth-callbackpnpm add oauth-callbackyarn add oauth-callbackBasic Usage β
The simplest way to capture an OAuth authorization code is with the getAuthCode() function:
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
const result = await getAuthCode(authUrl);
console.log("Authorization code:", result.code);
console.log("State:", result.state);That's it! The library will:
- Start a local HTTP server on port 3000
- Open the user's browser to the authorization URL
- Capture the callback with the authorization code
- 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:
GitHub OAuth Setup
- Go to Settings β Developer settings β OAuth Apps
- Click New OAuth App
- Fill in: - Application name: Your app name
- Homepage URL: Your website or GitHub repo
- Authorization callback URL: http://localhost:3000/callback
 
- Save and copy your Client ID and Client Secret
Google OAuth Setup
- Go to Google Cloud Console
- Create or select a project
- Enable the necessary APIs
- Go to APIs & Services β Credentials
- Click Create Credentials β OAuth client ID
- Choose Desktop app as application type
- Add http://localhost:3000/callbackto authorized redirect URIs
- Copy your Client ID and Client Secret
Step 2: Implement the Authorization Flow β
Create a file auth.ts with your OAuth implementation:
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(authUrl.toString());
    // 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:
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:
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 β
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({
  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:
import { browserAuth, inMemoryStore } from "oauth-callback/mcp";
// Tokens are lost when the process exits
const authProvider = browserAuth({
  store: inMemoryStore(),
});import { browserAuth, fileStore } from "oauth-callback/mcp";
// Tokens persist across sessions
const authProvider = browserAuth({
  store: fileStore(), // Saves to ~/.mcp/tokens.json
});
// Or specify custom location
const customAuth = browserAuth({
  store: fileStore("/path/to/tokens.json"),
});Pre-configured Credentials β
If you have pre-registered OAuth credentials:
const authProvider = browserAuth({
  clientId: "your-client-id",
  clientSecret: "your-client-secret",
  scope: "read write",
  store: fileStore(),
  storeKey: "my-app", // Namespace for multiple apps
});Advanced Configuration β
Custom Port and Timeout β
Configure the callback server port and timeout:
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:
const result = await getAuthCode({
  authorizationUrl: authUrl,
  successHtml: `
    <html>
      <body style="font-family: system-ui; text-align: center; padding: 50px;">
        <h1>β
 Authorization Successful!</h1>
        <p>You can now close this window and return to the application.</p>
      </body>
    </html>
  `,
  errorHtml: `
    <html>
      <body style="font-family: system-ui; text-align: center; padding: 50px;">
        <h1>β Authorization Failed</h1>
        <p>Error: {{error_description}}</p>
        <p>Please try again or contact support.</p>
      </body>
    </html>
  `,
});Request Logging β
Add logging for debugging OAuth flows:
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:
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:
import { getAuthCode, OAuthError } from "oauth-callback";
try {
  const result = await getAuthCode(authUrl);
  // 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:
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:
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:
# Run interactive demo
bun run example:demo
# The demo includes a mock OAuth server for testingTesting with Real Providers β
# Set credentials in .env file
GITHUB_CLIENT_ID=your_client_id
GITHUB_CLIENT_SECRET=your_client_secret
# Run example
bun run example:github# No credentials needed - uses Dynamic Client Registration
bun run example:notionTroubleshooting β
Common Issues and Solutions β
Port Already in Use
If port 3000 is already in use:
const result = await getAuthCode({
  authorizationUrl: authUrl,
  port: 8080, // Use a different port
});Also update your OAuth app's redirect URI to match.
Browser Doesn't Open
If the browser doesn't open automatically:
const result = await getAuthCode({
  authorizationUrl: authUrl,
  openBrowser: false, // Disable auto-open
});
console.log(`Please open: ${authUrl}`);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)
Token Refresh Errors
For MCP apps with token refresh issues:
const authProvider = browserAuth({
  store: fileStore(), // Use persistent storage
  authTimeout: 300000, // Increase timeout to 5 minutes
});Getting Help β
Need assistance? Here are your options:
- π GitHub Issues - Report bugs or request features
- π¬ GitHub Discussions - Ask questions and share ideas
- π¬ Discord Community - Join our Discord server for real-time help and discussions
- π Stack Overflow - Search or ask questions with the oauth-callbacktag
Happy coding! π