Skip to content

[v2] OAuth discovery validates any /.well-known/openid-configuration document as OIDC, rejecting conforming RFC 8414 metadata #2733

Description

@cliffhall

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:

  1. RFC 8414 §5 explicitly permits that filename for general OAuth metadata. The server is conforming; the client is not.
  2. The parse throws instead of continuing the candidate loop. Every other failure in that loop (continue on 4xx/502, continue on a CORS failure) falls through to the next candidate. A schema failure aborts discoverAuthorizationServerMetadata() 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:

  • OpenIdProviderDiscoveryMetadataSchema is a z.object(...), not a z.looseObject(...) like OAuthMetadataSchema. So a successful OIDC parse strips RFC 8414 fields that are not in the OIDC shape — revocation_endpoint and introspection_endpoint among them — from the returned metadata.
  • The doc comment on discoverAuthorizationServerMetadata says 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:

  1. try OAuthMetadataSchema and fall back to OpenIdProviderDiscoveryMetadataSchema (or vice versa) regardless of the candidate's typeOAuthMetadataSchema is a loose object, so it accepts a full OIDC document without dropping any of its fields; or
  2. treat a schema failure like the other per-candidate failures and continue to 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions