Skip to main content
The auth option uses FastMCP’s built-in OAuth Proxy that acts as a secure intermediary between MCP clients and upstream OAuth providers. The proxy handles the complete OAuth 2.1 authorization flow, including Dynamic Client Registration (DCR), PKCE, consent management, and token management with encryption and token swap patterns enabled by default.

Key Features

  • Secure by Default: Automatic encryption (AES-256-GCM) and token swap pattern
  • Zero Configuration: Auto-generates keys and handles OAuth flows automatically
  • Pre-configured Providers: Built-in support for Google, GitHub, and Azure
  • RFC Compliant: Implements DCR (RFC 7591), PKCE, and OAuth 2.1
  • Optional JWKS: Support for RS256/ES256 token verification (via optional jose dependency)

Quick Start

The simplest way to use the OAuth Proxy is through the auth option with a pre-configured provider:
import { FastMCP, getAuthSession, GoogleProvider, requireAuth } from "fastmcp";

const server = new FastMCP({
  auth: new GoogleProvider({
    baseUrl: "https://your-server.com",
    clientId: process.env.GOOGLE_CLIENT_ID!,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
  }),
  name: "My Server",
  version: "1.0.0",
});

server.addTool({
  canAccess: requireAuth,
  name: "protected-tool",
  execute: async (_args, { session }) => {
    const { accessToken } = getAuthSession(session);
    // Use accessToken to call upstream APIs
    return "Authenticated!";
  },
});
That’s it! All OAuth endpoints are automatically available:
  • /oauth/register - Dynamic Client Registration
  • /oauth/authorize - Authorization endpoint
  • /oauth/callback - OAuth callback handler
  • /oauth/consent - User consent screen
  • /oauth/token - Token exchange endpoint

Pre-configured Providers

import { GoogleProvider } from "fastmcp";

const server = new FastMCP({
  auth: new GoogleProvider({
    baseUrl: "https://your-server.com",
    clientId: process.env.GOOGLE_CLIENT_ID!,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    scopes: ["openid", "profile", "email"],
  }),
  name: "My Server",
  version: "1.0.0",
});
Setup:
  1. Go to Google Cloud Console
  2. Create OAuth 2.0 Client ID
  3. Add redirect URI: https://your-server.com/oauth/callback

Advanced Configuration

For more control over OAuth behavior, use the oauth option directly with an OAuthProxy:
import { FastMCP } from "fastmcp";
import { OAuthProxy } from "fastmcp/auth";

const authProxy = new OAuthProxy({
  upstreamAuthorizationEndpoint: "https://provider.com/oauth/authorize",
  upstreamTokenEndpoint: "https://provider.com/oauth/token",
  upstreamClientId: process.env.OAUTH_CLIENT_ID!,
  upstreamClientSecret: process.env.OAUTH_CLIENT_SECRET!,
  baseUrl: "https://your-server.com",
  scopes: ["openid", "profile"],
});

const server = new FastMCP({
  name: "My Server",
  oauth: {
    enabled: true,
    authorizationServer: authProxy.getAuthorizationServerMetadata(),
    proxy: authProxy,
  },
});

Token Swap Pattern

Token swap prevents upstream tokens from reaching the client. This is enabled by default for enhanced security.
import { OAuthProxy, DiskStore, JWTIssuer } from "fastmcp/auth";

const authProxy = new OAuthProxy({
  baseUrl: "https://your-server.com",
  upstreamAuthorizationEndpoint: "https://provider.com/oauth/authorize",
  upstreamTokenEndpoint: "https://provider.com/oauth/token",
  upstreamClientId: process.env.OAUTH_CLIENT_ID,
  upstreamClientSecret: process.env.OAUTH_CLIENT_SECRET,

  // Token swap is enabled by default
  // Optionally provide your own signing key (recommended for production)
  jwtSigningKey: await JWTIssuer.deriveKey(process.env.JWT_SECRET, 100000),

  // Use persistent storage
  tokenStorage: new DiskStore({
    directory: "/var/lib/fastmcp/oauth",
  }),
});
If you don’t provide jwtSigningKey, one will be auto-generated. For production, it’s recommended to provide your own derived key for consistency across server restarts.

Loading Upstream Tokens

When using token swap, load the upstream tokens in your tools:
server.addTool({
  name: "call-api",
  description: "Call upstream API with user's token",
  execute: async (args, { session }) => {
    const clientToken = session?.headers?.["authorization"]?.replace(
      "Bearer ",
      "",
    );

    // Load the upstream tokens
    const upstreamTokens = await authProxy.loadUpstreamTokens(clientToken);

    if (upstreamTokens) {
      const response = await fetch("https://api.provider.com/user", {
        headers: {
          Authorization: `Bearer ${upstreamTokens.accessToken}`,
        },
      });

      const data = await response.json();
      return {
        content: [{ type: "text", text: JSON.stringify(data) }],
      };
    }

    throw new Error("No valid token");
  },
});

Persistent Token Storage

Use DiskStore for production deployments:
import { DiskStore } from "fastmcp/auth";

const storage = new DiskStore({
  directory: "/var/lib/fastmcp/oauth",
  cleanupIntervalMs: 60000, // Cleanup every minute
  fileExtension: ".json",
});

