Skip to main content
RapidDev - Software Development Agency
mcp-tutorial

How to add OAuth authentication to an MCP server

Secure your remote MCP server with OAuth 2.1 and PKCE, the protocol's standard authentication mechanism. Register your server as an OAuth provider, implement the authorization and token endpoints, validate access tokens on each request, and configure clients to authenticate. Local stdio servers skip auth since the host process controls access.

What you'll learn

  • How MCP authentication works with OAuth 2.1 and PKCE
  • How to implement authorization and token endpoints for MCP
  • How to validate access tokens in MCP request handlers
  • How to configure MCP clients to authenticate with your server
  • When authentication is needed vs when it is not
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced9 min read45-60 minMCP TypeScript SDK 1.x with Streamable HTTP transport, OAuth 2.1 compliantMarch 2026RapidDev Engineering Team
TL;DR

Secure your remote MCP server with OAuth 2.1 and PKCE, the protocol's standard authentication mechanism. Register your server as an OAuth provider, implement the authorization and token endpoints, validate access tokens on each request, and configure clients to authenticate. Local stdio servers skip auth since the host process controls access.

Adding Authentication to Your MCP Server

MCP uses OAuth 2.1 with PKCE (Proof Key for Code Exchange) as its standard authentication mechanism for remote servers. When an MCP client connects to your server over HTTP, it discovers your OAuth endpoints through the /.well-known/oauth-authorization-server metadata endpoint, then performs the authorization flow to get an access token.

Local stdio servers do not need authentication because the host process (like Claude Desktop) manages access. But any server exposed over HTTP must implement auth to prevent unauthorized access. This tutorial walks through the full implementation: OAuth server metadata, authorization flow, token issuance, and request validation.

Prerequisites

  • A working MCP server using Streamable HTTP transport (not stdio)
  • Understanding of OAuth 2.0 concepts (authorization codes, access tokens, refresh tokens)
  • Node.js 18+ with a web framework like Express for HTTP endpoints
  • HTTPS configured for your server (required for OAuth in production)

Step-by-step guide

1

Understand when authentication is required

MCP authentication only applies to remote servers using HTTP-based transports (Streamable HTTP or the deprecated SSE transport). Local stdio servers inherit the security context of the host process — Claude Desktop, Cursor, or whichever application launched the server process. If your server is only used locally via stdio, you can skip authentication entirely. For remote servers, auth is critical to prevent unauthorized tool calls and data access.

typescript
1// Stdio transport — NO auth needed
2import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3const transport = new StdioServerTransport();
4await server.connect(transport);
5
6// HTTP transport — auth IS needed
7// Clients connect over the network and must authenticate

Expected result: You understand that auth is needed only for HTTP-exposed MCP servers.

2

Expose OAuth server metadata

MCP clients discover your auth configuration by fetching /.well-known/oauth-authorization-server from your server. This JSON document advertises your authorization endpoint, token endpoint, supported grant types, and PKCE requirements. Implement this endpoint on your HTTP server so clients know how to authenticate.

typescript
1// TypeScript — Express route for OAuth metadata
2import express from "express";
3
4const app = express();
5const SERVER_URL = "https://mcp.example.com";
6
7app.get("/.well-known/oauth-authorization-server", (req, res) => {
8 res.json({
9 issuer: SERVER_URL,
10 authorization_endpoint: `${SERVER_URL}/oauth/authorize`,
11 token_endpoint: `${SERVER_URL}/oauth/token`,
12 registration_endpoint: `${SERVER_URL}/oauth/register`,
13 response_types_supported: ["code"],
14 grant_types_supported: ["authorization_code", "refresh_token"],
15 token_endpoint_auth_methods_supported: ["none"],
16 code_challenge_methods_supported: ["S256"],
17 });
18});

Expected result: MCP clients can discover your auth endpoints by fetching the well-known metadata URL.

3

Implement the authorization and token endpoints

The authorization endpoint handles the user consent flow — the client redirects the user here to grant access. After consent, redirect back to the client with an authorization code. The token endpoint exchanges codes for access tokens and handles refresh token grants. Store authorization codes and tokens securely with expiration times.

