| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153 |
- //! 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::<serde_json::Value>().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());
- }
|