Add a conceptual guide for production OAuth authorization - #1801
Add a conceptual guide for production OAuth authorization#1801anneheartrecord wants to merge 1 commit into
Conversation
Documents how to protect an MCP server with OAuth 2.0 against an existing identity provider, rather than standing up an authorization server the way the TestOAuthServer fixture does. Covers the resource server / authorization server split, what to require of an authorization server, JwtBearer plus AddMcp configuration, protected resource metadata behind a reverse proxy, per-request metadata for multi-tenant servers, endpoint and per-primitive enforcement with AddAuthorizationFilters, scope-based policies, and how claims are refreshed relative to sessions.
jeffhandley
left a comment
There was a problem hiding this comment.
First off, this is a really nice addition. The production framing is exactly what''s been missing. The resource-server versus authorization-server split, the "don''t copy the TestOAuthServer" warning, the proxy and forwarded-headers section, and the checklist at the end all read like they came from real deployment experience. Thank you for tackling this and putting together a comprehensive guide!
To help with my review I ran a couple of AI-based passes over the guide against the SDK source. I want to be upfront that I''m not fully confident in these findings. I haven''t stood up an end-to-end provider to confirm the runtime behavior, and some of this may be intentional simplification for a conceptual doc. I''m sharing them as things to sanity-check rather than firm change requests.
1. Inbound claim mapping in the samples. With ASP.NET Core JwtBearer, MapInboundClaims defaults to true, which I think renames roles to ClaimTypes.Role and scp to the long schema URI before the app sees them. If that''s right, the scope policy searching "scp" and possibly [Authorize(Roles = "Admin")] might not match as written. Would adding options.MapInboundClaims = false; to the AddJwtBearer block, or a note about it, make the samples more copy-paste safe? Genuinely unsure here. You may have tested this and found it works.
2. "Any spec-compliant OAuth 2.0 authorization server works." The JwtBearer config shown assumes JWT access tokens specifically, and Authority seems to drive OIDC discovery (/.well-known/openid-configuration) rather than the RFC 8414 oauth-authorization-server path the prose also mentions. Might be worth narrowing to "issues JWT access tokens" so nobody wires up an opaque-token provider and gets stuck. Low confidence, could be splitting hairs.
3. A few smaller things I wasn''t sure about:
- The
WWW-Authenticate-on-401 description reads a touch broad. From a quick look theresource_metadataheader seems to be added only when the MCP scheme is the one challenging. The main sample sets that up correctly, so this may be a wording nit at most. - The session identity-pinning paragraph lists
sub,NameIdentifier, andUPN. The code path I glanced at looked like it prefersNameIdentifierfirst and can end up with no id if none are present. Might be worth a caveat, or I may just be misreading it.
None of these are blockers from where I sit, and I could easily be wrong on any of them. Happy to be corrected, and thanks again for putting this together.
Note
This review was assisted by AI-based analysis of the guide against the SDK source. A human is reviewing the findings before acting on them.
Resolves #1052
PR Summary
Adds a conceptual guide covering how to do OAuth for a real deployment, so that the answer to "how should someone building a real world MCP server go about authentication?" isn't "read the test fixture."
The core confusion in #1052 is that
TestOAuthServerlooks like the thing you're supposed to build. It isn't — it's an authorization server, and an MCP server is a resource server. Once that split is clear, the production configuration is mostlyAddJwtBearerpointed at an identity provider you already run, plusAddMcp()for the RFC 9728 metadata. The guide leads with that and then covers the parts that actually bite in production.What's included
docs/concepts/authorization/authorization.mddocs/concepts/toc.yml(under Server features, next to HTTP Context)docs/concepts/index.mdGuide coverage
TestOAuthServeris a fixture rather than a templateAddJwtBearer+AddMcpconfiguration, with audience validation tied to the advertised resourceResourcemust be set explicitly, and the reverse-proxy case — the resource identifier and theresource_metadatachallenge URL are both derived from the request, so forwarded headers (includingX-Forwarded-Prefix, andAllowedHoststo keep them un-spoofable) matterOnResourceMetadataRequestfor multi-tenant serversRequireAuthorization()on the endpoint, and[Authorize]/[AllowAnonymous]per primitive viaAddAuthorizationFilters()— including that listings are filtered, that custom list handlers are not, and that[AllowAnonymous]doesn't combine with endpoint-level gatingRequireClaim("scope", ...)is the wrong tool for a space-delimited claim[Authorize]checks re-read the principal per request, whileConfigureSessionOptionsis once per sessionNotes
McpAuthenticationHandler,McpAuthenticationOptions,ProtectedResourceMetadata,AuthorizationFilterSetup,HttpMcpServerBuilderExtensions, andAuthorizeAttributeTests.make generate-docslocally (no .NET SDK on this machine), so the DocFX build is unverified. The three<xref:>API targets and thexref:identity/xref:stateless/xref:httpcontextlinks were checked by hand against the source and the sibling docs' uids, and theuid: authorizationis unique. Worth a second look from CI.