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
- 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
- Fix proxy forwarding so the served metadata reflects the public origin (X-Forwarded-Proto/Host honored by the auth server)
- For Keycloak-like servers, use the canonical realm issuer URL (with the exact trailing-slash form the provider reports)
- 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
- In your metadata generator, derive the issuer field from the same constant used to build the well-known URL
- Watch for trailing-slash and default-port differences — Url equality is exact
- Honor X-Forwarded-Proto/Host in proxies so the served issuer reflects the public origin
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
- Protected Resource Metadata at {} has no authorization_serve
- Could not fetch Authorization Server Metadata for {}
- authorization server does not advertise code_challenge_metho
- OAuth endpoint must use HTTPS (got {}://{})
- OAuth endpoint must not point to private/reserved IP: {}
AI-assisted analysis of zed-industries/zed@f4178619ac (2026-08-20).
Data as JSON: /api/errors/799b1ed8c5afba39.
Report an issue: GitHub.