modelcontextprotocol / modelcontextprotocol/typescript-sdk
[v2] OAuth discovery validates any /.well-known/openid-configuration document as OIDC, rejecting conforming RFC 8414 metadata
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 13.4k
- Forks
- 2.2k
- Avg merge
- 3d 15h
- Merged PRs (30d)
- 4
Description
What happened?
discoverAuthorizationServerMetadata() picks its validation schema from which well-known filename resolved, not from the document that came back. Every openid-configuration candidate is typed "oidc", so anything served there is validated against OpenIdProviderDiscoveryMetadataSchema:
// client/dist/index.mjs (2.0.0)
const parsed = type === "oauth"
? OAuthMetadataSchema.parse(await response.json())
: OpenIdProviderDiscoveryMetadataSchema.parse(await response.json());
That schema requires jwks_uri, subject_types_supported and id_token_signing_alg_values_supported — three fields OpenID Connect Discovery 1.0 requires and RFC 8414 does not. So a plain OAuth 2.0 authorization server (no ID tokens, no JWKS, no sub) that publishes RFC 8414 metadata at /.well-known/openid-configuration is rejected:
[
{"expected":"string","code":"invalid_type","path":["jwks_uri"],"message":"Invalid input: expected string, received undefined"},
{"expected":"array","code":"invalid_type","path":["subject_types_supported"],"message":"Invalid input: expected array, received undefined"},
{"expected":"array","code":"invalid_type","path":["id_token_signing_alg_values_supported"],"message":"Invalid input: expected array, received undefined"}
]
Two things make this fatal rather than cosmetic:
- RFC 8414 §5 explicitly permits that filename for general OAuth metadata. The server is conforming; the client is not.
- The parse throws instead of continuing the candidate loop. Every other failure in that loop (
continueon 4xx/502,continueon a CORS failure) falls through to the next candidate. A schema failure abortsdiscoverAuthorizationServerMetadata()entirely, so no later candidate is tried and the whole connection fails.
Reported downstream at modelcontextprotocol/inspector#2172 against a real server (https://misc.poodll.com/mod/minilesson/mcp.php, a plain OAuth 2.0 AS whose resource lives under a path). Reproduced independently against @modelcontextprotocol/client@2.0.0 with an injected fetchFn, and again against a local test server that serves RFC 8414 metadata only at /.well-known/openid-configuration.
Two smaller things noticed in the same code path, mentioned here rather than as separate issues since a fix will likely touch both:
OpenIdProviderDiscoveryMetadataSchemais az.object(...), not az.looseObject(...)likeOAuthMetadataSchema. So a successful OIDC parse strips RFC 8414 fields that are not in the OIDC shape —revocation_endpointandintrospection_endpointamong them — from the returned metadata.- The doc comment on
discoverAuthorizationServerMetadatasays it "attempts RFC 8414 OAuth metadata discovery first / if OAuth discovery fails, falls back to OpenID Connect Discovery", which is what the URL ordering does but not what the schema selection does.
What did you expect?
A document should be validated by what it is, not by the filename it was found under. Concretely, either:
- try
OAuthMetadataSchemaand fall back toOpenIdProviderDiscoveryMetadataSchema(or vice versa) regardless of the candidate'stype—OAuthMetadataSchemais a loose object, so it accepts a full OIDC document without dropping any of its fields; or - treat a schema failure like the other per-candidate failures and
continueto the next candidate rather than throwing out of the whole function.
Either way, issuer validation (RFC 8414 §3.3 / OIDC Discovery §4.3) should stay exactly as it is.
Code to reproduce
import { discoverAuthorizationServerMetadata } from '@modelcontextprotocol/client';
// A plain OAuth 2.0 AS: RFC 8414 metadata, served only at the OIDC well-known path.
const METADATA = {
issuer: 'https://as.example.com',
authorization_endpoint: 'https://as.example.com/oauth/authorize',
token_endpoint: 'https://as.example.com/oauth/token',
response_types_supported: ['code'],
grant_types_supported: ['authorization_code', 'refresh_token'],
code_challenge_methods_supported: ['S256'],
};
const fetchFn: typeof fetch = async input => {
const url = String(input);
if (url === 'https://as.example.com/.well-known/openid-configuration') {
return new Response(JSON.stringify(METADATA), {
status: 200,
headers: { 'content-type': 'application/json' },
});
}
return new Response('not found', { status: 404 });
};
// Throws a ZodError for jwks_uri / subject_types_supported /
// id_token_signing_alg_values_supported. Expected: the RFC 8414 document.
await discoverAuthorizationServerMetadata('https://as.example.com', { fetchFn });
SDK version
@modelcontextprotocol/client@2.0.0 (with @modelcontextprotocol/core@2.0.0)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with discoverAuthorizationServerMetadata() and the related schemas in client/dist/index.mjs, then run the supplied fetchFn reproduction against the RFC 8414 document. Trace candidate failures and schema selection, and verify that a conforming OAuth metadata document at /.well-known/openid-configuration is accepted or that discovery continues to another candidate while issuer validation remains intact.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- authentication, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 58/100