Hmbown/CodeWhale · critical
OIDC discovery attempted to downgrade
Error message
OIDC discovery attempted to downgrade {field} from HTTPS What it means
The issuer is served over HTTPS, but one of the discovered OAuth endpoints is plain HTTP. Downgrading a token endpoint (where credentials and codes flow) to plaintext is a classic token-theft vector, so the client refuses any http:// endpoint advertised by an https:// issuer.
Solutions
- Fix the IdP/proxy so the discovery document advertises https:// endpoints (set the external/public base URL).
- Ensure the proxy forwards the correct scheme (X-Forwarded-Proto) so generated URLs are https.
- Migrate the IdP deployment to TLS end-to-end.
- If you control the client config, use a documented-path endpoint list with https URLs instead of discovery.
Example fix
// before issuer=https://auth.example.com, token_endpoint=http://auth.example.com/token // downgrade // 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 issuer.scheme() == "https" && endpoint.scheme() != "https" {
eprintln!("IdP advertises plaintext endpoint behind HTTPS issuer — fix IdP base URL");
} Type guard
fn no_downgrade(issuer: &Url, endpoint: &Url) -> bool {
issuer.scheme() != "https" || endpoint.scheme() == "https"
} Prevention
- Set the IdP's public/external base URL to https so all advertised endpoints are https.
- Configure X-Forwarded-Proto / forwarded headers on the reverse proxy.
- Run TLS end-to-end for self-hosted IdPs; never mix http backends with an https issuer.
- Re-check the discovery document after any TLS or proxy migration.
When it happens
Trigger: validate_discovered_oauth_endpoint finding issuer.scheme() == "https" while the parsed {field} endpoint scheme is "http".
Common situations: Self-hosted IdP (Keycloak, Dex, Authentik) configured behind TLS at the issuer but advertising internal http:// endpoint URLs; reverse proxy exposing https upstream but http backend URLs in the discovery doc; mixed http/https deployment after a TLS migration.
Related errors
- returned an untrusted verification URI
- OIDC discovery issuer does not match the requested issuer
- OIDC discovery returned credentials in
- OIDC discovery returned
- OIDC discovery returned unsupported
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/3414f1ef74d148dc.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/oauth.rs:694
/// no plaintext downgrade, no embedded credentials, same origin.
fn validate_discovered_oauth_endpoint(
endpoint: Option<String>,
field: &str,
issuer: &str,
) -> Result<String> {
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),View on GitHub (pinned to 73e0f67d83)