What
crates/librefang-kernel/src/mcp_oauth_provider.rs:183-187 (OAuth refresh failure path) and crates/librefang-api/src/routes/mcp_auth.rs:799-809 (2xx parse-failure body_preview) log the IdP token-endpoint response body verbatim via warn! / error!.
When the body contains access_token / refresh_token / id_token / client_secret (legitimately for an IdP that echoes the token on error, or adversarially for a malicious IdP planting secrets in operator logs), the secrets land in centralized log aggregators with longer retention than the OAuth flow that produced them.
The non-success branch of the OAuth callback handler in the same file (the lines immediately preceding the 2xx parse-failure site) has the same shape of leak via body_preview and is addressed together.
Why it matters
- IdPs (including some commercial ones) include token-shaped values in error bodies; even a
400 invalid_grant body can carry partial token state.
- Log aggregators ship to retention tiers (SIEM, Splunk, Loki) longer than the daemon-local log file rotation.
- Once a bearer token is in a SIEM, it's effectively forever-leaked.
Source
Local audit note docs/issues/oauth-refresh-error-body-token-leak.md, sub-finding "this". The other three rows (error classification, single-flight, unwrap) are separate issues, tracked separately.
Fix
Add a redact_token_endpoint_response(status, content_type, body) helper that returns a structured {status, content_type, body_sha256_prefix, body_len} for tracing — never the raw body. Use at both sites. Regression test: error body containing access_token produces a log line that does not contain the secret.
What
crates/librefang-kernel/src/mcp_oauth_provider.rs:183-187(OAuth refresh failure path) andcrates/librefang-api/src/routes/mcp_auth.rs:799-809(2xx parse-failurebody_preview) log the IdP token-endpoint response body verbatim viawarn!/error!.When the body contains
access_token/refresh_token/id_token/client_secret(legitimately for an IdP that echoes the token on error, or adversarially for a malicious IdP planting secrets in operator logs), the secrets land in centralized log aggregators with longer retention than the OAuth flow that produced them.The non-success branch of the OAuth callback handler in the same file (the lines immediately preceding the 2xx parse-failure site) has the same shape of leak via
body_previewand is addressed together.Why it matters
400 invalid_grantbody can carry partial token state.Source
Local audit note
docs/issues/oauth-refresh-error-body-token-leak.md, sub-finding "this". The other three rows (error classification, single-flight, unwrap) are separate issues, tracked separately.Fix
Add a
redact_token_endpoint_response(status, content_type, body)helper that returns a structured{status, content_type, body_sha256_prefix, body_len}for tracing — never the raw body. Use at both sites. Regression test: error body containingaccess_tokenproduces a log line that does not contain the secret.