//! No-secret-in-output gate (todo 39). //! //! Formats every loggable/`Debug` representation the BFF can emit and asserts //! that none of the four secret classes appears: the client secret, the DPoP //! private scalar, access/refresh tokens, and the PKCE verifier. Dummy //! sentinel strings stand in for real secrets — they are fabricated for this //! test, never provisioned anywhere, and this file only ever asserts their //! *absence* from formatted output (it never prints a secret to prove that). use std::time::Duration; use secrecy::SecretString; /// Fabricated client-secret stand-in (never a real credential). const CLIENT_SECRET_SENTINEL: &str = "xtask-39-client-secret-sentinel"; /// Fabricated access-token stand-in (never a real token). const ACCESS_TOKEN_SENTINEL: &str = "xtask-39-access-token-sentinel"; /// Fabricated refresh-token stand-in (never a real token). const REFRESH_TOKEN_SENTINEL: &str = "xtask-39-refresh-token-sentinel"; /// Fabricated PKCE-verifier stand-in (never a real verifier). const VERIFIER_SENTINEL: &str = "xtask-39-pkce-verifier-sentinel"; /// Fabricated authorization-code stand-in (never a real code). const CODE_SENTINEL: &str = "xtask-39-auth-code-sentinel"; /// Assert that `rendered` carries none of the fabricated secrets. fn assert_no_secret(rendered: &str, what: &str) { for sentinel in [ CLIENT_SECRET_SENTINEL, ACCESS_TOKEN_SENTINEL, REFRESH_TOKEN_SENTINEL, VERIFIER_SENTINEL, CODE_SENTINEL, ] { assert!( !rendered.contains(sentinel), "{what} leaked a secret class into output: {rendered:?}" ); } } #[test] fn client_secret_never_appears_in_debug_output() { let creds = oauth_core::ClientCredentials { client_id: "xtask-39-demo-client".to_owned(), client_secret: SecretString::from(CLIENT_SECRET_SENTINEL), }; let rendered = format!("{creds:?}"); assert_no_secret(&rendered, "ClientCredentials Debug"); assert!( rendered.contains(oauth_core::REDACTED), "redaction marker must be present: {rendered:?}" ); let profile = oauth_core::OAuthProfile { client: creds, ..oauth_core::OAuthProfile::default() }; let rendered = format!("{profile:?}"); assert_no_secret(&rendered, "OAuthProfile Debug"); } #[test] fn dpop_private_scalar_never_appears_in_debug_output() { let key = oauth_core::dpop::key::DpopKey::generate().expect("key generates"); let rendered = format!("{key:?}"); assert!( rendered.contains(oauth_core::REDACTED), "DPoP key Debug must redact: {rendered:?}" ); // The persisted file holds the real scalar; the Debug form must not repeat // any of it. Extract the `d` member and prove absence. let dir = tempfile::tempdir().expect("tempdir creates"); let path = dir.path().join("dpop-key.json"); key.save_to_file(&path).expect("key persists"); let raw = std::fs::read_to_string(&path).expect("key file reads"); let scalar = raw.parse::().expect("key file is JSON")["d"] .as_str() .expect("key file carries a `d` scalar") .to_owned(); assert!( !scalar.is_empty(), "scalar must be non-empty to prove absence" ); assert!( !rendered.contains(&scalar), "DPoP private scalar leaked into Debug output" ); } #[test] fn tokens_never_appear_in_debug_or_log_output() { let response = oauth_core::token::TokenResponse { access_token: SecretString::from(ACCESS_TOKEN_SENTINEL), token_type: "DPoP".to_owned(), expires_in: 300, refresh_token: Some(SecretString::from(REFRESH_TOKEN_SENTINEL)), scope: Some("demo.read".to_owned()), }; let rendered = format!("{response:?}"); assert_no_secret(&rendered, "TokenResponse Debug"); // The BFF log line for an outbound token call: query/fragment stripped, // header presence recorded only as the marker. let line = server::http_client::loggable_request( "POST", &format!("https://as.example/token?code={CODE_SENTINEL}"), true, true, ); let decorated = format!( "{line} token={} secret={}", server::http_client::redact_secret(ACCESS_TOKEN_SENTINEL), server::http_client::redact_secret(CLIENT_SECRET_SENTINEL), ); assert_no_secret(&decorated, "BFF log line"); // Server-side session Debug + cookie carry no token material. let store = server::session::SessionStore::new(Duration::from_secs(3600)); let created = store.create().expect("session creates"); assert!(store.store_tokens( &created.id, Some(SecretString::from(ACCESS_TOKEN_SENTINEL)), Some(SecretString::from(REFRESH_TOKEN_SENTINEL)), Some("DPoP".to_owned()), )); let rendered = format!("{:?}", store.get(&created.id).expect("session lives")); assert_no_secret(&rendered, "SessionData Debug"); let cookie = server::session::set_cookie_value(&created.id, false, Duration::from_secs(60)); assert_no_secret(&cookie, "session cookie"); } #[test] fn pkce_verifier_never_appears_in_debug_output() { let pending = server::oauth::PendingStore::new(); pending.insert( "xtask-39-state".to_owned(), SecretString::from(VERIFIER_SENTINEL), "xtask-39-session".to_owned(), "https://as.example".to_owned(), ); let record = pending .take("xtask-39-state") .expect("pending record lives"); let rendered = format!("{record:?}"); assert_no_secret(&rendered, "PendingRecord Debug"); assert!( rendered.contains(oauth_core::REDACTED), "redaction marker must be present: {rendered:?}" ); // Single-use: the state never resolves twice (replay defense). assert!(pending.take("xtask-39-state").is_none()); }