mcp-server-auth
Make an MCP server authenticate its callers correctly. The spec profiles OAuth
2.1 precisely; the documented failures this skill fixes are servers that invent
bearer-token schemes, pass client tokens through to downstream APIs, skip
discovery metadata (so no client can connect), or accept any validly-signed
token regardless of audience.
Targets spec revision 2025-11-25 (verified 2026-07-20):
https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization.
When NOT to use
- Threat modeling / injection / sandboxing →
mcp-server-security (auth is one
control; that skill owns the attack taxonomy).
- Transport mechanics (Origin validation, sessions, headers) →
mcp-server-implementation. Hosting/gateways → mcp-server-deployment.
- OAuth for ordinary web/mobile apps — this is the MCP profile specifically.
Workflow
1. Pick the auth model by transport
- stdio (local subprocess): the OAuth spec does NOT apply — read
credentials from the environment, scope the underlying API key minimally,
never put secrets in client-config
args.
- Streamable HTTP: authorization is OPTIONAL in the spec, and HTTP
transports SHOULD conform to the OAuth 2.1 profile. Once you implement it,
the MUSTs below are normative. Strong recommendation: any internet-exposed
server implements it — scans keep finding thousands of open MCP endpoints.
(Many secondary sources say "OAuth is mandatory for remote" — that is not
what the spec says; don't repeat it.)
- A static shared API key is not a substitute for identity-scoped tokens: no
per-user scoping, no revocation story, total blast radius. If a gateway
terminates auth for you, the requirements below move there.
2. Be a resource server, not an authorization server
Your MCP server validates tokens; an AS (Auth0/Okta/Keycloak/Entra or your
IdP) issues them. Building your own AS is almost always the wrong call —
delegate. If the server must broker access to a third-party API, see step 6.
3. Publish discovery metadata (or no client can connect)
- RFC 9728 Protected Resource Metadata (MUST): serve
/.well-known/oauth-protected-resource with your canonical resource URI
and authorization_servers; AND/OR return
WWW-Authenticate: Bearer resource_metadata="<url>" on 401 (clients try
header first, then well-known — support both).
- The AS must offer RFC 8414 metadata or OIDC Discovery (2025-11-25 accepts
either; clients must support both).
- Every 401/403 carries the right challenge — see the exact HTTP exchanges in
references/oauth-wire.md.
4. Validate every token, every request
Authorization: Bearer <token> on every request — sessions never carry
auth (spec MUST NOT). Tokens never in query strings.
- Audience validation (RFC 8707): accept only tokens whose audience is
your canonical resource URI; a validly-signed, unexpired token for another
resource → 401. Clients send
resource= on authorization and token
requests; you enforce it on receipt.
- Validate locally: cache the AS JWKS and verify signature/exp/aud in-process
(sub-ms) instead of a network introspection per call; re-fetch on key
rotation.
- Insufficient scope at runtime → 403 with
error="insufficient_scope" +
the required scope in WWW-Authenticate (SEP-835) so clients can step up
incrementally. Publish minimal scopes; separate read from write.
- PKCE: clients MUST use S256 and MUST refuse an AS whose metadata lacks
code_challenge_methods_supported — when you operate the AS config, make
sure that field is present or every spec-conforming client will refuse you.
5. Client registration: CIMD first
Priority order (2025-11-25): pre-registered credentials → Client ID Metadata
Documents (the client_id is an HTTPS URL serving the client's metadata;
recommended when the AS advertises client_id_metadata_document_supported) →
RFC 7591 Dynamic Client Registration only as back-compat fallback (deprecated
in the next revision) → prompt the user. If you allow DCR: allowlist/exact
redirect URIs, no wildcards — open DCR + arbitrary redirect URI is a
disclosed one-click account-takeover class.
6. Never pass tokens through; proxy with consent
- Token passthrough is forbidden: never accept a token not issued for your
server, never forward the client's token upstream. Downstream APIs are
called with credentials the server owns (or an explicit token exchange).
- If your server fronts a third-party AS with a static client_id (the
"OAuth proxy" shape): obtain per-client consent before every forward,
exact-match redirect URIs, bind and verify
state — the confused-deputy
writeups that broke real products in 2025 all violated one of these.
7. Verify the flow end to end
python3 "${CLAUDE_SKILL_DIR}/scripts/check_auth_endpoints.py" https://your-host/mcp
probes: unauthenticated request → 401 + parseable challenge; PRM well-known
resolves and matches the server URL; AS metadata reachable with S256
advertised. Then run a real client (MCP Inspector supports the OAuth flow) and
confirm a wrong-audience token is rejected. Conformance suite:
npx @modelcontextprotocol/conformance server --url ....
Output spec
A server that: serves RFC 9728 PRM (+ challenge headers), validates
signature/expiry/audience on every request via cached JWKS, returns the
401/403 contract with correct WWW-Authenticate, scopes split read/write with
step-up, registers clients via CIMD (DCR only as fallback, allowlisted),
touches downstream APIs only with its own credentials, and passes the probe
script + a live client flow.
Gotchas
- Sessions are not auth — an
Mcp-Session-Id proves nothing; validate the
bearer token on every request even mid-session.
- SSRF in discovery: SDK clients have fetched attacker-controlled
resource_metadata URLs before validating them (disclosed against both
major SDKs) — if you implement client-side discovery anywhere, validate
scheme/host before fetching; as a server, never reflect user input into
challenge URLs. Full taxonomy: mcp-server-security.
- Issuer validation: verify AS metadata
issuer matches where you fetched
it (RFC 8414 §3.3); mixed-endpoint metadata (legit authorization_endpoint,
attacker token_endpoint) is a disclosed credential-theft class. The
2026-07-28 revision adds RFC 9207 iss checks and requires client
credentials keyed per-issuer — align now.
- Access tokens short-lived (minutes-hours); refresh handled by the client and
AS, not you. Log auth failures with reasons; never log tokens.
- The auth extension ecosystem (
modelcontextprotocol/ext-auth) hosts
optional additions — check it before inventing an extension.
Pointers
references/oauth-wire.md — the exact HTTP exchanges (401/403 challenges,
PRM and AS metadata JSON, CIMD document, token validation pseudocode, RFC
map, IdP wiring notes).
scripts/check_auth_endpoints.py — deterministic discovery/challenge probe
(stdlib-only; non-zero exit on failures).
1---2name: mcp-server-auth3description: Implements MCP server authorization - OAuth 2.1 resource server role, RFC 9728/8414 discovery, PKCE, RFC 8707 audience validation, scopes, client registration. Use when adding auth, OAuth, tokens, or credentials to an MCP server, or debugging 401/403/discovery failures. Not for general threat hardening, transport wiring, or non-MCP app login.4---56# mcp-server-auth78Make an MCP server authenticate its callers correctly. The spec profiles OAuth92.1 precisely; the documented failures this skill fixes are servers that invent10bearer-token schemes, pass client tokens through to downstream APIs, skip11discovery metadata (so no client can connect), or accept any validly-signed12token regardless of audience.1314Targets spec revision **2025-11-25** (verified 2026-07-20):15<https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization>.1617## When NOT to use1819- Threat modeling / injection / sandboxing → `mcp-server-security` (auth is one20 control; that skill owns the attack taxonomy).21- Transport mechanics (Origin validation, sessions, headers) →22 `mcp-server-implementation`. Hosting/gateways → `mcp-server-deployment`.23- OAuth for ordinary web/mobile apps — this is the MCP profile specifically.2425## Workflow2627### 1. Pick the auth model by transport2829- **stdio (local subprocess)**: the OAuth spec does NOT apply — read30 credentials from the environment, scope the underlying API key minimally,31 never put secrets in client-config `args`.32- **Streamable HTTP**: authorization is OPTIONAL in the spec, and HTTP33 transports SHOULD conform to the OAuth 2.1 profile. Once you implement it,34 the MUSTs below are normative. Strong recommendation: any internet-exposed35 server implements it — scans keep finding thousands of open MCP endpoints.36 (Many secondary sources say "OAuth is mandatory for remote" — that is not37 what the spec says; don't repeat it.)38- A static shared API key is not a substitute for identity-scoped tokens: no39 per-user scoping, no revocation story, total blast radius. If a gateway40 terminates auth for you, the requirements below move there.4142### 2. Be a resource server, not an authorization server4344Your MCP server **validates** tokens; an AS (Auth0/Okta/Keycloak/Entra or your45IdP) issues them. Building your own AS is almost always the wrong call —46delegate. If the server must broker access to a third-party API, see step 6.4748### 3. Publish discovery metadata (or no client can connect)4950- **RFC 9728 Protected Resource Metadata** (MUST): serve51 `/.well-known/oauth-protected-resource` with your canonical `resource` URI52 and `authorization_servers`; AND/OR return53 `WWW-Authenticate: Bearer resource_metadata="<url>"` on 401 (clients try54 header first, then well-known — support both).55- The AS must offer RFC 8414 metadata or OIDC Discovery (2025-11-25 accepts56 either; clients must support both).57- Every 401/403 carries the right challenge — see the exact HTTP exchanges in58 `references/oauth-wire.md`.5960### 4. Validate every token, every request6162- `Authorization: Bearer <token>` on **every** request — sessions never carry63 auth (spec MUST NOT). Tokens never in query strings.64- **Audience validation (RFC 8707)**: accept only tokens whose audience is65 your canonical resource URI; a validly-signed, unexpired token for another66 resource → **401**. Clients send `resource=` on authorization and token67 requests; you enforce it on receipt.68- Validate locally: cache the AS JWKS and verify signature/exp/aud in-process69 (sub-ms) instead of a network introspection per call; re-fetch on key70 rotation.71- Insufficient scope at runtime → **403** with `error="insufficient_scope"` +72 the required `scope` in `WWW-Authenticate` (SEP-835) so clients can step up73 incrementally. Publish minimal scopes; separate read from write.74- PKCE: clients MUST use S256 and MUST refuse an AS whose metadata lacks75 `code_challenge_methods_supported` — when you operate the AS config, make76 sure that field is present or every spec-conforming client will refuse you.7778### 5. Client registration: CIMD first7980Priority order (2025-11-25): pre-registered credentials → **Client ID Metadata81Documents** (the client_id is an HTTPS URL serving the client's metadata;82recommended when the AS advertises `client_id_metadata_document_supported`) →83RFC 7591 Dynamic Client Registration only as back-compat fallback (deprecated84in the next revision) → prompt the user. If you allow DCR: allowlist/exact85redirect URIs, no wildcards — open DCR + arbitrary redirect URI is a86disclosed one-click account-takeover class.8788### 6. Never pass tokens through; proxy with consent8990- **Token passthrough is forbidden**: never accept a token not issued for your91 server, never forward the client's token upstream. Downstream APIs are92 called with credentials the *server* owns (or an explicit token exchange).93- If your server fronts a third-party AS with a static client_id (the94 "OAuth proxy" shape): obtain **per-client consent** before every forward,95 exact-match redirect URIs, bind and verify `state` — the confused-deputy96 writeups that broke real products in 2025 all violated one of these.9798### 7. Verify the flow end to end99100```bash101python3 "${CLAUDE_SKILL_DIR}/scripts/check_auth_endpoints.py" https://your-host/mcp102```103104probes: unauthenticated request → 401 + parseable challenge; PRM well-known105resolves and matches the server URL; AS metadata reachable with S256106advertised. Then run a real client (MCP Inspector supports the OAuth flow) and107confirm a wrong-audience token is rejected. Conformance suite:108`npx @modelcontextprotocol/conformance server --url ...`.109110## Output spec111112A server that: serves RFC 9728 PRM (+ challenge headers), validates113signature/expiry/**audience** on every request via cached JWKS, returns the114401/403 contract with correct `WWW-Authenticate`, scopes split read/write with115step-up, registers clients via CIMD (DCR only as fallback, allowlisted),116touches downstream APIs only with its own credentials, and passes the probe117script + a live client flow.118119## Gotchas120121- **Sessions are not auth** — an `Mcp-Session-Id` proves nothing; validate the122 bearer token on every request even mid-session.123- **SSRF in discovery**: SDK clients have fetched attacker-controlled124 `resource_metadata` URLs before validating them (disclosed against both125 major SDKs) — if you implement client-side discovery anywhere, validate126 scheme/host before fetching; as a server, never reflect user input into127 challenge URLs. Full taxonomy: `mcp-server-security`.128- **Issuer validation**: verify AS metadata `issuer` matches where you fetched129 it (RFC 8414 §3.3); mixed-endpoint metadata (legit authorization_endpoint,130 attacker token_endpoint) is a disclosed credential-theft class. The131 2026-07-28 revision adds RFC 9207 `iss` checks and requires client132 credentials keyed per-issuer — align now.133- Access tokens short-lived (minutes-hours); refresh handled by the client and134 AS, not you. Log auth failures with reasons; never log tokens.135- The auth extension ecosystem (`modelcontextprotocol/ext-auth`) hosts136 optional additions — check it before inventing an extension.137138## Pointers139140- `references/oauth-wire.md` — the exact HTTP exchanges (401/403 challenges,141 PRM and AS metadata JSON, CIMD document, token validation pseudocode, RFC142 map, IdP wiring notes).143- `scripts/check_auth_endpoints.py` — deterministic discovery/challenge probe144 (stdlib-only; non-zero exit on failures).