createmcps.com

Guide

How to add OAuth to an MCP server

Last verified: 10 August 2026 · spec 2026-07-28 · ruleset 1.4.2

Your MCP server is an OAuth 2.1 resource server — it validates tokens, it does not issue them. Four things to build: a 401 challenge that points somewhere, RFC 9728 Protected Resource Metadata, audience validation on every request, and correct 403 handling for scope failures.

Get the roles right first

The single most expensive wrong turn here is building an authorization server. MCP does not ask you to. Three parties, three jobs:

PartyDoesYou build it?
MCP clientRuns the flow, validates iss, sends the tokenNo
Authorization serverAuthenticates users, issues tokens, publishes metadataNo — use an existing one
Your MCP serverChallenges, publishes PRM, validates tokensYes — this page
Our recommendation, not a spec quote: if you already have an identity provider — Auth0, Okta, Entra, Keycloak, Cognito, your own OIDC deployment — that is your authorization server. Point at it. The work below is only the resource-server half, and it is a few hundred lines rather than a subsystem.

1. Return a 401 that tells the client where to go

MCP-AUT-001

An unauthenticated request must be refused in a way the client can act on. A bare 401 is a dead end; the header is the whole point.

HTTP/1.1 401 Unauthorized. WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource".
MCP-AUT-001 MUST · spec 2026-07-28
The challenge
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"
Express
const PRM_URL = "https://mcp.example.com/.well-known/oauth-protected-resource"

function challenge(res, extra = "") {
  res.setHeader(
    "WWW-Authenticate",
    `Bearer resource_metadata="${PRM_URL}"${extra}`
  )
  return res.status(401).json({
    jsonrpc: "2.0", id: null,
    error: { code: -32001, message: "Unauthorized" }
  })
}

app.post("/mcp", (req, res, next) => {
  const auth = req.headers.authorization
  if (!auth?.startsWith("Bearer ")) return challenge(res)
  next()
})
MCP servers SHOULD include a scope parameter in the WWW-Authenticate header as defined in RFC 6750 Section 3.
MCP-AUT-006 SHOULD · spec 2026-07-28

Adding scope to the challenge is SHOULD-level and cheap: it tells the client which scopes to request rather than making it guess and retry.

2. Serve RFC 9728 Protected Resource Metadata

MCP-AUT-002MCP-AUT-003
MCP servers MUST implement OAuth 2.0 Protected Resource Metadata (RFC9728).
MCP-AUT-002 MUST · spec 2026-07-28
/.well-known/oauth-protected-resource
{
  "resource": "https://mcp.example.com",
  "authorization_servers": [
    "https://auth.example.com"
  ],
  "scopes_supported": ["mcp:read", "mcp:write"],
  "bearer_methods_supported": ["header"]
}
The resource parameter MUST identify the MCP server that the client intends to use the token with, using the canonical URI as defined in RFC 8707 Section 2.
MCP-AUT-003 MUST · spec 2026-07-28

resource must be the canonical URI of the server the token is for — not a display name, not a path variant. A client uses this value as the resource parameter when requesting a token, and your audience check in step 3 compares against the same value. If those two disagree, every token you issue is rejected by your own validation.

MCP Servers (Protected Resources) SHOULD NOT include offline_access in WWW-Authenticate scope or Protected Resource Metadata scopes_supported.
MCP-AUT-009 SHOULD_NOT · spec 2026-07-28
Don't advertise offline_access: It is a SHOULD NOT for MCP resource servers, in both scopes_supported and the WWW-Authenticate challenge. Refresh-token lifetime is the authorization server's concern; advertising it from a resource server invites clients to request long-lived access you have no way to revoke.

3. Validate the token — audience first

MCP-AUT-008

This is the check that most matters and is most often skipped, because a server that merely verifies a signature appears to work.

MCP servers MUST validate that access tokens were issued specifically for them as the intended audience, according to RFC 8707 Section 2.
MCP-AUT-008 MUST · spec 2026-07-28
What skipping it costs: Your authorization server issues tokens for several services. Without an audience check, a token minted for the billing API — by a client that legitimately holds it — is accepted by your MCP server. Every other client of your identity provider becomes a client of your MCP server.
Validating with a JWT access token
import { createRemoteJWKSet, jwtVerify } from "jose"

const JWKS = createRemoteJWKSet(
  new URL("https://auth.example.com/.well-known/jwks.json")
)

const CANONICAL_URI = "https://mcp.example.com"   // === PRM "resource"

async function verify(token) {
  const { payload } = await jwtVerify(token, JWKS, {
    issuer:   "https://auth.example.com",
    audience: CANONICAL_URI,        // ← the RFC 8707 check
  })
  return payload
}
Or with token introspection, for opaque tokens
const res = await fetch("https://auth.example.com/oauth/introspect", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({ token }),
})
const info = await res.json()

if (!info.active) throw new Error("inactive token")

// aud may be a string or an array — handle both
const aud = Array.isArray(info.aud) ? info.aud : [info.aud]
if (!aud.includes(CANONICAL_URI)) throw new Error("wrong audience")

4. Handle scope failures with 403, not another 401

MCP-AUT-007
The server SHOULD respond with HTTP 403 Forbidden and a WWW-Authenticate header with error="insufficient_scope".
MCP-AUT-007 SHOULD · spec 2026-07-28
The distinction clients depend on
// No token, or an invalid one → authenticate
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="…"