typescript
1// TypeScript — simplified auth endpoints
2import crypto from "crypto";
3
4const authCodes = new Map<string, { clientId: string; codeChallenge: string; userId: string; expiresAt: number }>();
5const accessTokens = new Map<string, { clientId: string; userId: string; expiresAt: number }>();
6
7// Authorization endpoint
8app.get("/oauth/authorize", (req, res) => {
9 const { client_id, redirect_uri, code_challenge, code_challenge_method, state } = req.query;
10
11 if (code_challenge_method !== "S256") {
12 return res.status(400).json({ error: "S256 code challenge required" });
13 }
14
15 // In production: show consent page, authenticate user
16 // For simplicity, auto-approve:
17 const code = crypto.randomUUID();
18 authCodes.set(code, {
19 clientId: client_id as string,
20 codeChallenge: code_challenge as string,
21 userId: "user-1",
22 expiresAt: Date.now() + 600_000, // 10 minutes
23 });
24
25 const redirectUrl = new URL(redirect_uri as string);
26 redirectUrl.searchParams.set("code", code);
27 if (state) redirectUrl.searchParams.set("state", state as string);
28 res.redirect(redirectUrl.toString());
29});
30
31// Token endpoint
32app.post("/oauth/token", express.urlencoded({ extended: false }), (req, res) => {
33 const { grant_type, code, code_verifier } = req.body;
34
35 if (grant_type !== "authorization_code") {
36 return res.status(400).json({ error: "unsupported_grant_type" });
37 }
38
39 const authCode = authCodes.get(code);
40 if (!authCode || authCode.expiresAt < Date.now()) {
41 return res.status(400).json({ error: "invalid_grant" });
42 }
43
44 // Verify PKCE code challenge
45 const hash = crypto.createHash("sha256").update(code_verifier).digest("base64url");
46 if (hash !== authCode.codeChallenge) {
47 return res.status(400).json({ error: "invalid_grant" });
48 }
49
50 authCodes.delete(code);
51 const accessToken = crypto.randomUUID();
52 accessTokens.set(accessToken, {
53 clientId: authCode.clientId,
54 userId: authCode.userId,
55 expiresAt: Date.now() + 3600_000, // 1 hour
56 });
57
58 res.json({
59 access_token: accessToken,
60 token_type: "Bearer",
61 expires_in: 3600,
62 });
63});

Expected result: Clients can complete the OAuth authorization flow and receive access tokens.

4

Validate tokens on MCP requests

Add middleware to your HTTP server that extracts the Bearer token from the Authorization header and validates it before processing any MCP request. Reject requests with missing, expired, or invalid tokens. Pass the authenticated user context to your MCP tool handlers so they can apply per-user authorization logic.

typescript
1// TypeScript — auth middleware
2function authMiddleware(req: express.Request, res: express.Response, next: express.NextFunction) {
3 const authHeader = req.headers.authorization;
4 if (!authHeader?.startsWith("Bearer ")) {
5 return res.status(401).json({ error: "Bearer token required" });
6 }
7
8 const token = authHeader.slice(7);
9 const tokenData = accessTokens.get(token);
10
11 if (!tokenData || tokenData.expiresAt < Date.now()) {
12 accessTokens.delete(token);
13 return res.status(401).json({ error: "Token expired or invalid" });
14 }
15
16 // Attach user context for tool handlers
17 (req as any).userId = tokenData.userId;
18 next();
19}
20
21// Apply to MCP endpoint
22app.post("/mcp", authMiddleware, (req, res) => {
23 // Process MCP JSON-RPC request with authenticated user context
24});

Expected result: Unauthenticated requests are rejected with 401. Authenticated requests proceed with user context available.

5

Configure clients to authenticate with your server

MCP clients handle the OAuth flow automatically when they discover the well-known metadata. For Claude Desktop, add your server to the config with the HTTP URL. Claude will detect the auth requirement, open a browser for user consent, and manage token refresh. For teams deploying authenticated MCP servers to production, RapidDev can help set up the OAuth infrastructure, user management, and token rotation.

typescript
1// claude_desktop_config.json
2{
3 "mcpServers": {
4 "my-remote-server": {
5 "url": "https://mcp.example.com/mcp"
6 }
7 }
8}
9
10// The client will:
11// 1. Fetch /.well-known/oauth-authorization-server
12// 2. Generate PKCE code_verifier and code_challenge
13// 3. Open browser to authorization_endpoint
14// 4. Exchange auth code for access token at token_endpoint
15// 5. Include Bearer token in all subsequent MCP requests

Expected result: Claude Desktop or other MCP clients can connect to your server after completing the OAuth flow.

Complete working example

