fix(security/oauth): never log raw IdP token-endpoint response bodies#5526
Merged
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
…#5525) The OAuth refresh path (`crates/librefang-kernel/src/mcp_oauth_provider.rs::try_refresh`) and the OAuth callback handler (`crates/librefang-api/src/routes/mcp_auth.rs`) previously logged the token-endpoint response body verbatim via `warn!` / `error!`. When the body contained `access_token` / `refresh_token` / `id_token` / `client_secret` (legitimately for IdPs that echo session state on `invalid_grant`, or adversarially for malicious IdPs designed to plant secrets in operator logs) the secret was persisted to whatever log aggregator the daemon ships to — typically a SIEM with retention longer than the OAuth flow itself. Replaces both sites with a shared `redact_token_endpoint_response(status, content_type, body)` helper in `librefang-kernel::mcp_oauth_provider`. It returns a `RedactedTokenEndpointResponse { status, content_type, body_sha256_prefix, body_len }`; the raw body never leaves the helper. Both `Display` and `Debug` impls are sanitized so a `tracing::warn!` with `%redacted` or `?redacted` is equally safe. The hex digest prefix lets operators correlate two log lines that saw the same body without ever seeing the body itself. Three regression tests asserting that secrets like `super-secret-12345` do not appear in the captured log output across both the kernel `try_refresh` call shape and the API callback's 2xx-parse-failure and non-success call shapes. The other three sub-findings of the rollup (`docs/issues/oauth-refresh-error-body-token-leak.md` — error classification, single-flight refresh lock, `refresh_token.unwrap()` removal) are intentionally separate and tracked separately. Refs #5525.
`KernelOAuthProvider::register_client` (RFC 7591 Dynamic Client Registration) had the same body-interpolation pattern as the token-endpoint refresh path: on non-success status the raw response body was both returned in the `Err(String)` (which surfaces to `McpAuthState::Error.message` in the dashboard) and logged via `tracing::warn!(error = %e, "Dynamic Client Registration failed")` in `routes::mcp_auth::auth_start`. RFC 7591 §3.2.1 allows the registration response (including error bodies) to contain `client_secret`, and an adversarial AS can plant any token-shaped value here just as easily as on the token endpoint — the threat model is identical. Route both emissions through the existing `redact_token_endpoint_response` helper instead of the verbatim body. Added `dcr_failure_does_not_leak_raw_body_into_error_or_log` to lock in the behaviour: the returned `Err` keeps the HTTP status but no body, and a `tracing::warn!` capture across the call shape contains the sanitized digest but none of the token-shaped substrings. Verified: cargo check -p librefang-kernel --lib, cargo clippy -p librefang-kernel --lib -- -D warnings, cargo test -p librefang-kernel --lib mcp_oauth_provider — 16/16 passing, cargo test -p librefang-api --lib mcp_auth::tests::callback_body_preview_redaction_strips_token_fields — passing.
houko
force-pushed
the
fix/oauth-refresh-log-sanitization
branch
from
May 22, 2026 12:18
01efa76 to
139830a
Compare
houko
added a commit
that referenced
this pull request
May 22, 2026
…drop unwrap (#5609) Hardens the MCP OAuth refresh path (`librefang-kernel::mcp_oauth_provider`) — the remaining three sub-findings of `docs/issues/oauth-refresh-error-body-token-leak.md` after the log-sanitization sub-finding landed in #5526. Internal audit. Error classification: `try_refresh` previously returned `Result<OAuthTokens, String>` and `load_token` collapsed every failure — a 503, a DNS blip, an `invalid_grant` — into `Ok(None)`, which the connection layer reads as "no token, re-auth". That throws away a still-valid refresh token on a transient outage. Introduce a `RefreshError { Revoked, Transient, Permanent }` enum and classify: `400 invalid_grant` -> Revoked (the only re-auth signal, mapped to `Ok(None)`); 5xx / timeout / transport error -> Transient (kept for retry); everything else -> Permanent. Transient/Permanent now surface as the new `McpOAuthError::RefreshFailed` so the refresh token is retained rather than discarded. The OAuth `error` code is parsed only as a short `'static` code; no token-shaped body field is ever read or logged. Single-flight: rotating-refresh-token providers (Google, GitHub Apps, Notion) invalidate the old refresh token on every refresh. Concurrent `load_token` calls that raced past the expiry check both POSTed the same soon-to-be-rotated token, burning a valid session. Add a process-global per-`server_url` lock (`KernelOAuthProvider` is stateless and rebuilt per request, so an instance field would not serialize across callers). After acquiring the lock, re-read `expires_at` from the vault and return the peer's freshly-stored token without a second network POST. Unwrap removal: `find_any_with_refresh` now returns the refresh token as a non-optional `String`, so `entry.refresh_token.unwrap()` in `auth_refresh` is gone — the `Some(..)` arm is the type-level proof. Tests (kernel, next to the code): 400 invalid_grant -> Revoked; 503/502 -> Transient; non-invalid_grant 4xx and bare 401 -> Permanent; classified error Display does not leak token-shaped body fields; `oauth_error_code` extracts only the code; the single-flight lock is shared per server_url and isolated across servers; the recheck branch returns an already-refreshed token without touching the network; an expired token with no refresh token yields Ok(None). The unwrap removal is a compile-time proof. Verification: - cargo check -p librefang-kernel -p librefang-api -p librefang-runtime-mcp - cargo check --workspace --lib - cargo test -p librefang-kernel --lib mcp_oauth_provider (24 passed) - cargo test -p librefang-api --lib oauth (23) and mcp_auth (34) passed - cargo clippy on changed files clean (pre-existing 1.95 lints in untouched validation.rs/webchat.rs/registry.rs are CI-pinned-toolchain noise, identical on origin/main)
f-liva
pushed a commit
to f-liva/librefang
that referenced
this pull request
May 25, 2026
Adds `McpOAuthConfig.client_secret_env` field so operators can declare which environment variable holds the OAuth client secret for confidential providers (e.g. Google Workspace MCP servers that reject refresh requests without `client_secret`). Flow: auth_start resolves env var -> persists to vault -> auth_callback and try_refresh include client_secret in form params. Pure PKCE clients (client_secret_env unset) are unaffected. Addresses all review blockers from PR librefang#5060 v1: - Ground-up rebase on upstream/main preserves librefang#5609 (RefreshError enum, single-flight lock, classify_refresh_failure), librefang#5526 (DCR body redaction via redact_token_endpoint_response), and librefang#5069 regression test (vault_set_twice_round_trips_via_env_key) - Bug-D ordering: DCR incompatibility check runs BEFORE vault_set so no stale secret is persisted on misconfiguration - New integration test: client_secret_env_vault_round_trip pins the vault storage contract - Golden fixture regenerated against current main
houko
added a commit
that referenced
this pull request
Jun 21, 2026
…5060) (#6260) * feat(mcp-oauth): add client_secret_env for confidential OAuth clients Adds `McpOAuthConfig.client_secret_env` field so operators can declare which environment variable holds the OAuth client secret for confidential providers (e.g. Google Workspace MCP servers that reject refresh requests without `client_secret`). Flow: auth_start resolves env var -> persists to vault -> auth_callback and try_refresh include client_secret in form params. Pure PKCE clients (client_secret_env unset) are unaffected. Addresses all review blockers from PR #5060 v1: - Ground-up rebase on upstream/main preserves #5609 (RefreshError enum, single-flight lock, classify_refresh_failure), #5526 (DCR body redaction via redact_token_endpoint_response), and #5069 regression test (vault_set_twice_round_trips_via_env_key) - Bug-D ordering: DCR incompatibility check runs BEFORE vault_set so no stale secret is persisted on misconfiguration - New integration test: client_secret_env_vault_round_trip pins the vault storage contract - Golden fixture regenerated against current main * fix(mcp): set client_secret_env in test McpOAuthConfig + refresh secrets baseline The new client_secret_env field made an OAuth test initializer non-exhaustive after the main merge; set it to None. Refresh .secrets.baseline for the new client_secret_env doc-example env var name (false positive, consistent with the existing example entries). * fix(mcp): add client_secret_env to mcp_oauth_integration test McpOAuthConfig The cherry-picked feature adds client_secret_env: Option<String> to McpOAuthConfig. The struct-literal in crates/librefang-runtime/tests/mcp_oauth_integration.rs constructs the config without ..Default::default(), so the new field is mandatory there too — otherwise E0063 breaks the runtime test-build lanes (the exact blocker flagged on #5060). --------- Co-authored-by: Federico Liva <[email protected]> Co-authored-by: Evan <[email protected]> Co-authored-by: Evan <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #5525
Summary
The OAuth-refresh failure log path in
crates/librefang-kernel/src/mcp_oauth_provider.rsand the 2xx-parse-failurebody_preview(plus the immediately preceding non-successbody_preview) incrates/librefang-api/src/routes/mcp_auth.rspreviously logged the IdP token-endpoint response body verbatim, which could emitaccess_token/refresh_token/id_token/client_secretin clear text into the operator's logs.Replaced all three with a shared
redact_token_endpoint_response(status, content_type, body)helper that produces aRedactedTokenEndpointResponse { status, content_type, body_sha256_prefix, body_len }value for tracing. The body itself never reaches the log. BothDisplayandDebugimpls are sanitized, so%redactedand?redactedare equally safe.The hex digest prefix lets operators correlate two log lines that saw the same body without ever holding the body itself.
Files changed
crates/librefang-kernel/src/mcp_oauth_provider.rs— addedRedactedTokenEndpointResponse+redact_token_endpoint_responsehelper (pub), updatedtry_refreshcall site, and added three regression tests.crates/librefang-api/src/routes/mcp_auth.rs— updated bothbody_previewlog sites (non-success status branch and 2xx parse-failure branch) to use the kernel helper; added a regression test that exercises both call shapes.Out of scope
The other three sub-findings of
docs/issues/oauth-refresh-error-body-token-leak.md(error classification refactor, per-server single-flight refresh lock,refresh_token.unwrap()removal) are intentionally separate — each warrants its own focused review.Verification
cargo check -p librefang-kernel -p librefang-api --lib— clean.cargo clippy -p librefang-kernel -p librefang-api --lib --all-targets -- -D warnings— clean.cargo test -p librefang-kernel --lib mcp_oauth_provider— 15 passed (3 new:redact_token_endpoint_response_strips_token_fields,redact_token_endpoint_response_digest_is_stable,warn_emission_does_not_contain_raw_token_body).cargo test -p librefang-api --lib mcp_auth::tests::callback_body_preview_redaction_strips_token_fields— passes.Refs
docs/issues/oauth-refresh-error-body-token-leak.md.