// Valid token, insufficient scope → get different scopes
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope", scope="mcp:write"

Returning 401 for a scope failure sends the client back through a full authentication it does not need, and it will usually arrive with the same insufficient scopes — a loop rather than a recovery.

5. Check what your authorization server advertises

MCP-AUT-004MCP-AUT-010

Three properties of your authorization server determine whether MCP clients can work with it at all. None are yours to implement, but all are yours to check before you name it in your PRM.

MCP authorization servers MUST provide at least one of: OAuth 2.0 Authorization Server Metadata (RFC8414) or OpenID Connect Discovery 1.0.
MCP-AUT-004 MUST · spec 2026-07-28
Authorization servers providing OpenID Connect Discovery 1.0 MUST include code_challenge_methods_supported in their metadata to ensure MCP compatibility.
MCP-AUT-010 MUST · spec 2026-07-28
Authorization servers and MCP clients SHOULD support OAuth Client ID Metadata Documents. Dynamic Client Registration is deprecated and retained for backwards compatibility.
MCP-AUT-011 SHOULD · spec 2026-07-28
One request tells you all three
curl -s https://auth.example.com/.well-known/oauth-authorization-server | jq '{
  pkce:  .code_challenge_methods_supported,
  iss:   .authorization_response_iss_parameter_supported,
  cimd:  .client_id_metadata_document_supported
}'

# Want:
#   pkce  contains "S256"
#   iss   true          ← RFC 9207
#   cimd  true          ← DCR is deprecated

The iss one is worth understanding rather than just checking — it closes the authorization-server mix-up attack, and it is a client obligation your server can only enable. Full explanation.

What not to build

  • An authorization server. Token issuance, PKCE, key rotation, client registration — none of it is asked of an MCP server, and all of it is security-critical.
  • Session-based auth. There are no sessions in 2026-07-28. Credentials arrive on every request or not at all — see statelessness.
  • Dynamic Client Registration as the plan. It is deprecated and retained for backwards compatibility. Target Client ID Metadata Documents.
  • Bearer tokens over plain HTTP. OAuth URLs must use HTTPS, loopback redirects excepted. We check this as MCP-SEC-002.

The 11 authorization rules we check

RuleWhat we checkLevelIf it fails
MCP-AUT-001Unauthenticated request returns 401 with WWW-Authenticate resource_metadataMUSTfail
MCP-AUT-002RFC 9728 Protected Resource Metadata is served and validMUSTfail
MCP-AUT-003PRM resource equals the canonical server URIMUSTfail
MCP-AUT-004Each listed authorization server exposes RFC 8414 or OIDC discovery metadataMUSTfail
MCP-AUT-008Tokens with a foreign audience are rejectedMUSTfail
MCP-AUT-010AS advertises S256 in code_challenge_methods_supportedMUSTfail
MCP-AUT-005AS advertises authorization_response_iss_parameter_supported: trueSHOULDwarn
MCP-AUT-006WWW-Authenticate includes a scope parameterSHOULDwarn
MCP-AUT-007Insufficient scope returns 403 with error="insufficient_scope"SHOULDwarn
MCP-AUT-011AS supports Client ID Metadata Documents, not DCR aloneSHOULDwarn
MCP-AUT-009offline_access absent from scopes_supported / challenge scopeSHOULD_NOTinfo

All eleven carry verbatim spec sentences. Security-relevant failures cap a report grade at D.

Check the whole chain from outside

The validator walks the same path a client does — 401 challenge, Protected Resource Metadata, each authorization server's metadata — and reports all eleven rules, which is the fastest way to find a canonical-URI mismatch.

Validate a server →

Frequently asked

How do I add OAuth to an MCP server?

Treat your server as an OAuth 2.1 resource server, not an authorization server. Return HTTP 401 with a WWW-Authenticate header pointing at your Protected Resource Metadata; serve that metadata at /.well-known/oauth-protected-resource per RFC 9728, naming the authorization servers you trust; then validate on every request that the access token was issued for your server as its audience.

Do I need to build my own OAuth authorization server?

Almost certainly not. An MCP server is a resource server: it validates tokens, it does not issue them. Use an existing authorization server — your identity provider, or a hosted one — and name it in your Protected Resource Metadata. Building an authorization server means owning token issuance, PKCE, key rotation and client registration, none of which MCP asks of you.

What is RFC 9728 Protected Resource Metadata?

A JSON document at /.well-known/oauth-protected-resource that tells a client which authorization servers can issue tokens for your server, and what the canonical identifier for your server is. MCP servers MUST implement it — it is how a client discovers where to authenticate after receiving your 401.

Why must I validate the token audience?

Because without it, a token issued for a different service by the same authorization server would be accepted by yours. The spec requires servers to validate that access tokens were issued specifically for them as the intended audience, per RFC 8707. Skipping this turns any other client of your identity provider into a client of your MCP server.

What status code should an MCP server return for insufficient scope?

403 Forbidden with a WWW-Authenticate header carrying error="insufficient_scope", rather than another 401. The distinction matters to clients: 401 means authenticate, 403 with insufficient_scope means you are authenticated but need different scopes, which is a different recovery path.

Is Dynamic Client Registration still recommended?

No. The 2026-07-28 spec says authorization servers and MCP clients SHOULD support OAuth Client ID Metadata Documents, and that Dynamic Client Registration is deprecated and retained only for backwards compatibility. New work should target Client ID Metadata Documents.