From 71682ee835f1c269f92ba7ff29b6d6761d01ac46 Mon Sep 17 00:00:00 2001 From: peatey Date: Tue, 10 Mar 2026 15:55:16 -0500 Subject: [PATCH 1/8] fix(auth): redact secrets in Debug output for StoredCredentials and StoredAuthorizationState Removes `Debug` from the derive macros on `StoredCredentials` and `StoredAuthorizationState` and replaces them with manual `Debug` impls that print `[REDACTED]` for sensitive fields (access/refresh tokens, PKCE verifiers, and CSRF tokens), preventing accidental credential leakage via `{:?}` formatters, log calls, and error chains. Fixes #741 Co-Authored-By: Claude Sonnet 4.6 --- crates/rmcp/src/transport/auth.rs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/crates/rmcp/src/transport/auth.rs b/crates/rmcp/src/transport/auth.rs index a75b9ab54..e89e4d938 100644 --- a/crates/rmcp/src/transport/auth.rs +++ b/crates/rmcp/src/transport/auth.rs @@ -59,7 +59,7 @@ impl<'c> AsyncHttpClient<'c> for OAuthReqwestClient { const DEFAULT_EXCHANGE_URL: &str = "http://localhost"; /// Stored credentials for OAuth2 authorization -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub struct StoredCredentials { pub client_id: String, pub token_response: Option, @@ -69,6 +69,17 @@ pub struct StoredCredentials { pub token_received_at: Option, } +impl std::fmt::Debug for StoredCredentials { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("StoredCredentials") + .field("client_id", &self.client_id) + .field("token_response", &self.token_response.as_ref().map(|_| "[REDACTED]")) + .field("granted_scopes", &self.granted_scopes) + .field("token_received_at", &self.token_received_at) + .finish() + } +} + /// Trait for storing and retrieving OAuth2 credentials /// /// Implementations of this trait can provide custom storage backends @@ -119,13 +130,23 @@ impl CredentialStore for InMemoryCredentialStore { } /// Stored authorization state for OAuth2 PKCE flow -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub struct StoredAuthorizationState { pub pkce_verifier: String, pub csrf_token: String, pub created_at: u64, } +impl std::fmt::Debug for StoredAuthorizationState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("StoredAuthorizationState") + .field("pkce_verifier", &"[REDACTED]") + .field("csrf_token", &"[REDACTED]") + .field("created_at", &self.created_at) + .finish() + } +} + /// A transparent wrapper around a JSON object that captures any extra fields returned by the /// authorization server during token exchange that are not part of the standard OAuth 2.0 token /// response. From c09e84c2657ee3fa7f8e65e195f9e7cd8c8c91e7 Mon Sep 17 00:00:00 2001 From: peatey Date: Tue, 10 Mar 2026 21:03:26 -0500 Subject: [PATCH 2/8] test(auth): assert Debug output redacts secrets for credential types Adds regression tests for the fix in the previous commit, verifying that `{:?}` formatting of `StoredAuthorizationState` and `StoredCredentials` does not emit plaintext secrets. Co-Authored-By: Claude Sonnet 4.6 --- crates/rmcp/src/transport/auth.rs | 35 +++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/crates/rmcp/src/transport/auth.rs b/crates/rmcp/src/transport/auth.rs index e89e4d938..8622136d1 100644 --- a/crates/rmcp/src/transport/auth.rs +++ b/crates/rmcp/src/transport/auth.rs @@ -2797,6 +2797,41 @@ mod tests { assert_eq!(deserialized.csrf_token, "my-csrf"); } + #[test] + fn test_stored_authorization_state_debug_redacts_secrets() { + let pkce = PkceCodeVerifier::new("super-secret-verifier".to_string()); + let csrf = CsrfToken::new("super-secret-csrf".to_string()); + let state = StoredAuthorizationState::new(&pkce, &csrf); + let debug_output = format!("{:?}", state); + + assert!(!debug_output.contains("super-secret-verifier")); + assert!(!debug_output.contains("super-secret-csrf")); + assert!(debug_output.contains("[REDACTED]")); + } + + #[test] + fn test_stored_credentials_debug_redacts_token_response() { + use super::{OAuthTokenResponse, StoredCredentials, VendorExtraTokenFields}; + use oauth2::{AccessToken, basic::BasicTokenType}; + + let token_response = OAuthTokenResponse::new( + AccessToken::new("super-secret-access-token".to_string()), + BasicTokenType::Bearer, + VendorExtraTokenFields::default(), + ); + let creds = StoredCredentials { + client_id: "my-client".to_string(), + token_response: Some(token_response), + granted_scopes: vec![], + token_received_at: None, + }; + let debug_output = format!("{:?}", creds); + + assert!(!debug_output.contains("super-secret-access-token")); + assert!(debug_output.contains("[REDACTED]")); + assert!(debug_output.contains("my-client")); + } + #[test] fn test_stored_authorization_state_into_pkce_verifier() { let pkce = PkceCodeVerifier::new("original-verifier".to_string()); From ed2fa16aea29ec77ddc014120074224690aa3327 Mon Sep 17 00:00:00 2001 From: peatey Date: Thu, 12 Mar 2026 09:05:15 -0500 Subject: [PATCH 3/8] test(auth): address review feedback on debug redaction tests - Remove redundant VendorExtraTokenFields from use super:: in test_stored_credentials_debug_redacts_token_response (already imported at module scope) - Add assert!(debug_output.contains("created_at")) to test_stored_authorization_state_debug_redacts_secrets to verify non-secret fields remain visible in Debug output - Run cargo fmt Co-Authored-By: Claude Sonnet 4.6 --- crates/rmcp/src/transport/auth.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/rmcp/src/transport/auth.rs b/crates/rmcp/src/transport/auth.rs index 8622136d1..211a702fb 100644 --- a/crates/rmcp/src/transport/auth.rs +++ b/crates/rmcp/src/transport/auth.rs @@ -73,7 +73,10 @@ impl std::fmt::Debug for StoredCredentials { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("StoredCredentials") .field("client_id", &self.client_id) - .field("token_response", &self.token_response.as_ref().map(|_| "[REDACTED]")) + .field( + "token_response", + &self.token_response.as_ref().map(|_| "[REDACTED]"), + ) .field("granted_scopes", &self.granted_scopes) .field("token_received_at", &self.token_received_at) .finish() @@ -2807,11 +2810,12 @@ mod tests { assert!(!debug_output.contains("super-secret-verifier")); assert!(!debug_output.contains("super-secret-csrf")); assert!(debug_output.contains("[REDACTED]")); + assert!(debug_output.contains("created_at")); } #[test] fn test_stored_credentials_debug_redacts_token_response() { - use super::{OAuthTokenResponse, StoredCredentials, VendorExtraTokenFields}; + use super::{OAuthTokenResponse, StoredCredentials}; use oauth2::{AccessToken, basic::BasicTokenType}; let token_response = OAuthTokenResponse::new( From 7d9c4e4d9dbbb588dcff5b37ab18911f641c1669 Mon Sep 17 00:00:00 2001 From: Warwick Date: Thu, 12 Mar 2026 09:09:08 -0500 Subject: [PATCH 4/8] Update crates/rmcp/src/transport/auth.rs Co-authored-by: Dale Seo <5466341+DaleSeo@users.noreply.github.com> --- crates/rmcp/src/transport/auth.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/rmcp/src/transport/auth.rs b/crates/rmcp/src/transport/auth.rs index 211a702fb..8e03ba0bd 100644 --- a/crates/rmcp/src/transport/auth.rs +++ b/crates/rmcp/src/transport/auth.rs @@ -2811,6 +2811,7 @@ mod tests { assert!(!debug_output.contains("super-secret-csrf")); assert!(debug_output.contains("[REDACTED]")); assert!(debug_output.contains("created_at")); + assert!(debug_output.contains("created_at")); } #[test] From 71f9e7f573d76017cebb3957dd12669d4b255532 Mon Sep 17 00:00:00 2001 From: Dale Seo <5466341+DaleSeo@users.noreply.github.com> Date: Thu, 12 Mar 2026 18:28:37 -0400 Subject: [PATCH 5/8] fix: remaining formatting issue --- crates/rmcp/src/transport/auth.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/rmcp/src/transport/auth.rs b/crates/rmcp/src/transport/auth.rs index 8e03ba0bd..e7883f56e 100644 --- a/crates/rmcp/src/transport/auth.rs +++ b/crates/rmcp/src/transport/auth.rs @@ -2816,8 +2816,9 @@ mod tests { #[test] fn test_stored_credentials_debug_redacts_token_response() { - use super::{OAuthTokenResponse, StoredCredentials}; use oauth2::{AccessToken, basic::BasicTokenType}; + + use super::{OAuthTokenResponse, StoredCredentials}; let token_response = OAuthTokenResponse::new( AccessToken::new("super-secret-access-token".to_string()), From 6dc115b9b0a8f64ccb5d5d1542aa462bf43c682d Mon Sep 17 00:00:00 2001 From: Dale Seo <5466341+DaleSeo@users.noreply.github.com> Date: Thu, 12 Mar 2026 18:29:46 -0400 Subject: [PATCH 6/8] fix: formatting --- crates/rmcp/src/transport/auth.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/rmcp/src/transport/auth.rs b/crates/rmcp/src/transport/auth.rs index e7883f56e..e67f569c9 100644 --- a/crates/rmcp/src/transport/auth.rs +++ b/crates/rmcp/src/transport/auth.rs @@ -2817,7 +2817,6 @@ mod tests { #[test] fn test_stored_credentials_debug_redacts_token_response() { use oauth2::{AccessToken, basic::BasicTokenType}; - use super::{OAuthTokenResponse, StoredCredentials}; let token_response = OAuthTokenResponse::new( From 0b1b10c0fa7f88154bbb4fbbb000f7a078125574 Mon Sep 17 00:00:00 2001 From: Dale Seo <5466341+DaleSeo@users.noreply.github.com> Date: Thu, 12 Mar 2026 18:31:30 -0400 Subject: [PATCH 7/8] fix: formatting --- crates/rmcp/src/transport/auth.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/rmcp/src/transport/auth.rs b/crates/rmcp/src/transport/auth.rs index e67f569c9..e7883f56e 100644 --- a/crates/rmcp/src/transport/auth.rs +++ b/crates/rmcp/src/transport/auth.rs @@ -2817,6 +2817,7 @@ mod tests { #[test] fn test_stored_credentials_debug_redacts_token_response() { use oauth2::{AccessToken, basic::BasicTokenType}; + use super::{OAuthTokenResponse, StoredCredentials}; let token_response = OAuthTokenResponse::new( From 3989f5aa2bc22c5d101073be96e9ec5a45acf4ab Mon Sep 17 00:00:00 2001 From: Dale Seo <5466341+DaleSeo@users.noreply.github.com> Date: Thu, 12 Mar 2026 18:33:30 -0400 Subject: [PATCH 8/8] fix: please --- crates/rmcp/src/transport/auth.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rmcp/src/transport/auth.rs b/crates/rmcp/src/transport/auth.rs index e7883f56e..9e048b1a2 100644 --- a/crates/rmcp/src/transport/auth.rs +++ b/crates/rmcp/src/transport/auth.rs @@ -2817,7 +2817,7 @@ mod tests { #[test] fn test_stored_credentials_debug_redacts_token_response() { use oauth2::{AccessToken, basic::BasicTokenType}; - + use super::{OAuthTokenResponse, StoredCredentials}; let token_response = OAuthTokenResponse::new(