{"record":{"id":"3414f1ef74d148dc","repo":"Hmbown/CodeWhale","slug":"oidc-discovery-attempted-to-downgrade-field-from-https","errorCode":null,"errorMessage":"OIDC discovery attempted to downgrade {field} from HTTPS","messagePattern":"OIDC discovery attempted to downgrade (.+?) from HTTPS","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/tui/src/oauth.rs","lineNumber":694,"sourceCode":"/// no plaintext downgrade, no embedded credentials, same origin.\nfn validate_discovered_oauth_endpoint(\n    endpoint: Option<String>,\n    field: &str,\n    issuer: &str,\n) -> Result<String> {\n    let endpoint = endpoint\n        .as_deref()\n        .map(str::trim)\n        .filter(|endpoint| !endpoint.is_empty())\n        .with_context(|| format!(\"OIDC discovery missing {field}\"))?;\n    let parsed = reqwest::Url::parse(endpoint)\n        .with_context(|| format!(\"OIDC discovery returned an invalid {field}\"))?;\n    if !matches!(parsed.scheme(), \"http\" | \"https\") {\n        bail!(\"OIDC discovery returned unsupported {field} scheme\");\n    }\n    let issuer = oauth_endpoint_url(issuer).context(\"OIDC issuer is not a trusted URL\")?;\n    if issuer.scheme() == \"https\" && parsed.scheme() != \"https\" {\n        bail!(\"OIDC discovery attempted to downgrade {field} from HTTPS\");\n    }\n    if !parsed.username().is_empty() || parsed.password().is_some() {\n        bail!(\"OIDC discovery returned credentials in {field}\");\n    }\n    if parsed.origin() != issuer.origin() {\n        bail!(\"OIDC discovery returned {field} on a different origin than the issuer\");\n    }\n    let _ = oauth_endpoint_url(parsed.as_str())?;\n    Ok(endpoint.to_string())\n}\n\n/// Documented-path endpoints for a provider row, no discovery.\nfn fallback_oauth_endpoints(params: &OAuthProviderParams, issuer: &str) -> OAuthEndpoints {\n    OAuthEndpoints {\n        device_authorization_endpoint: params\n            .device_code_path\n            .map(|path| format!(\"{}/{}\", issuer.trim_end_matches('/'), path)),\n        token_endpoint: format!(\"{}/{}\", issuer.trim_end_matches('/'), params.token_path),","sourceCodeStart":676,"sourceCodeEnd":712,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/oauth.rs#L676-L712","documentation":"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.","triggerScenarios":"validate_discovered_oauth_endpoint finding issuer.scheme() == \"https\" while the parsed {field} endpoint scheme is \"http\".","commonSituations":"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.","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."],"exampleFix":"// before\nissuer=https://auth.example.com, token_endpoint=http://auth.example.com/token // downgrade\n// after\ntoken_endpoint=https://auth.example.com/token","handlingStrategy":"validation","validationCode":"let issuer = reqwest::Url::parse(issuer_url)?;\nlet endpoint = reqwest::Url::parse(doc[\"token_endpoint\"].as_str()?)?;\nif issuer.scheme() == \"https\" && endpoint.scheme() != \"https\" {\n    eprintln!(\"IdP advertises plaintext endpoint behind HTTPS issuer — fix IdP base URL\");\n}","typeGuard":"fn no_downgrade(issuer: &Url, endpoint: &Url) -> bool {\n    issuer.scheme() != \"https\" || endpoint.scheme() == \"https\"\n}","tryCatchPattern":null,"preventionTips":["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."],"tags":["oauth","security","tls","discovery","downgrade"],"backgroundTag":"https-downgrade-blocked","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-22T21:17:16.096Z"}