Remote MCP OAuth is not "add login, then trust the token." It is a multi-party authorization protocol in which an MCP server acts as an OAuth protected resource, discovers or selects an authorization server, validates a token intended for itself, and still makes an application decision for every tool call.
The 2026-07-28 MCP specification materially changes older integration advice. Client ID Metadata Documents (CIMD) are now preferred for clients without a prior relationship, Dynamic Client Registration (DCR) is deprecated, the resource parameter is mandatory, authorization-response issuer binding is explicit, and runtime scope challenges define a step-up flow.
Key Takeaways
- Model the MCP server, authorization server, client, user, and downstream APIs as separate principals and resources.
- Discover authorization servers through RFC 9728 Protected Resource Metadata; do not infer them from a user-supplied URL.
- Prefer pre-registration, then CIMD, and use DCR only as a compatibility fallback.
- Bind an authorization response and stored client credentials to the exact authorization-server issuer.
- Include
resourcein authorization and token requests, then validate the token audience at the MCP server. - Treat Scope as a coarse capability; enforce tenant, subject, object, action, purpose, and side-effect policy separately.
- Return a complete
insufficient_scopechallenge for step-up authorization rather than over-requesting every scope initially. - Never pass an MCP access token through to an upstream or downstream API.
- Protect every metadata fetch against SSRF, redirects, DNS rebinding, and private-address targets.
Start With the Trust Topology
The trust topology determines which credential is valid at each boundary:
resource owner
-> MCP client
-> MCP protected resource
-> downstream API (optional)
authorization server
-> issues token for the MCP resource
Document these values before choosing an SDK:
| Decision | Required record |
|---|---|
| MCP resource identity | Canonical HTTPS URI used in metadata and resource |
| Authorization server | Validated issuer and metadata origin |
| Client registration | Mechanism, client ID, redirect URIs, issuer binding |
| Principal mapping | Subject, tenant, client, and trusted claim source |
| Tool policy | Required scopes, object rule, side-effect class |
| Downstream access | Separate audience, credential type, and delegation policy |
The authorization server issues evidence. The MCP server decides whether that evidence permits the requested operation. A signed token for another API remains invalid at the MCP server.
The 2026-07-28 Authorization Flow
The normative flow has five security-relevant stages:
Authorization applies to HTTP transports. The MCP specification advises stdio implementations to obtain credentials from the environment rather than applying this remote OAuth flow.
Discover the Protected Resource Safely
An MCP resource publishes RFC 9728 metadata that names its canonical resource and authorization servers:
{
"resource": "https://mcp.example.com/mcp",
"authorization_servers": ["https://login.example.com"],
"scopes_supported": ["files:read"]
}
An unauthenticated request can point the client to this document:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
scope="files:read"
These URLs are network inputs, not trusted configuration. A server-side client must validate every initial URL and redirect, require HTTPS outside explicit loopback development, reject private and reserved addresses, and account for DNS rebinding. An egress proxy provides a stronger boundary than custom string checks.
Protected-resource metadata tells the client where to continue; it does not prove that a particular user or client has permission.
Register Clients in the Correct Order
The current MCP priority is:
- Use pre-registered client information when the client and authorization server already have a relationship.
- Use CIMD when the authorization server advertises
client_id_metadata_document_supported. - Use DCR only when it is supported and the first two options are unavailable.
- Ask the user for registered client information if no automated mechanism works.
A CIMD client ID is an HTTPS URL with a path. The document must include matching client_id, client_name, and redirect_uris. The authorization server must fetch and validate the document, exactly match redirect URIs, cache under HTTP rules, and defend the fetch against SSRF.
DCR is retained for backwards compatibility, not recommended as the default for new integrations. Persisted DCR or pre-registered credentials must be keyed by the issuer that created them. If protected-resource metadata points to a new issuer, do not silently reuse credentials from the previous issuer.
Bind the Response to the Authorization Server
OAuth mix-up attacks try to make a client send a code or token from an honest authorization server to an attacker-controlled one. The MCP client must record the selected issuer beside the PKCE verifier and state before redirecting the user.
When the authorization response contains iss, compare it to the recorded issuer using exact string comparison. If metadata advertises authorization_response_iss_parameter_supported=true, absence of iss is an error. Do not normalize scheme, host case, ports, trailing slashes, or percent encoding before this comparison.
Validate state as a single-use, short-lived value bound to the initiating browser session. PKCE with S256 protects the code exchange, but it does not replace issuer validation, exact redirect-URI matching, or resource authorization.
Bind Tokens to the MCP Resource
MCP clients must include the RFC 8707 resource parameter in both authorization and token requests:
resource=https%3A%2F%2Fmcp.example.com%2Fmcp
Use the most specific canonical URI that identifies the MCP server. The resource must be an absolute URI with a scheme and no fragment. Consistency matters: accidental path or trailing-slash differences can produce tokens for a different resource identifier.
The MCP server must then validate:
- cryptographic signature or introspection result;
- allowed algorithm and key;
- exact trusted issuer;
- intended audience or equivalent resource binding;
- expiration, not-before time, and bounded clock skew;
- subject and tenant claims required by local policy;
- client identity when the operation depends on it;
- scopes for the requested capability.
JWT parsing alone is not verification. Use a maintained OAuth/JWT library or authorization-server introspection, configure the expected values explicitly, and fail closed on unknown keys or unavailable validation dependencies.
Enforce Tool and Object Policy in Go
After token verification, normalize trusted claims into a small principal. The following standard-library program demonstrates the application policy that runs after cryptographic verification. It rejects cross-tenant access, missing scopes, unknown resources, and unapproved external effects.
package main
import (
"errors"
"fmt"
"os"
)
type principal struct {
Subject string
Tenant string
Scopes map[string]bool
}
type toolCall struct {
Name string
ResourceID string
Effect string
}
type policy struct {
RequiredScope string
Owners map[string]string
ExternalTools map[string]bool
}
func authorize(p principal, tenant string, call toolCall, rule policy) (string, error) {
if !p.Scopes[rule.RequiredScope] {
return "deny", errors.New("insufficient_scope")
}
if p.Tenant != tenant {
return "deny", errors.New("tenant_mismatch")
}
if owner, ok := rule.Owners[call.ResourceID]; !ok || owner != p.Subject {
return "deny", errors.New("resource_not_authorized")
}
if call.Effect == "destructive" {
return "confirm", nil
}
if call.Effect == "external_write" && !rule.ExternalTools[call.Name] {
return "deny", errors.New("external_effect_not_allowed")
}
return "allow", nil
}
func main() {
p := principal{
Subject: "user-42",
Tenant: "tenant-a",
Scopes: map[string]bool{"files:write": true},
}
call := toolCall{
Name: "update_document",
ResourceID: "doc-7",
Effect: "external_write",
}
rule := policy{
RequiredScope: "files:write",
Owners: map[string]string{"doc-7": "user-42"},
ExternalTools: map[string]bool{"update_document": true},
}
decision, err := authorize(p, "tenant-a", call, rule)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println(decision)
}
Never trust tenant_id, owner_id, role, destination, price, or approval state supplied in model arguments. Resolve identity from the verified principal and ownership from an authoritative policy or resource service.
Request Scopes Incrementally
The initial WWW-Authenticate challenge may include the scopes needed for basic access. The client should treat scopes in that challenge as authoritative for the current request; it must not assume they are a subset of scopes_supported.
When an operation needs more authority, the server should return:
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope",
scope="files:write approvals:create",
resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"
Return all scopes required for that operation in one challenge. The client should request the union of previously granted and newly required scopes so a successful step-up does not discard earlier access. The user must see what additional authority is being requested before the client retries.
Step-up authorization is not object authorization. files:write can permit a capability while local policy still denies writes to another tenant's document.
Never Pass Tokens Through
The MCP server must accept only access tokens intended for itself and must not forward that token unchanged to a downstream API. Token passthrough creates four failures:
- the downstream audience is wrong;
- MCP-level policy and rate limits can be bypassed;
- audit logs cannot distinguish the intended client boundary;
- a stolen token may be replayed across services.
For downstream access, obtain a separate token for that API:
| Downstream need | Credential pattern | Required check |
|---|---|---|
| User-owned data | Provider-supported delegated exchange | MCP user and object authorization before exchange |
| Service-owned operation | Narrow workload identity | Server-side policy and fixed downstream audience |
| No delegated support | Service credential plus explicit user policy | Never impersonate the user |
Do not let the model choose the downstream audience, scopes, or credential. Keep assertions and refresh tokens out of logs.
Defend the OAuth Fetch Surface
MCP authorization adds several attacker-influenced fetches: resource_metadata, authorization_servers, authorization endpoints, token endpoints, JWKS, and CIMD documents. Apply one policy to every hop:
- Parse URLs with a standards-compliant URL library.
- Require HTTPS except explicit loopback development cases.
- Resolve and block private, loopback, link-local, and reserved destinations.
- Revalidate every redirect target.
- Pin the resolved destination or route through an egress proxy to reduce DNS TOCTOU risk.
- Bound response size, timeout, redirect count, and content type.
- Cache validated metadata with an expiry and issuer key.
- Never reflect fetched error bodies or metadata to users without sanitization.
CIMD moves a metadata fetch to the authorization server, so the server must apply the same SSRF discipline there.
Production Test Matrix
Test protocol mechanics and business authorization independently:
| Scenario | Expected result |
|---|---|
| Missing token | 401 with valid resource_metadata |
| Wrong issuer or audience | Reject before tool dispatch |
| Missing PKCE support | Client refuses authorization |
Response iss mismatch |
Client rejects code and error details |
| Reused state or code | Reject without token exchange |
| Token lacks operation scope | 403 insufficient_scope with complete scope set |
| Valid scope, wrong tenant or owner | Deny without revealing object existence |
| Token issued for downstream API | Reject; never pass through |
| Metadata points to private IP | Block before fetch |
| DNS or redirect changes destination | Revalidate and block |
| Unknown signing key | One bounded refresh, then fail closed |
| Destructive call after step-up | Require a separately bound confirmation |
Record issuer, resource, client ID, subject hash, policy version, tool, decision, and request ID. Do not record bearer tokens, authorization codes, PKCE verifiers, or raw sensitive arguments.
Release Checklist
- Pin the MCP specification revision used by the client and server.
- Define one canonical MCP resource URI.
- Validate Protected Resource Metadata and authorization-server metadata.
- Implement pre-registration and CIMD before DCR for new clients.
- Bind stored credentials and authorization responses to the exact issuer.
- Include
resourcein both authorization and token requests. - Require PKCE
S256, exact redirect URIs, and single-use state. - Validate tokens for this MCP resource and reject token passthrough.
- Enforce tenant, object, purpose, and side-effect policy outside the model.
- Implement complete step-up Scope challenges.
- Apply SSRF controls to metadata, CIMD, JWKS, and redirects.
- Run the failure matrix with synthetic identities before enabling high-impact tools.
Conclusion
Enterprise MCP OAuth succeeds when each authority remains separate: discovery identifies a trusted authorization server, registration identifies the client, PKCE and issuer binding protect the authorization response, resource binds the token to the MCP server, and application policy authorizes the exact tool effect. The 2026-07-28 specification makes those boundaries more explicit. Implement them as protocol invariants, not as instructions to the model.
Primary Sources
- MCP Authorization, 2026-07-28
- MCP Client Registration, 2026-07-28
- MCP Authorization Security Considerations
- MCP Security Best Practices
- RFC 8707: Resource Indicators for OAuth 2.0
- RFC 9207: Authorization Server Issuer Identification
- RFC 9728: OAuth 2.0 Protected Resource Metadata
- RFC 9700: OAuth 2.0 Security Best Current Practice