const authProxy = new OAuthProxy({
  // ... other config
  tokenStorage: storage,
});
Benefits:
  • Tokens persist across server restarts
  • Automatic cleanup of expired entries
  • Thread-safe concurrent operations

Encrypted Token Storage

Storage is automatically encrypted with AES-256-GCM:
import { DiskStore, JWTIssuer } from "fastmcp/auth";

const authProxy = new OAuthProxy({
  // ... other config
  tokenStorage: new DiskStore({ directory: "/var/lib/fastmcp/oauth" }),
  // ← Automatically encrypted!

  // Optional: Provide custom encryption key (recommended for production)
  encryptionKey: await JWTIssuer.deriveKey(
    process.env.ENCRYPTION_SECRET + ":storage",
    100000,
  ),
});
To disable encryption (only for development/testing):
const authProxy = new OAuthProxy({
  tokenStorage: new MemoryTokenStorage(),
  encryptionKey: false, // Explicitly disable encryption
});

Custom Claims Passthrough

Pass custom claims from upstream tokens (roles, permissions, etc.) to your proxy-issued JWTs. Enabled by default:
import { OAuthProxy } from "fastmcp/auth";

const authProxy = new OAuthProxy({
  // ... other config
  customClaimsPassthrough: {
    // Extract from access token (default: true)
    fromAccessToken: true,

    // Extract from ID token (default: true)
    fromIdToken: true,

    // No prefix by default for RBAC compatibility
    claimPrefix: false,

    // Optional: Only allow specific claims
    allowedClaims: ["role", "roles", "permissions", "email", "groups"],

    // Optional: Block specific claims
    blockedClaims: ["internal_id", "debug_info"],

    // Maximum claim value size (default: 2000 chars)
    maxClaimValueSize: 2000,

    // Allow complex objects/arrays (default: false)
    allowComplexClaims: false,
  },
});

Using Claims for Authorization

server.addTool({
  name: "admin-dashboard",
  description: "Access admin dashboard",
  canAccess: async ({ session }) => {
    const token = session?.headers?.["authorization"]?.replace("Bearer ", "");
    if (!token) return false;

    // Decode the proxy JWT
    const payload = JSON.parse(
      Buffer.from(token.split(".")[1], "base64url").toString(),
    );

    // Check role claim from upstream IDP
    return payload.role === "admin" || payload.roles?.includes("admin");
  },
  execute: async () => {
    return {
      content: [{ type: "text", text: "Admin dashboard data..." }],
    };
  },
});

Configuration Options

OptionTypeDefaultDescription
upstreamAuthorizationEndpointstring-Required - OAuth provider’s authorization endpoint
upstreamTokenEndpointstring-Required - OAuth provider’s token endpoint
upstreamClientIdstring-Required - Your OAuth client ID
upstreamClientSecretstring-Required - Your OAuth client secret
baseUrlstring-Required - Your server’s base URL
redirectPathstring/oauth/callbackOAuth callback path
scopesstring[]Provider defaultsOAuth scopes to request
forwardPkcebooleanfalseForward PKCE to upstream provider
consentRequiredbooleantrueShow consent screen
enableTokenSwapbooleantrueEnable token swap pattern
jwtSigningKeystringAuto-generatedJWT signing key
encryptionKeystring | falseAuto-generatedStorage encryption key
tokenStorageTokenStorageMemoryTokenStorageToken storage backend
transactionTtlnumber600Transaction TTL (seconds)
authorizationCodeTtlnumber300Auth code TTL (seconds)
accessTokenTtlnumber3600Access token TTL (seconds)
refreshTokenTtlnumber2592000Refresh token TTL (seconds)

Security Best Practices

1

Use HTTPS in Production

const authProxy = new OAuthProxy({
  baseUrl: "https://your-server.com", // Not http://
  // ...
});
2

Derive Keys from Secrets

import { JWTIssuer } from "fastmcp/auth";

const jwtSigningKey = await JWTIssuer.deriveKey(
  process.env.JWT_SECRET,
  100000, // PBKDF2 iterations
);

const encryptionKey = await JWTIssuer.deriveKey(
  process.env.ENCRYPTION_SECRET,
  100000,
);
3

Use Different Keys for Different Purposes

const jwtKey = await JWTIssuer.deriveKey(process.env.SECRET + ":jwt", 100000);
const storageKey = await JWTIssuer.deriveKey(process.env.SECRET + ":storage", 100000);
const consentKey = await JWTIssuer.deriveKey(process.env.SECRET + ":consent", 100000);
4

Enable Consent Screen

const authProxy = new OAuthProxy({
  consentRequired: true, // Default, but be explicit
  // ...
});
5

Use Persistent Encrypted Storage

const storage = new DiskStore({
  directory: "/var/lib/fastmcp/oauth"
});

const authProxy = new OAuthProxy({
  tokenStorage: storage,
  encryptionKey: await JWTIssuer.deriveKey(process.env.ENCRYPTION_SECRET, 100000),
  // ...
});
6

Validate Redirect URIs

const authProxy = new OAuthProxy({
  allowedRedirectUriPatterns: [
    "https://yourdomain.com/*",
    "http://localhost:*", // Only for development
  ],
  // ...
});

Next Steps

Authentication

Learn about authentication options and tool authorization

Custom Routes

Add authenticated custom HTTP routes to your server

Build docs developers (and LLMs) love