zed-industries/zed · error

Auth server metadata issuer mismatch: expected {}, got {}

Error message

Auth server metadata issuer mismatch: expected {}, got {}

What it means

RFC 8414 requires that the issuer value inside an authorization server's metadata document exactly match the issuer used to construct the well-known URL. fetch_auth_server_metadata() enforces this: it takes the issuer field from the response (falling back to the requested issuer when the field is absent) and compares Url equality; any difference bails with both values. This stops a metadata document hosted at one URL from claiming to be a different authorization server, which would break token validation downstream.

Source

Thrown at crates/context_server/src/oauth.rs:817

        server_url
    )
}

/// Fetch Authorization Server Metadata, trying RFC 8414 and OIDC Discovery
/// endpoints in the priority order specified by the MCP spec.
pub async fn fetch_auth_server_metadata(
    http_client: &Arc<dyn HttpClient>,
    issuer: &Url,
) -> Result<AuthServerMetadata> {
    let candidate_urls = auth_server_metadata_urls(issuer);

    for url in &candidate_urls {
        match fetch_json::<AuthServerMetadataResponse>(http_client, url).await {
            Ok(response) => {
                let reported_issuer = response.issuer.unwrap_or_else(|| issuer.clone());

                if reported_issuer != *issuer {
                    bail!(
                        "Auth server metadata issuer mismatch: expected {}, got {}",
                        issuer,
                        reported_issuer
                    );
                }

                return Ok(AuthServerMetadata {
                    issuer: reported_issuer,
                    grant_types_supported: response.grant_types_supported,
                    authorization_endpoint: response
                        .authorization_endpoint
                        .ok_or_else(|| anyhow!("missing authorization_endpoint"))?,
                    token_endpoint: response
                        .token_endpoint
                        .ok_or_else(|| anyhow!("missing token_endpoint"))?,
                    registration_endpoint: response.registration_endpoint,
                    scopes_supported: response.scopes_supported,
                    code_challenge_methods_supported: response.code_challenge_methods_supported,

View on GitHub (pinned to f4178619ac)

Solutions

  1. Make the issuer field in the authorization server metadata byte-identical to the URL listed in authorization_servers / used for discovery, including scheme, host, port, and path
  2. Fix proxy forwarding so the served metadata reflects the public origin (X-Forwarded-Proto/Host honored by the auth server)
  3. For Keycloak-like servers, use the canonical realm issuer URL (with the exact trailing-slash form the provider reports)
  4. curl the well-known document and diff its issuer value against the URL you configured in the MCP server metadata

Example fix

// before
authorization_servers: ["https://auth.example.com"]
// document: { "issuer": "https://auth.example.com/" }  // trailing slash

// after
// document: { "issuer": "https://auth.example.com" }
Defensive patterns

Strategy: validation

Validate before calling

// after fetching the AS metadata document, compare issuers yourself to give a better message
let doc: serde_json::Value = fetch_json(&client, &candidate).await?;
if let Some(reported) = doc.get("issuer").and_then(|v| v.as_str()) {
    anyhow::ensure!(
        Url::parse(reported)? == *issuer,
        "server metadata issuer {reported} != configured {issuer}; fix server config"
    );
}

Type guard

fn issuers_match(configured: &Url, doc: &serde_json::Value) -> bool {
    doc.get("issuer")
        .and_then(|v| v.as_str())
        .and_then(|s| Url::parse(s).ok())
        .map(|u| u == *configured)
        .unwrap_or(true) // absent field falls back to configured, matching the library
}

Try / catch

match fetch_auth_server_metadata(&client, &issuer).await {
    Err(err) if err.to_string().contains("issuer mismatch") => {
        // fix the issuer in authorization_servers OR in the AS metadata; do not blindly retry
        show_config_hint("make metadata issuer exactly equal the authorization_servers entry");
        Err(err)
    }
    other => other,
}

Prevention

When it happens

Trigger: fetch_auth_server_metadata(http_client, issuer) succeeds in fetching the document, but response.issuer (when present) differs from the issuer URL used to build the candidate well-known URL. Trivial differences like a trailing slash, default-port inclusion (https://auth.example.com:443 vs https://auth.example.com), scheme http vs https, or a path suffix all fail Url equality.

Common situations: Server behind a proxy where the internal issuer is http://internal:9000 but the advertised one is https://auth.example.com; metadata generated with a trailing slash mismatch; OIDC providers that require the issuer to exactly match their configured base URL (Keycloak realm URL with/without trailing slash is a classic); authorization_servers entry in Protected Resource Metadata pointing at a redirecting alias rather than the canonical issuer.

Related errors


AI-assisted analysis of zed-industries/zed@f4178619ac (2026-08-20). Data as JSON: /api/errors/799b1ed8c5afba39. Report an issue: GitHub.