diff --git a/swarm-queue-client/src/lib.rs b/swarm-queue-client/src/lib.rs index 862d72a6..e998648d 100644 --- a/swarm-queue-client/src/lib.rs +++ b/swarm-queue-client/src/lib.rs @@ -296,13 +296,7 @@ async fn mint_token(http: &reqwest::Client, cfg: &QueueConfig) -> Result Result reqwest::RequestBuilder { + http.post(&cfg.token_endpoint) + .basic_auth(&cfg.client_id, Some(secret)) + .form(&[("grant_type", "client_credentials")]) +} + /// Build the HTTP client used to reach `cfg.token_endpoint`, trusting /// `cfg.ca_file` when set. Shared by [`connect`]'s auth callback and by /// [`mint_token_for`] — anything presenting this identity's credentials to @@ -533,4 +556,65 @@ mod tests { std::env::remove_var("SWARM_QUEUE_HALF_NATS_URL"); } } + + fn token_cfg() -> QueueConfig { + QueueConfig { + url: "nats://127.0.0.1:4222".to_owned(), + token_endpoint: "https://auth.example.com/api/oidc/token".to_owned(), + client_id: "hive-alpha".to_owned(), + client_secret_file: PathBuf::from("/nonexistent"), + ca_file: None, + } + } + + /// 🩸 THE REGRESSION TEST FOR AN OUTAGE THAT RAN FOR WEEKS. + /// + /// The credentials used to go in the form body (`client_secret_post`). + /// Authelia's client registration allows only `client_secret_basic`, so + /// every token request was refused — and the refusals tripped a rate + /// limiter whose 429 then arrived *before* the credentials were evaluated, + /// so the error naming the cause appeared roughly once an hour inside a + /// continuous storm of a different error. + /// + /// This asserts the *shape of the request* rather than a server's reply, + /// which is the whole point: it fails on the old code with no identity + /// provider, no deployment and no network. + #[test] + fn the_token_request_authenticates_with_http_basic() { + let req = token_request(&reqwest::Client::new(), &token_cfg(), "s3cret") + .build() + .expect("the token request must build"); + + let auth = req + .headers() + .get(reqwest::header::AUTHORIZATION) + .expect("credentials must travel in the Authorization header") + .to_str() + .expect("the header is ascii"); + assert!( + auth.starts_with("Basic "), + "must be client_secret_basic, got: {auth}" + ); + + let body = std::str::from_utf8( + req.body() + .and_then(reqwest::Body::as_bytes) + .expect("the request has an in-memory body"), + ) + .expect("the body is utf-8"); + assert!( + body.contains("grant_type=client_credentials"), + "the grant type still belongs in the body, got: {body}" + ); + // The half that was actually broken: a secret in the body is both the + // wrong auth method for our registration and a value proxies log. + assert!( + !body.contains("client_secret"), + "the secret must not be in the request body, got: {body}" + ); + assert!( + !body.contains("client_id"), + "the client id belongs in the Basic credentials, got: {body}" + ); + } }