Security & Production2026-07-193 min read

OAuth 2.0 in MCP: Implementation Step-by-Step

A working OAuth 2.1 with PKCE flow for an MCP server, from provider setup through token validation on every request — the same pattern real servers like Swiggy's and Zomato's use.

OAuth setup guidesCode examples

OAuth 2.1 with PKCE is the authentication pattern behind most production, user-facing MCP servers — it's what Swiggy's three official servers and Zomato's checkout-capable server both use. Here's the real flow, step by step.

Step 1: Register With an Authorization Server

You need an OAuth provider issuing tokens — either a managed identity provider (Auth0, Okta, WorkOS) or your own existing auth system if it already speaks OAuth 2.1. Register your MCP server as a client and define scopes specific to what your tools actually do — not a single blanket scope, but granular ones like mcp:read and mcp:orders:write, so a token can be limited to exactly what its holder needs.

Step 2: Implement the PKCE Authorization Code Flow

The client generates a random code_verifier, derives a code_challenge from it, and sends the challenge with the authorization request. After the user logs in and approves access, the authorization server returns a code that can only be exchanged for a token by presenting the original verifier — this is what prevents an intercepted authorization code from being usable by anyone else, which matters because MCP clients are frequently native apps without a securely hidden secret.

Step 3: Validate Every Incoming Request

Every tool call needs its token checked — not just the initial connection:

const authHeader = request.headers.authorization;
const token = authHeader?.split(" ")[1];

if (!token) {
  throw new Error("Missing bearer token");
}

const payload = await verifyJWT(token, jwksUri);

if (!payload.scopes.includes("mcp:tools")) {
  throw new Error("Insufficient permissions");
}

if (payload.exp < Date.now() / 1000) {
  throw new Error("Token expired");
}

Three checks matter here, not one: the token exists at all, it carries the specific scope the requested tool needs (not just "any valid token"), and it hasn't expired. Skipping the scope check is the most common real mistake — a server that only verifies "is this token valid" rather than "does this token have permission for THIS specific tool" ends up letting a low-privilege token call high-privilege tools.

Step 4: Handle Token Refresh

Access tokens should be short-lived (minutes to a couple of hours), with a separate, longer-lived refresh token used to obtain new access tokens without forcing the user to log in again. Store refresh tokens server-side, never in a location the AI client itself can read directly — the client should only ever hold the short-lived access token.

The Real-World Pattern to Copy

Swiggy's documented approach — OAuth 2.1 with PKCE over standard JSON-RPC, per-server scoping (Food, Instamart, and Dineout each have independent tool sets rather than one shared token covering everything) — is a genuinely good reference architecture if you're implementing this from scratch: narrow scopes per capability area, short-lived tokens, and no credential storage inside the AI client itself.

Join the Discussion

Code Snippets (0)

No code snippets shared yet. Be the first to contribute!