src/auth-server.ts
1import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2import { z } from "zod";
3import express from "express";
4import crypto from "crypto";
5
6const SERVER_URL = process.env.SERVER_URL || "https://mcp.example.com";
7const app = express();
8
9// In-memory stores (use a database in production)
10const authCodes = new Map<string, {
11 clientId: string; codeChallenge: string;
12 userId: string; expiresAt: number;
13}>();
14const tokens = new Map<string, {
15 userId: string; expiresAt: number;
16}>();
17
18// OAuth metadata
19app.get("/.well-known/oauth-authorization-server", (_req, res) => {
20 res.json({
21 issuer: SERVER_URL,
22 authorization_endpoint: `${SERVER_URL}/oauth/authorize`,
23 token_endpoint: `${SERVER_URL}/oauth/token`,
24 response_types_supported: ["code"],
25 grant_types_supported: ["authorization_code"],
26 code_challenge_methods_supported: ["S256"],
27 token_endpoint_auth_methods_supported: ["none"],
28 });
29});
30
31// Authorization endpoint (simplified)
32app.get("/oauth/authorize", (req, res) => {
33 const { redirect_uri, code_challenge, state } = req.query;
34 const code = crypto.randomUUID();
35 authCodes.set(code, {
36 clientId: "mcp-client",
37 codeChallenge: code_challenge as string,
38 userId: "user-1",
39 expiresAt: Date.now() + 600_000,
40 });
41 const url = new URL(redirect_uri as string);
42 url.searchParams.set("code", code);
43 if (state) url.searchParams.set("state", state as string);
44 res.redirect(url.toString());
45});
46
47// Token endpoint
48app.post("/oauth/token", express.urlencoded({ extended: false }), (req, res) => {
49 const { code, code_verifier } = req.body;
50 const authCode = authCodes.get(code);
51 if (!authCode || authCode.expiresAt < Date.now()) {
52 return res.status(400).json({ error: "invalid_grant" });
53 }
54 const hash = crypto.createHash("sha256").update(code_verifier).digest("base64url");
55 if (hash !== authCode.codeChallenge) {
56 return res.status(400).json({ error: "invalid_grant" });
57 }
58 authCodes.delete(code);
59 const accessToken = crypto.randomUUID();
60 tokens.set(accessToken, { userId: authCode.userId, expiresAt: Date.now() + 3600_000 });
61 res.json({ access_token: accessToken, token_type: "Bearer", expires_in: 3600 });
62});
63
64// Auth middleware
65function requireAuth(req: express.Request, res: express.Response, next: express.NextFunction) {
66 const bearer = req.headers.authorization?.slice(7);
67 const t = bearer ? tokens.get(bearer) : undefined;
68 if (!t || t.expiresAt < Date.now()) return res.status(401).end();
69 (req as any).userId = t.userId;
70 next();
71}
72
73app.post("/mcp", requireAuth, (req, res) => {
74 // Wire up MCP server to handle JSON-RPC over HTTP
75 console.error(`Authenticated request from user: ${(req as any).userId}`);
76});
77
78app.listen(3000, () => console.error("Auth MCP server on port 3000"));

Common mistakes when adding OAuth authentication to an MCP server

Why it's a problem: Adding authentication to a local stdio server

How to avoid: Stdio servers run as a child process of the host application. The host controls access. Auth is only needed for HTTP-exposed servers.

Why it's a problem: Not requiring PKCE

How to avoid: OAuth 2.1 mandates PKCE for public clients. MCP clients are public clients. Always require S256 code_challenge_method.

Why it's a problem: Storing tokens in memory only

How to avoid: In-memory token stores are lost on restart. Use a database or Redis for production deployments.

Why it's a problem: Not setting token expiration

How to avoid: Always set expires_in on tokens and reject expired tokens. Implement refresh tokens for long-lived sessions.

Why it's a problem: Running OAuth over HTTP instead of HTTPS

How to avoid: OAuth requires HTTPS in production. Tokens sent over HTTP can be intercepted. Use HTTPS for all auth endpoints.

Best practices

  • Use an established OAuth library or service (Auth0, Keycloak) instead of building from scratch
  • Always require PKCE with S256 for all MCP OAuth flows
  • Set short access token lifetimes (1 hour) and implement refresh tokens
  • Store tokens in a database with proper encryption, not in memory
  • Validate tokens on every MCP request, not just the initial connection
  • Pass authenticated user context to tool handlers for per-user authorization
  • Use HTTPS for all auth endpoints in production
  • Log authentication events (login, token refresh, failures) to stderr for security monitoring

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I'm building a remote MCP server that needs OAuth 2.1 with PKCE authentication. Show me how to implement the well-known metadata endpoint, authorization endpoint, token endpoint, and request validation middleware in TypeScript with Express.

MCP Prompt

Add OAuth 2.1 authentication to my MCP server at [URL]. Implement the /.well-known/oauth-authorization-server metadata endpoint, PKCE authorization flow, token issuance, and Bearer token validation middleware.

Frequently asked questions

Do stdio MCP servers need authentication?

No. Stdio servers run as child processes of the host application (Claude Desktop, Cursor). The host controls which servers are launched and has full access. Authentication is only needed for HTTP-based remote servers.

Why does MCP use OAuth 2.1 instead of API keys?

OAuth 2.1 with PKCE provides a standard, secure flow that works across different clients without sharing secrets. API keys would require each client to securely store the key, which browser-based and desktop clients cannot reliably do.

Can I use my existing OAuth provider (Auth0, Okta) with MCP?

Yes. Configure your MCP server's well-known metadata to point to your OAuth provider's authorization and token endpoints. The MCP client will follow the standard OAuth flow with your provider.

How does the client handle token refresh?

MCP clients should implement standard OAuth refresh token flow. When an access token expires (401 response), the client uses the refresh token to get a new access token without requiring user interaction.

Can I restrict which tools a user can access?

MCP does not have built-in per-tool authorization. Implement authorization checks inside each tool handler using the authenticated user context. Return isError: true for unauthorized tool calls.

Is there a simpler auth option for internal servers?

For internal servers on a private network, you could use a reverse proxy with basic auth or mutual TLS. However, OAuth 2.1 is the standard MCP mechanism and is recommended for interoperability. For production auth architecture, the RapidDev team can help evaluate the right approach for your infrastructure.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.