MCP Authentication: Implementing OAuth 2.1 with PKCE
A complete guide to securing MCP servers with OAuth 2.1 and PKCE — the auth spec, dynamic client registration, bearer tokens, and token rotation.
Mohammed Kafeel
Machine Learning Researcher
On this page
- TL;DR - Key Takeaways
- What Is MCP Authentication? (And Why It's Now Non-Negotiable)
- Why OAuth 2.1? What Changed from 2.0?
- What Is PKCE and Why Does MCP Require It?
- The MCP OAuth 2.1 Authorization Flow, Step by Step
- How to Implement OAuth 2.1 with PKCE for Your MCP Server
- Where MCP Authentication Still Goes Wrong
- MCP Auth for Enterprise: What You Actually Need
- Frequently Asked Questions
- Key Takeaways
- Ready to Build Secure AI Agents?
- Useful Sources
Most MCP servers are wide open. No token rotation. No PKCE. Static API keys baked into config files. For a protocol that's becoming the backbone of production AI agents, that's a serious problem - and it's fixable in an afternoon.
TL;DR - Key Takeaways
MCP OAuth is the standard authorization mechanism for remote MCP servers; HTTP-based transports must use it.
The MCP auth spec (updated June 18, 2025) formally positions MCP servers as OAuth 2.1 resource servers, with token issuance delegated to external IdPs.
PKCE became mandatory in the November 2025 spec update - all clients using the authorization code flow must implement it.
The flow has six steps: Protected Resource Metadata discovery → authorization server discovery → Dynamic Client Registration → PKCE auth request → code exchange for bearer token → token rotation on refresh.
Dynamic client registration (RFC 7591) lets AI agents self-register with new authorization servers at runtime - no pre-configuration needed.
Static API keys are fine for STDIO transport; they're a liability for remote HTTP servers.
Short-lived tokens + refresh token rotation are non-negotiable for production deployments.
What Is MCP Authentication? (And Why It's Now Non-Negotiable)
MCP authentication is the mechanism by which an MCP client proves it's authorized to access a protected MCP server - and by which the server validates that proof before returning any data.
For a long time, teams got away with "just use an API key." That worked when MCP was a local tool. It doesn't work anymore. (For the bigger picture on why MCP auth matters, nearly half of deployed servers still ship with none.)
Here's why API keys break for AI agents:
AI agents are non-human clients. They operate autonomously, across sessions, often without a user present to re-authenticate.
A single compromised API key gives an attacker unlimited, unscoped access - with no expiry and no audit trail.
API keys can't represent delegated authority. You can't say "this agent can read Salesforce records but not write them" with a static string.
The MCP authorization spec defines three authentication approaches:
Transport | Auth Method | When to Use |
|---|---|---|
HTTP (remote) | OAuth 2.1 with PKCE | Production remote servers |
STDIO (local) | Environment-based credentials | Local tools, CLI agents |
HTTP (simple) | API keys | Internal/dev environments only |
OAuth 2.1 with PKCE is the standard for any remote MCP server. The spec is explicit: implementations using HTTP-based transport should conform to the OAuth 2.1 authorization flow. As of November 2025, PKCE is mandatory - not optional.
Why OAuth 2.1? What Changed from 2.0?
OAuth 2.1 consolidates a decade of security patches into a single, stricter standard. It removes the grant types that were most commonly exploited and makes PKCE a first-class requirement for all clients.
Here's what changed - and why it matters for AI agents specifically:
OAuth 2.0 vs. OAuth 2.1: Key Differences
Feature | OAuth 2.0 | OAuth 2.1 |
|---|---|---|
Implicit flow | ✅ Allowed | ❌ Removed |
Password grant (ROPC) | ✅ Allowed | ❌ Removed |
PKCE | Optional (public clients) | Mandatory (all clients) |
Redirect URI matching | Partial matching allowed | Exact string match only |
Bearer token in URL | Allowed | ❌ Forbidden |
Refresh token rotation | Optional | Required for public clients |
The implicit flow was a disaster for browser-based apps - it returned access tokens directly in the URL fragment, where they'd sit in browser history and server logs. Gone.
The password grant required users to hand their credentials directly to the client. For AI agents, that's a non-starter. Also gone.
Why Agentic AI Makes This More Complex
AI agents introduce authorization challenges that human-facing OAuth flows never had to solve:
No human in the loop. An agent can't pop up a browser window at 3 AM to re-authenticate.
Dynamic behavior. An agent might discover a new MCP server at runtime and need to register with its authorization server immediately.
No client secret. Public clients - which most AI agents are - can't securely store a client secret. PKCE fills that gap.
Runtime token management. Agents need to handle token expiry, rotation, and re-issuance without dropping the task they're executing.
OAuth 2.1 was designed with exactly these constraints in mind. That's why the MCP OAuth spec is built on it.
What Is PKCE and Why Does MCP Require It?
PKCE (Proof Key for Code Exchange) is a security extension that prevents authorization code interception attacks by binding an authorization request to the client that initiated it.
Without PKCE, a malicious app on the same device can intercept the authorization code returned in the redirect URI and exchange it for a token - before your legitimate client does. PKCE closes that window entirely.
The Attack It Prevents
On mobile and desktop environments, redirect URIs can be intercepted by other apps registered to the same custom scheme. The attacker gets the code. Without PKCE, that's enough to get a token.
With PKCE, the authorization server requires the original code_verifier to issue a token. The attacker has the code but not the verifier. The exchange fails.
How PKCE Works: The Core Mechanic
code_verifier → SHA-256 → base64url → code_challenge
The client generates a random code_verifier, hashes it to produce a code_challenge, sends the challenge with the authorization request, and then proves ownership by sending the original verifier at token exchange time.
The 5-Step PKCE Flow
01. Client generates a cryptographically random code_verifier (43–128 characters).
02. Client computes code_challenge = BASE64URL(SHA256(code_verifier)) using the S256 method.
03. Client sends the authorization request with code_challenge and code_challenge_method=S256. The verifier stays local.
04. Authorization server stores the code_challenge and returns an authorization code.
05. Client sends the authorization code plus the original code_verifier to the token endpoint. The server recomputes the hash, compares it to the stored challenge, and issues the token only if they match.
Why it's especially critical for AI agents: Most MCP clients are public clients - they can't store a client secret securely. PKCE replaces the client secret as the proof of identity. No PKCE means no security boundary. That's the whole reason the MCP auth spec made it mandatory.
The MCP OAuth 2.1 Authorization Flow, Step by Step
The full MCP OAuth flow runs from Protected Resource Metadata discovery through bearer token issuance - six steps, fully automated, no user friction after initial consent.
The June 2025 spec update made one critical architectural change: MCP servers are now defined as OAuth resource servers, not authorization servers. Token issuance is delegated to an external IdP. This separates concerns cleanly and lets you plug in Auth0, Okta, Keycloak, or any RFC 8414-compliant authorization server. (We dig into separating resource and auth servers and why the split is mandatory by design.)
The Complete Flow
01. Client connects → server returns 401 + WWW-Authenticate header
The MCP server responds to an unauthenticated request with HTTP 401 Unauthorized and a WWW-Authenticate header pointing to the Protected Resource Metadata (PRM) URL.
02. Client fetches Protected Resource Metadata (RFC 9728)
The client hits /.well-known/oauth-protected-resource (or the URL from the header). The PRM document contains the authorization_servers field - the list of IdPs that can issue tokens for this server.
03. Client discovers the authorization server (RFC 8414)
The client fetches /.well-known/oauth-authorization-server from the listed IdP to get endpoints: authorization_endpoint, token_endpoint, registration_endpoint, etc.
04. Dynamic Client Registration - optional but strongly recommended (RFC 7591)
If the client hasn't registered before, it POSTs its metadata to the registration_endpoint and receives a client_id in response. No pre-configuration. No manual setup. The agent registers itself at runtime.
05. Authorization request with PKCE code_challenge
The client redirects to the authorization endpoint with response_type=code, code_challenge, code_challenge_method=S256, and the resource parameter (RFC 8707) set to the canonical MCP server URI.
06. Code exchange → bearer token → rotation on refresh
The client exchanges the authorization code + code_verifier for an access token. The server validates the PKCE pair and issues a short-lived bearer token. On refresh, the authorization server rotates the refresh token - the old one is invalidated immediately.
ASCII Flow Diagram
MCP Client MCP Server Auth Server (IdP)
| | |
|--- GET /mcp ---------------->| |
|<-- 401 + WWW-Authenticate ---| |
| | |
|--- GET /.well-known/oauth-protected-resource -------------->|
|<-- { authorization_servers: ["https://auth.example.com"] } |
| | |
|--- GET /.well-known/oauth-authorization-server ------------>|
|<-- { authorization_endpoint, token_endpoint, ... } --------|
| | |
|--- POST /register (DCR) ---------------------------------->|
|<-- { client_id: "abc123" } --------------------------------|
| | |
|--- GET /authorize?code_challenge=...&resource=... -------->|
|<-- redirect with ?code=AUTH_CODE --------------------------|
| | |
|--- POST /token (code + code_verifier) -------------------->|
|<-- { access_token, refresh_token, expires_in } ------------|
| | |
|--- GET /mcp (Authorization: Bearer <token>) ------------> |
|<-- 200 OK + MCP response ----| |
Every HTTP request from client to server must include Authorization: Bearer <token>. The spec is explicit: tokens must never appear in URI query strings.
How to Implement OAuth 2.1 with PKCE for Your MCP Server
Before you start, you need: an OAuth 2.1-compliant authorization server (Auth0, Okta, Keycloak, or self-hosted), a registered redirect URI, and the ability to expose a /.well-known/oauth-protected-resource endpoint on your MCP server.
Step 1: Generate code_verifier and code_challenge
Python:
import base64
import hashlib
import secrets
import string
def generate_code_verifier(length: int = 64) -> str:
# Must be 43–128 characters; use URL-safe alphabet
alphabet = string.ascii_letters + string.digits + "-._~"
return ''.join(secrets.choice(alphabet) for _ in range(length))
def generate_code_challenge(code_verifier: str) -> str:
digest = hashlib.sha256(code_verifier.encode("ascii")).digest()
return base64.urlsafe_b64encode(digest).decode("ascii").rstrip("=")
code_verifier = generate_code_verifier()
code_challenge = generate_code_challenge(code_verifier)
# Store code_verifier securely - you'll need it at token exchange
JavaScript (Node.js):
import crypto from "crypto";
function generateCodeVerifier(length = 64) {
const alphabet =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
const bytes = crypto.randomBytes(length);
return Array.from(bytes, (b) => alphabet[b % alphabet.length]).join("");
}
function generateCodeChallenge(codeVerifier) {
return crypto
.createHash("sha256")
.update(codeVerifier, "ascii")
.digest("base64url");
}
const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);
Always use S256. The plain method exists in RFC 7636 but provides no real security benefit - skip it.
Step 2: Configure Your Authorization Server
Point your MCP server at an external IdP. Good options:
Auth0 - fastest setup, excellent DCR support
Okta - enterprise-grade, strong RBAC
Keycloak - self-hosted, open source, full RFC 7591 support
AWS Cognito - if you're already in the AWS ecosystem
The IdP must expose RFC 8414 metadata at /.well-known/oauth-authorization-server.
Step 3: Implement the Protected Resource Metadata Endpoint
Your MCP server must serve this at /.well-known/oauth-protected-resource:
{
"resource": "https://mcp.example.com",
"authorization_servers": ["https://auth.example.com"],
"bearer_methods_supported": ["header"],
"scopes_supported": ["mcp:read", "mcp:write", "mcp:admin"]
}
Return a WWW-Authenticate header on every 401 response:
WWW-Authenticate: Bearer realm="mcp.example.com",
resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"
Step 4: Handle Dynamic Client Registration
If your IdP supports RFC 7591, clients can self-register. Your server doesn't need to do anything extra - the client POSTs to the IdP's registration_endpoint directly.
If your IdP doesn't support DCR, you have two options: hardcode a client_id for known clients, or build a lightweight registration UI. The spec is clear that DCR is strongly recommended for AI agent use cases.
Step 5: Token Validation and Scope Enforcement
On every request, your MCP server must:
import jwt # PyJWT
def validate_token(token: str, expected_audience: str) -> dict:
# Fetch JWKS from IdP's jwks_uri
jwks_client = jwt.PyJWKClient("https://auth.example.com/.well-known/jwks.json")
signing_key = jwks_client.get_signing_key_from_jwt(token)
payload = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience=expected_audience, # MUST match your MCP server's canonical URI
)
return payload # Contains scopes, subject, expiry
Audience validation is mandatory. The spec explicitly forbids accepting tokens issued for other resources - this prevents the "confused deputy" attack where a token meant for Service A gets reused against Service B.
Step 6: Token Rotation and Short-Lived Tokens
Set access token expiry to 15 minutes or less for production.
Rotate refresh tokens on every use - the old token must be invalidated immediately.
If a refresh token is used twice (replay attack), revoke the entire token family.
Common Mistakes to Avoid
01. Using code_challenge_method=plain - it defeats the purpose of PKCE entirely.
02. Skipping audience validation - your server will accept tokens meant for other services.
03. Logging the Authorization header - you're logging bearer tokens into your observability stack.
04. Setting access token TTL to 24 hours - that's an API key with extra steps.
05. Not validating the state parameter - leaves you open to CSRF on the redirect.
06. Storing code_verifier in localStorage - use session storage or an in-memory store.
Where MCP Authentication Still Goes Wrong
The four most common MCP auth failures are all preventable - and all stem from treating AI agent auth like a simple web app login.
01. Static Tokens in Config Files
The problem: BEARER_TOKEN=eyJhbGci... in a .env file committed to git.
The fix: Use your IdP's machine-to-machine flow with short-lived tokens. Rotate on every deployment. Use a secrets manager (AWS Secrets Manager, HashiCorp Vault, Doppler).
02. Over-Permissioned Scopes
The problem: Requesting mcp:admin when the agent only needs mcp:read. One compromised token gives full access.
The fix: Define granular scopes per capability. Request the minimum scope the agent actually needs. Enforce scope checks server-side on every request - don't trust the token just because it's valid. (For a finer-grained model, see our guide to scoping access per tool.)
03. No Token Rotation
The problem: A refresh token that never expires is effectively a permanent credential. Leaked once, exploited indefinitely.
The fix: Enable refresh token rotation in your IdP. Implement token family tracking. Treat any double-use of a refresh token as a breach indicator and revoke the entire family. (We cover token rotation in MCP and how it shrinks the blast radius of a leak.)
04. No Audit Trail
The problem: You have no idea which agent accessed which tool, when, with what token, and what it returned.
The fix: Log every token validation event with: token_id, subject, scopes, resource, timestamp, outcome. Ship those logs to your SIEM. Set alerts on anomalous patterns (e.g., 50 tool calls in 10 seconds from a single agent).
MCP Auth for Enterprise: What You Actually Need
Enterprise MCP deployments need more than a working OAuth flow - they need centralized credential management, IdP integration, and audit logging that satisfies compliance requirements.
Here's the minimum viable enterprise auth stack:
Centralized credential management
All tokens issued and rotated by a single IdP (not per-service)
Secrets manager integration - no credentials in environment variables
Automated rotation on a defined schedule
IdP integration (SSO + RBAC)
Connect your MCP authorization server to your corporate IdP (Okta, Azure AD, Google Workspace) - our guide to enterprise SSO for MCP walks through the integration
Map organizational roles to MCP scopes - a "read-only analyst" role should never get
mcp:writeEnforce MFA for the initial authorization grant
Audit logging
Every token issuance, validation, and rejection logged with full context
Logs immutable and shipped off-device within seconds
Retention policy aligned to your compliance framework (SOC 2, ISO 27001, HIPAA)
Short-lived tokens + rotation
Access tokens: 15 minutes max
Refresh tokens: rotated on every use, revoked on anomaly detection
Token binding where supported (RFC 8705)
Ginger Labs handles this natively - the platform ships with built-in OAuth 2.1 flows, scope enforcement, and audit logging so you're not building auth infrastructure from scratch every time you add a new agent.
Frequently Asked Questions
Does MCP require OAuth 2.1?
For HTTP-based transports, yes - the MCP authorization spec says implementations should conform to OAuth 2.1. As of the November 2025 spec update, PKCE is mandatory for all clients using the authorization code flow. STDIO transport is exempt; it uses environment-based credentials instead.
What is PKCE in MCP authentication?
PKCE (Proof Key for Code Exchange) is a security mechanism that prevents authorization code interception attacks. The client generates a code_verifier, hashes it to a code_challenge, sends the challenge with the auth request, and proves ownership by sending the original verifier at token exchange. The MCP auth spec mandates PKCE for all clients - it replaces the client secret for public clients like AI agents.
Can I use API keys instead of OAuth for MCP?
Yes, but only for STDIO transport or internal development environments. The MCP spec explicitly states that STDIO implementations should retrieve credentials from the environment. For any remote HTTP-based MCP server in production, API keys don't provide the scoping, expiry, or rotation that OAuth 2.1 does - and they're a single point of failure if compromised.
What is Dynamic Client Registration in MCP?
Dynamic Client Registration (RFC 7591) lets an MCP client register itself with an authorization server at runtime, without any pre-configuration. The client POSTs its metadata to the registration_endpoint and receives a client_id in response. This is critical for AI agents that may encounter new MCP servers they weren't pre-configured to use. The MCP auth spec says both clients and authorization servers should support it.
What changed in the MCP auth spec in June 2025?
The June 2025 spec update (2025-06-18) redefined MCP servers as OAuth 2.1 resource servers, moving token issuance to external IdPs. Previously, MCP servers were expected to act as both resource server and authorization server - a design that created friction for enterprise adoption. The update also added mandatory support for Protected Resource Metadata (RFC 9728) and Resource Indicators (RFC 8707), requiring clients to include the resource parameter in all authorization and token requests.
How do I handle token refresh in MCP OAuth 2.1?
When an access token expires, the client sends the refresh token to the IdP's token_endpoint to get a new access token. The IdP must rotate the refresh token on every use - the old one is immediately invalidated. If the same refresh token is used twice (indicating a replay attack), the entire token family should be revoked. Set access token TTL to 15 minutes or less; never issue non-expiring tokens.
Is MCP authentication different for STDIO vs HTTP transport?
Yes - completely different approaches. HTTP transport uses the full OAuth 2.1 flow described in this guide. STDIO transport (local tools, CLI agents) should not use OAuth; instead, credentials are passed via environment variables at process startup. The MCP spec is explicit: STDIO implementations should not follow the OAuth authorization specification.
Key Takeaways
MCP OAuth is mandatory for remote HTTP servers - not optional, not "nice to have."
The MCP OAuth spec (June 2025) separates resource servers from authorization servers. Use an external IdP.
PKCE has been mandatory since November 2025. Always use
S256. Never useplain.The full flow: PRM discovery → auth server discovery → DCR → auth request with
code_challenge→ code exchange withcode_verifier→ bearer token.Dynamic client registration is how AI agents self-register at runtime. Build for it.
Short-lived tokens + refresh token rotation = the minimum viable security posture.
The four failure modes - static tokens, over-permissioned scopes, no rotation, no audit trail - are all fixable today.
Ready to Build Secure AI Agents?
If you're integrating MCP into a SaaS product and don't want to build auth infrastructure from scratch, talk to the Ginger Labs team. We've already solved the OAuth 2.1 plumbing - you focus on the agent logic.
Got a specific auth question? Reach out to the Ginger Labs team directly - we read everything.
Useful Sources
MCP Authorization Specification (2025-06-18) - Official spec, the primary reference for everything in this guide.
OAuth 2.1 Draft Spec - IETF Datatracker - The current OAuth 2.1 draft (draft-ietf-oauth-v2-1-13).
RFC 7636 - PKCE - The original PKCE specification.
RFC 7591 - OAuth 2.0 Dynamic Client Registration - Dynamic Client Registration protocol.
RFC 9728 - OAuth 2.0 Protected Resource Metadata - How MCP servers advertise their authorization servers.
RFC 8707 - Resource Indicators for OAuth 2.0 - The
resourceparameter, mandatory in MCP OAuth flows.OAuth 2.1 Overview - oauth.net - Accessible summary of what changed from 2.0.
Auth0 - Authorization Code Flow with PKCE - Practical implementation reference.
Keep reading
MCP Resource Server vs Authorization Server: Why the Separation Matters
MCP's auth spec draws a hard line between the Resource Server and the Authorization Server. Here's what each role does, how the OAuth 2.1 flow works end-to-end, and why the split is smart.
How to Audit Third-Party MCP Servers Using mcp-scan
A step-by-step guide to auditing third-party MCP servers with mcp-scan — installation, CLI commands, threat types, tool pinning, CI/CD integration, and security best practices.
MCP Per-Tool Kill Switches: Disable Individual Tools Without Server Downtime
Running 91 GitHub MCP tools can burn 46,000 tokens before your LLM writes a line. Here's how to disable individual MCP tools at runtime — no server restart required.



