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.

MK

Mohammed Kafeel

Machine Learning Researcher

June 17, 202617 min read
On this page

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:write

  • Enforce 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 use plain.

  • The full flow: PRM discovery → auth server discovery → DCR → auth request with code_challenge → code exchange with code_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