AI Engineering from Scratch(英文原版)
Given an MCP server config and an IdP capability set, emit the auth surface and refusal rules that constitute a production MCP authorization layer.
Inputs:
mcp_resource_url— canonical resource URL (most-specific identifier; keep a path only when it distinguishes co-hosted servers), used asaudand as the protected-resource metadataresourcevalue.idp_metadata_url— the IdP's/.well-known/oauth-authorization-server(or OpenID Connect Discovery) URL.idp_capabilities: observed values forissuer,code_challenge_methods_supported,grant_types_supported,client_id_metadata_document_supported, deprecatedregistration_endpoint,response_types_supported, andauthorization_response_iss_parameter_supported.pre_registered_client_ids: optional issuer-to-client-ID map provisioned by authorization-server operators. Prefer this issuer-scoped identity before CIMD, then use deprecated DCR only as the final compatibility path.application_type:nativeorweb, required when deprecated DCR compatibility is selected.credential_store: client IDs and registration credentials keyed by authorization-server issuer, with access tokens keyed by(issuer, mcp_resource_url).tools: the MCP tool list with the scope each requires.
Produce:
Refusal gate. Refuse to wire and stop if any hard condition fails:
S256is missing fromcode_challenge_methods_supported(PKCE has no degraded mode).authorization_codeis missing fromgrant_types_supported.response_types_supportedis anything other than exactly["code"].- No enrollment path exists: none of a pre-registered
client_id,client_id_metadata_document_supported: true, or a deprecated DCR compatibility endpoint is available. - CIMD is selected but its
client_idis not the absolute HTTPS document URL with a path, does not match the document URL, or the document lacks a non-emptyclient_nameorredirect_urisarray.application_typeis optional for CIMD. - A returned RFC 9207
issdiffers from the issuer recorded before redirect, or is omitted when the server advertised it as supported. - Deprecated DCR lacks
application_type, or its redirect URI policy conflicts withnativeorweb.
Protected-resource metadata document (RFC 9728) for the MCP server. For a resource with a path, insert the well-known segment before that path:
https://host/team/mcpmaps tohttps://host/.well-known/oauth-protected-resource/team/mcp. Includeresource,authorization_servers(the issuer allow-list),scopes_supported, andbearer_methods_supported: ["header"].HTTP endpoints.
GET /.well-known/oauth-protected-resource— returns the document from (2).POST /mcp(the stateless MCP transport): validates the bearer token for this request before any tool dispatches.- DCR compatibility only:
POST /register, with an application-type check and a rate-limit check ahead of it.
Background job + routines.
- A scheduled JWKS refresh that re-fetches
jwks_uriinto the cache\{keys, fetched_at\}. Idempotent; never mints keys. The AS rotates; the resource server only refreshes. Default0 */6 * * *; tighten to*/15 * * * *for high-rotation IdPs. - A
validateroutine — checksissallow-list, signature against cached JWKS,aud == mcp_resource_url,exp, required scope. - A step-up issuance path — only if the tool list contains operations gated behind a scope the user does not initially grant.
- A scheduled JWKS refresh that re-fetches
Cache plan. One entry per accepted issuer keyed by
issuer, holding\{keys, fetched_at\}. Document the read pattern: the validator reads the cache and falls back to a single synchronous refresh onkidmiss (re-fetch, not rotate — re-fetch is idempotent and cannot be turned into a key-creation DoS).Scope mapping. Map every tool to the scope it requires. Output a table:
| tool | required_scope | rationale |. Group destructive tools under their own scope; never reuse a read scope for a write tool.Refusal rules at runtime (the validator must encode these):
- Reject when
aud != mcp_resource_url→ 401Bearer error="invalid_token", error_description="audience mismatch", resource_metadata="<prm_url>". - Reject when
iss not in authorization_servers. - Reject when
kidnot in cached JWKS after a single re-fetch fall-back. - Reject when required scope is absent → 403
Bearer error="insufficient_scope", scope="<required>", resource_metadata="<prm_url>". - Reject any authorization request without an S256
code_challenge, and reject any token request whosecode_verifier, client, redirect URI, orresourcedoes not match the one-time authorization-code record. - Reject any credential or token whose issuer does not match its credential-store key. Issuer change requires new enrollment.
- Reject when
Hard rejects (never wire any of these — refuse the request and document why):
- Storing
client_secretin plaintext. Public clients usetoken_endpoint_auth_method: none; confidential clients useprivate_key_jwt. No plaintext shared secrets at rest or in the registration response logs. - Skipping the
audcheck on the validator. Audience binding (access-token privilege restriction) is the entire reason for RFC 8707 + RFC 9728. - Wiring the JWKS cache-miss fall-back to a rotate-and-mint instead of a re-fetch. It never produces the missing
kidand lets attacker-controlledkidvalues force unbounded key creation. The fall-back must be the idempotent refresh. - Allowing PKCE-less authorization code requests. OAuth 2.1 forbids it; the validator must reject any
/tokenexchange whose stored authorization-code record lacks acode_challenge. - Caching JWKS without a refresh job. Either the scheduled refresh ships, or the auth surface does not deploy.
- Trusting the
issclaim without an allow-list. Any validator that accepts a token from anyisslets an attacker stand up their own IdP and forge tokens. - Forwarding the inbound MCP token to an upstream API (token passthrough). If the MCP server calls upstream APIs it MUST obtain its own separate token; passthrough creates the confused-deputy problem.
- Storing
registration_access_tokenin plaintext. Hash-at-rest; require cleartext on every update. - Treating MCP request metadata or a removed protocol session as authorization state. The 2026-07-28 transport is stateless; authenticate and authorize every request.
Output: a one-page plan with the protected-resource document, issuer-keyed enrollment layout, issuer-and-resource token layout, chosen enrollment path, HTTP endpoints, JWKS refresh job, scope mapping, and runtime refusal rules. End with the first unmet deployment gate found in the authorization server's actual metadata.