This guide shows you how to implement OAuth authentication in your FastMCP server using the OAuth Proxy.
- Quick Start
- Provider Setup
- Configuration Options
- Advanced Features
- Security Best Practices
- Troubleshooting
The simplest way to add OAuth is using 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!,
scopes: ["openid", "profile", "email"],
}),
name: "My Server",
version: "1.0.0",
});
// Add a protected tool
server.addTool({
canAccess: requireAuth,
description: "Get user profile from Google",
execute: async (_args, { session }) => {
const { accessToken } = getAuthSession(session);
const response = await fetch(
"https://www.googleapis.com/oauth2/v2/userinfo",
{
headers: { Authorization: `Bearer ${accessToken}` },
},
);
return JSON.stringify(await response.json());
},
name: "get-profile",
});
await server.start({
transportType: "httpStream",
httpStream: { port: 3000 },
});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
For providers without pre-built support (SAP, Auth0, Okta, etc.), use OAuthProvider:
import { FastMCP, getAuthSession, OAuthProvider, requireAuth } from "fastmcp";
const server = new FastMCP({
auth: new OAuthProvider({
authorizationEndpoint: "https://provider.com/oauth/authorize",
baseUrl: "https://your-server.com",
clientId: process.env.OAUTH_CLIENT_ID!,
clientSecret: process.env.OAUTH_CLIENT_SECRET!,
scopes: ["openid", "profile"],
tokenEndpoint: "https://provider.com/oauth/token",
}),
name: "My Server",
version: "1.0.0",
});
server.addTool({
canAccess: requireAuth,
description: "Call protected API",
execute: async (_args, { session }) => {
const { accessToken } = getAuthSession(session);
const response = await fetch("https://api.provider.com/data", {
headers: { Authorization: `Bearer ${accessToken}` },
});
return JSON.stringify(await response.json());
},
name: "get-data",
});
await server.start({
transportType: "httpStream",
httpStream: { port: 3000 },
});For more control over OAuth behavior, you can 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,
},
});
await server.start({
transportType: "httpStream",
httpStream: { port: 3000 },
});1. Create OAuth 2.0 Credentials
- Go to Google Cloud Console
- Create OAuth 2.0 Client ID
- Application type: "Web application"
- Add authorized redirect URI:
https://your-server.com/oauth/callback
2. Implementation
import { FastMCP, GoogleProvider, requireAuth } from "fastmcp";
const server = new FastMCP({
auth: new GoogleProvider({
baseUrl: "https://your-server.com",
clientId: "xxx.apps.googleusercontent.com",
clientSecret: "your-secret",
scopes: ["openid", "profile", "email"],
}),
name: "My Server",
version: "1.0.0",
});Common Scopes:
openid- OpenID Connect authenticationprofile- Basic profile informationemail- Email addresshttps://www.googleapis.com/auth/userinfo.profile- Full profilehttps://www.googleapis.com/auth/gmail.readonly- Gmail read access
1. Create OAuth App
- Go to GitHub Developer Settings
- Click "New OAuth App"
- Set Authorization callback URL:
https://your-server.com/oauth/callback
2. Implementation
import { FastMCP, GitHubProvider, requireAuth } from "fastmcp";
const server = new FastMCP({
auth: new GitHubProvider({
baseUrl: "https://your-server.com",
clientId: "your-github-app-id",
clientSecret: "your-github-app-secret",
scopes: ["read:user", "user:email"],
}),
name: "My Server",
version: "1.0.0",
});Common Scopes:
read:user- Read user profile datauser:email- Access email addressesrepo- Access repositoriesread:org- Read organization membership
1. Register Application
- Go to Azure Portal
- Click "New registration"
- Add redirect URI:
https://your-server.com/oauth/callback - Create a client secret under "Certificates & secrets"
2. Implementation
import { FastMCP, AzureProvider, requireAuth } from "fastmcp";
const server = new FastMCP({
auth: new AzureProvider({
baseUrl: "https://your-server.com",
clientId: "your-azure-app-id",
clientSecret: "your-azure-app-secret",
scopes: ["openid", "profile", "email"],
tenantId: "common", // or specific tenant ID
}),
name: "My Server",
version: "1.0.0",
});Tenant Options:
common- Multi-tenant, allows any Azure AD accountorganizations- Any organizational accountconsumers- Personal Microsoft accounts only<tenant-id>- Specific tenant only
Common Scopes:
openid- OpenID Connectprofile- User profileemail- Email addressUser.Read- Read user profileMail.Read- Read user's mail
Complete configuration reference:
interface OAuthProxyConfig {
// REQUIRED: Upstream provider settings
upstreamAuthorizationEndpoint: string;
upstreamTokenEndpoint: string;
upstreamClientId: string;
upstreamClientSecret: string;
baseUrl: string;
// OPTIONAL: OAuth behavior
redirectPath?: string; // default: "/oauth/callback"
scopes?: string[]; // provider-specific defaults
forwardPkce?: boolean; // default: false
consentRequired?: boolean; // default: true
consentSigningKey?: string; // auto-generated if not provided
allowedRedirectUriPatterns?: string[];
transactionTtl?: number; // seconds, default: 600
authorizationCodeTtl?: number; // seconds, default: 300
// OPTIONAL: Token swap pattern (enabled by default)
enableTokenSwap?: boolean; // default: true
jwtSigningKey?: string; // optional (auto-generated if not provided)
accessTokenTtl?: number; // seconds, default: 3600
refreshTokenTtl?: number; // seconds, default: 2592000
// OPTIONAL: Storage
tokenStorage?: TokenStorage; // default: MemoryTokenStorage
tokenVerifier?: TokenVerifier; // custom JWT verification
}Control which callback URIs clients can register:
const authProxy = new OAuthProxy({
// ... other config
allowedRedirectUriPatterns: [
"https://*.example.com/*", // Wildcard subdomain
"http://localhost:*", // Any localhost port
"https://app.example.com/callback", // Exact match
],
});Adjust timeouts for your security requirements:
const authProxy = new OAuthProxy({
// ... other config
transactionTtl: 600, // 10 minutes for authorization flow
authorizationCodeTtl: 300, // 5 minutes for code exchange
accessTokenTtl: 3600, // 1 hour for access tokens
refreshTokenTtl: 2592000, // 30 days for refresh tokens
});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",
}),
});Note: 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 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");
},
});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
Pass custom claims from upstream tokens (roles, permissions, etc.) to your proxy-issued JWTs for authorization in MCP tools.
Enabled by default - Claims are automatically passed through with secure defaults:
import { OAuthProxy } from "fastmcp/auth";
// Default behavior - claims passthrough enabled
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",
// customClaimsPassthrough is enabled by default
});Custom configuration:
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,
},
});
// Or disable if not needed
const authProxyNoClaims = new OAuthProxy({
// ... other config ...
customClaimsPassthrough: false,
});Using claims for authorization:
// Example: Role-based access control
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..." }],
};
},
});
// Example: Permission-based access
server.addTool({
name: "delete-resource",
description: "Delete a resource",
canAccess: async ({ session }) => {
const token = session?.headers?.["authorization"]?.replace("Bearer ", "");
if (!token) return false;
const payload = JSON.parse(
Buffer.from(token.split(".")[1], "base64url").toString(),
);
// Check fine-grained permissions
return payload.permissions?.includes("resource:delete");
},
execute: async (args) => {
// Delete logic here
return {
content: [{ type: "text", text: "Resource deleted" }],
};
},
});Key features:
- Extracts from both access tokens and ID tokens
- Protected claims (aud, iss, exp, iat, nbf, jti, client_id) never copied
- Access token claims take precedence over ID token claims
- Size limits and type validation for security
- Supports allowlist/blocklist filtering
- Optional prefix for claim names
Storage is automatically encrypted with AES-256-GCM. You don't need to manually wrap with EncryptedTokenStorage:
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({
// ... other config
tokenStorage: new MemoryTokenStorage(),
encryptionKey: false, // Explicitly disable encryption
});Encryption details:
- AES-256-GCM encryption (enabled by default)
- Scrypt key derivation
- Authentication tag verification
- Auto-generated key if not provided (recommended to provide your own)
Implement your own storage backend:
import { TokenStorage } from "fastmcp/auth";
class RedisTokenStorage implements TokenStorage {
private redis: RedisClient;
constructor(redisClient: RedisClient) {
this.redis = redisClient;
}
async save(key: string, value: unknown, ttl?: number): Promise<void> {
const serialized = JSON.stringify(value);
if (ttl) {
await this.redis.setex(key, ttl, serialized);
} else {
await this.redis.set(key, serialized);
}
}
async get(key: string): Promise<unknown | null> {
const value = await this.redis.get(key);
return value ? JSON.parse(value) : null;
}
async delete(key: string): Promise<void> {
await this.redis.del(key);
}
async cleanup(): Promise<void> {
// Redis handles TTL automatically
}
}
const authProxy = new OAuthProxy({
// ... other config
tokenStorage: new RedisTokenStorage(redisClient),
});For distributed systems or when you need to verify tokens using public keys (RS256/ES256), use JWKS (JSON Web Key Set) verification.
JWKS support requires the optional jose package:
npm install joseimport { JWKSVerifier } from "fastmcp/auth";
const verifier = new JWKSVerifier({
jwksUri: "https://provider.com/.well-known/jwks.json",
issuer: "https://provider.com",
audience: "your-client-id",
});
// Verify a token
const result = await verifier.verify(token);
if (result.valid) {
console.log("Token valid:", result.claims);
} else {
console.log("Token invalid:", result.error);
}Replace the default HS256 JWT issuer with JWKS verification:
import { OAuthProxy, JWKSVerifier } 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.CLIENT_ID,
upstreamClientSecret: process.env.CLIENT_SECRET,
// Use JWKS verification instead of HS256
tokenVerifier: new JWKSVerifier({
jwksUri: "https://provider.com/.well-known/jwks.json",
issuer: "https://provider.com",
audience: process.env.CLIENT_ID,
}),
});interface JWKSVerifierConfig {
/**
* URL to the JWKS endpoint
*/
jwksUri: string;
/**
* Expected token issuer
*/
issuer: string;
/**
* Expected token audience
*/
audience: string;
/**
* How long to cache JWKS keys (milliseconds)
* @default 600000 (10 minutes)
*/
cacheDuration?: number;
/**
* Minimum time between JWKS refetches (milliseconds)
* @default 30000 (30 seconds)
*/
cooldownDuration?: number;
}Verify tokens from multiple OAuth providers:
import { JWKSVerifier } from "fastmcp/auth";
// Create verifiers for each provider
const googleVerifier = new JWKSVerifier({
jwksUri: "https://www.googleapis.com/oauth2/v3/certs",
issuer: "https://accounts.google.com",
audience: process.env.GOOGLE_CLIENT_ID,
});
const githubVerifier = new JWKSVerifier({
jwksUri: "https://token.actions.githubusercontent.com/.well-known/jwks",
issuer: "https://token.actions.githubusercontent.com",
audience: "your-app",
});
// Verify based on token issuer
async function verifyToken(token: string, provider: string) {
const verifier = provider === "google" ? googleVerifier : githubVerifier;
return await verifier.verify(token);
}- Key Caching: JWKS keys are cached automatically to reduce network requests
- Cooldown Period: Prevents excessive refetching during key rotation
- Lazy Loading: The
josepackage is only loaded when JWKSVerifier is instantiated - Zero Impact: If you don't use JWKS, the jose package isn't required
Use JWKS verification when:
- ✅ You need to verify tokens in multiple services (distributed systems)
- ✅ You want to use asymmetric keys (RS256/ES256)
- ✅ Your upstream provider uses JWKS for token validation
- ✅ You need public key verification without shared secrets
Use default HS256 (JWTIssuer) when:
- ✅ You have a single server verifying tokens
- ✅ You want simpler setup without additional dependencies
- ✅ You prefer symmetric key signing (faster)
- ✅ You don't need to share verification keys with external services
Use the built-in authorization helpers to restrict tool access:
import {
requireAuth,
requireScopes,
requireRole,
requireAll,
requireAny,
getAuthSession,
} from "fastmcp";
// Require any authenticated user
server.addTool({
canAccess: requireAuth,
description: "Requires authentication",
execute: async (_args, { session }) => {
const { accessToken } = getAuthSession(session);
// Use accessToken to call upstream APIs
return "Authenticated!";
},
name: "protected-tool",
});
// Require specific OAuth scopes
server.addTool({
canAccess: requireScopes("read:user", "write:data"),
description: "Requires specific scopes",
execute: async () => "Access granted with required scopes!",
name: "scoped-tool",
});
// Require specific role (from session)
server.addTool({
canAccess: requireRole("admin"),
description: "Admin only",
execute: async () => "Welcome, admin!",
name: "admin-tool",
});
// Combine requirements (AND logic)
server.addTool({
canAccess: requireAll(requireAuth, requireScopes("admin")),
description: "Auth AND admin scope required",
execute: async () => "Full access granted!",
name: "full-access-tool",
});
// Allow alternatives (OR logic)
server.addTool({
canAccess: requireAny(requireRole("admin"), requireRole("moderator")),
description: "Admin or moderator",
execute: async () => "Staff access granted!",
name: "staff-tool",
});Custom Authorization:
For complex authorization logic, use a custom function:
server.addTool({
canAccess: (auth) => {
if (!auth) return false;
return auth.role === "admin" || auth.permissions?.includes("special");
},
description: "Custom authorization logic",
execute: async () => "Custom access granted!",
name: "custom-auth-tool",
});Extracting Session Data:
Use getAuthSession for type-safe access to the OAuth session:
import { getAuthSession, GoogleSession } from "fastmcp";
server.addTool({
canAccess: requireAuth,
name: "get-profile",
execute: async (_args, { session }) => {
// Type-safe destructuring (throws if not authenticated)
const { accessToken } = getAuthSession(session);
// Or with provider-specific typing:
// const { accessToken } = getAuthSession<GoogleSession>(session);
const response = await fetch("https://api.example.com/user", {
headers: { Authorization: `Bearer ${accessToken}` },
});
return JSON.stringify(await response.json());
},
});For local testing environments:
const authProxy = new GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
baseUrl: "http://localhost:3000",
consentRequired: false, // ⚠️ Development only!
});Warning: Only disable consent in trusted development environments.
- Use HTTPS
const authProxy = new OAuthProxy({
baseUrl: "https://your-server.com", // Not http://
// ...
});- 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,
);- 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,
);- Enable Consent Screen
const authProxy = new OAuthProxy({
consentRequired: true, // Default, but be explicit
// ...
});- Use Persistent Encrypted Storage
const storage = new EncryptedTokenStorage(
new DiskStore({ directory: "/var/lib/fastmcp/oauth" }),
encryptionKey,
);- Validate Redirect URIs
const authProxy = new OAuthProxy({
allowedRedirectUriPatterns: [
"https://yourdomain.com/*",
"http://localhost:*", // Only for development
],
// ...
});- Set Appropriate TTLs
const authProxy = new OAuthProxy({
transactionTtl: 600, // 10 minutes
authorizationCodeTtl: 300, // 5 minutes
accessTokenTtl: 900, // 15 minutes (shorter = more secure)
refreshTokenTtl: 604800, // 7 days
// ...
});Store all secrets in environment variables:
# .env file
GOOGLE_CLIENT_ID=xxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-secret-here
JWT_SECRET=generate-with-crypto-random-bytes
ENCRYPTION_SECRET=different-secret-hereLoad them securely:
import * as dotenv from "dotenv";
dotenv.config();
const authProxy = new GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
baseUrl: process.env.BASE_URL!,
});Generate strong secrets:
import { randomBytes } from "crypto";
// Generate a strong secret (32 bytes = 256 bits)
const secret = randomBytes(32).toString("base64");
console.log(secret);Or use command line:
# Generate random secret
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"Problem: OAuth provider rejects the redirect URI.
Solution: Ensure the redirect URI in provider settings matches exactly:
{baseUrl}/oauth/callback
Examples:
https://your-server.com/oauth/callbackhttp://localhost:3000/oauth/callback
Causes:
- Transaction expired (default 10 minutes)
- Server restarted (in-memory storage lost)
- Clock skew between client and server
Solutions:
- Use persistent storage (DiskStore)
- Increase
transactionTtlif needed - Check system time synchronization
Problem: Code verifier doesn't match the challenge.
Solution: Ensure client is:
- Storing the code verifier correctly
- Sending it in the token request
- Using the same verifier that generated the challenge
Problem: Being redirected directly without consent.
Solutions:
- Check
consentRequiredistrue - Clear browser cookies for the domain
- Check consent cookie signing key is consistent
Problem: Using in-memory storage.
Solution: Use persistent storage:
const authProxy = new OAuthProxy({
tokenStorage: new DiskStore({
directory: "/var/lib/fastmcp/oauth",
}),
// ...
});Problem: TTL configuration issue.
Solution: Check your TTL values:
const authProxy = new OAuthProxy({
accessTokenTtl: 3600, // seconds, not milliseconds
refreshTokenTtl: 2592000, // 30 days
// ...
});Problem: Import path issue.
Solution: Ensure you're importing from the correct path:
// Correct
import { OAuthProxy } from "fastmcp/auth";
// Also correct
import { OAuthProxy } from "fastmcp";Make sure fastmcp is properly installed:
npm install fastmcpComplete working examples are available in the repository:
- oauth-integrated-server.ts - Google OAuth with FastMCP integration
- oauth-proxy-server.ts - Standalone OAuth proxy
- oauth-proxy-github.ts - GitHub provider example
- oauth-proxy-custom.ts - Custom provider with advanced features
# All tests
npm test
# OAuth tests only
npm test -- auth/
# Specific test file
npm test -- src/auth/OAuthProxy.test.ts- Start your server:
npm run dev- Register a client:
curl -X POST http://localhost:3000/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "Test Client",
"redirect_uris": ["http://localhost:8080/callback"]
}'- Visit authorization URL in browser:
http://localhost:3000/oauth/authorize?client_id=<client_id>&response_type=code&redirect_uri=http://localhost:8080/callback&code_challenge=<challenge>&code_challenge_method=S256
-
Complete OAuth flow through consent and provider authentication
-
Exchange authorization code for token:
curl -X POST http://localhost:3000/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code&code=<auth_code>&redirect_uri=http://localhost:8080/callback&code_verifier=<verifier>&client_id=<client_id>"- Review OAuth Proxy Features for detailed capabilities
- See Python vs TypeScript Comparison for migration guidance
- Check out the example implementations in
src/examples/