Hmbown/CodeWhale · critical
OIDC discovery returned
Error message
OIDC discovery returned {field} on a different origin than the issuer What it means
A discovered OAuth endpoint is on a different origin (scheme + host + port) than the issuer that advertised it. OAuth security guidance requires endpoints to be same-origin with the issuer to prevent an attacker-controlled document from steering tokens to another host, so cross-origin endpoints are rejected.
Solutions
- Configure the IdP so all advertised endpoints share the issuer's public origin.
- Check port and scheme match exactly, including default-port handling.
- Fix reverse-proxy headers (Host, X-Forwarded-Proto) so generated endpoint URLs use the public origin.
- If the provider legitimately uses separate origins, use documented-path endpoint configuration instead of discovery.
Example fix
// before issuer=https://auth.example.com, token_endpoint=https://internal.svc:8080/token // cross-origin // after token_endpoint=https://auth.example.com/token
Defensive patterns
Strategy: validation
Validate before calling
let issuer = reqwest::Url::parse(issuer_url)?;
let endpoint = reqwest::Url::parse(doc["token_endpoint"].as_str()?)?;
if endpoint.origin() != issuer.origin() {
eprintln!("endpoint origin {} differs from issuer origin {}", endpoint.origin(), issuer.origin());
} Type guard
fn same_origin(issuer: &Url, endpoint: &Url) -> bool {
endpoint.origin() == issuer.origin()
} Prevention
- Keep all OAuth endpoints on the issuer's public origin; avoid separate internal hostnames/ports in advertised URLs.
- Fix proxy Host/X-Forwarded-* headers so the IdP generates public-origin URLs.
- Watch default-port handling: 443 vs an explicit :8443 are different origins.
- If a provider legitimately uses other origins, use explicit documented-path endpoints rather than discovery.
When it happens
Trigger: validate_discovered_oauth_endpoint comparing parsed.origin() of the {field} endpoint against issuer.origin() and finding them different (different host, scheme, or port).
Common situations: IdP behind a proxy advertising internal hostnames (e.g. http://internal-svc:8080) while the issuer is public; port mismatches (default 443 vs explicit :8443); DNS aliases or load-balancer hostnames differing from the issuer host.
Related errors
- OIDC discovery returned credentials in
- OIDC discovery returned unsupported
- returned a verification URI with embedded credentials
- returned an untrusted verification URI
- OIDC discovery attempted to downgrade
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/eebd61c1143abcb5.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/oauth.rs:700
let endpoint = endpoint
.as_deref()
.map(str::trim)
.filter(|endpoint| !endpoint.is_empty())
.with_context(|| format!("OIDC discovery missing {field}"))?;
let parsed = reqwest::Url::parse(endpoint)
.with_context(|| format!("OIDC discovery returned an invalid {field}"))?;
if !matches!(parsed.scheme(), "http" | "https") {
bail!("OIDC discovery returned unsupported {field} scheme");
}
let issuer = oauth_endpoint_url(issuer).context("OIDC issuer is not a trusted URL")?;
if issuer.scheme() == "https" && parsed.scheme() != "https" {
bail!("OIDC discovery attempted to downgrade {field} from HTTPS");
}
if !parsed.username().is_empty() || parsed.password().is_some() {
bail!("OIDC discovery returned credentials in {field}");
}
if parsed.origin() != issuer.origin() {
bail!("OIDC discovery returned {field} on a different origin than the issuer");
}
let _ = oauth_endpoint_url(parsed.as_str())?;
Ok(endpoint.to_string())
}
/// Documented-path endpoints for a provider row, no discovery.
fn fallback_oauth_endpoints(params: &OAuthProviderParams, issuer: &str) -> OAuthEndpoints {
OAuthEndpoints {
device_authorization_endpoint: params
.device_code_path
.map(|path| format!("{}/{}", issuer.trim_end_matches('/'), path)),
token_endpoint: format!("{}/{}", issuer.trim_end_matches('/'), params.token_path),
}
}
/// POST a device-authorization request. Pure transport over an explicit
/// endpoint: discovery (or its absence) is the caller's decision.
fn request_device_grant(View on GitHub (pinned to 73e0f67d83)