swarm-queue-client: trim token_request comment block under the 30-line lint

The scope-parameter addition pushed the doc comment on token_request to 39
lines. Moved the HTTP-Basic incident story and the audience/scope rationale
to the crate README's new "Token request shape" section (docs/ is markdown,
exempt from the lint); the code comment keeps the pointer plus the one-line
summary of the invariant. Refs #4464
This commit is contained in:
atlas 2026-09-17 10:01:11 +02:00
commit d9d6d37951
2 changed files with 44 additions and 33 deletions

View file

@ -48,6 +48,41 @@ would publish it to anything that can read `/proc/<pid>/environ`. It is read
per token request rather than cached, so a rotation the operator believes took
effect actually did.
## Token request shape: HTTP Basic, `audience`, `scope`
`token_request` builds a `client_credentials` grant, authenticated with HTTP
Basic — **never** the form body. Both are legal OAuth 2.0
(`client_secret_basic` vs `client_secret_post`), but a client registration
names one, and authelia's default (and ours) is Basic. Sending the
credentials in the body once got every token request refused with `Client
authentication failed … the registered client is configured to only support
'client_secret_basic'`, which reached the operator as an endless `429`
because the retries tripped a rate limiter whose penalty grew faster than the
retry interval — the 429 arrived _before_ the credentials were ever
evaluated, so the line naming the real cause appeared once an hour. RFC 6749
§2.3.1 says clients SHOULD use Basic, both introspection callers in this
workspace already do, and a secret in a header is one fewer place for a proxy
to log it.
`audience` (RFC 8707 resource indicators) and `scope` are both opt-in;
`None` for either reproduces the request this crate sent before the
parameter existed. Omitted, `audience` gets whatever authelia defaults a
scopeless `client_credentials` grant to — the queue connection's own case,
which has always worked without asking. A caller proving this identity to a
specifically audience-checked receiver (the swarm-otel `oidc/swarm`
authenticator being the first one) has to ask by name, the same way
`swarm-otel.nix`'s own prometheus scrape config already does per target
(`endpoint_params.audience`): a token minted without asking carries `aud:
[]`, and an audience-checked receiver refuses that just as readily as the
wrong one. `scope` follows the same rule and travels with `audience`:
registration is not issuance, and the authorisation server grants no scope
the client never requested. A caller reaching a destination behind
authelia's `/api/authz/auth-request` needs `authelia.bearer.authz` here
however completely the client is registered for it — the failure
`swarm-otel.nix` records against its own client (a scopeless token refused
at introspection with "the requested scope is invalid, unknown, or
malformed") is the same one, one layer down.
## What this crate does not do
It ends at a connected client. `jetstream`/`kv` are **off by default** — what a

View file

@ -460,42 +460,18 @@ fn parse_token_response(status: reqwest::StatusCode, body: &str) -> Result<Cache
/// Build the token request: `client_credentials`, authenticated with HTTP
/// Basic.
///
/// 🩸 **The credentials go in the `Authorization` header, not the form body.**
/// Both are legal OAuth 2.0 — `client_secret_basic` and `client_secret_post` —
/// but a client registration names *one*, and authelia's default (and ours) is
/// Basic. Sending them in the body got every token request refused with
/// `Client authentication failed … the registered client is configured to only
/// support 'client_secret_basic'`, which reached the operator as an endless
/// `429` because the retries tripped a rate limiter whose penalty grew faster
/// than the retry interval. The 429 then arrived *before* the credentials were
/// ever evaluated, so the one line naming the real cause appeared once an hour.
///
/// RFC 6749 §2.3.1 says clients SHOULD use Basic, both introspection callers in
/// this workspace already do, and a secret in a header is one fewer place for a
/// proxy to log it.
/// 🩸 **The credentials go in the `Authorization` header, not the form
/// body.** A client registration names one flavor, and authelia's default
/// (and ours) is Basic — the other one once produced an hour of misleading
/// `429`s before the real cause (every request refused at the auth step)
/// surfaced. `audience` and `scope` are both opt-in, RFC 8707 /
/// registered-scope territory; `None` for either reproduces the request
/// this crate sent before the parameter existed. See the crate README's
/// "Token request shape" section for the incident, the RFC citations, and
/// why each parameter exists.
///
/// Split out of [`mint_token`] so the request's *shape* is testable without a
/// running identity provider — see the tests at the bottom of this file.
///
/// `audience` is RFC 8707 resource-indicator territory, and it is opt-in:
/// omitted, this identity gets whatever audience authelia defaults a
/// scopeless `client_credentials` grant to (the queue connection's own
/// case — it has always worked without asking). A caller proving this
/// identity to a SPECIFIC audience-checked receiver — the swarm-otel
/// `oidc/swarm` authenticator being the first one — has to ask for it by
/// name, the same way `swarm-otel.nix`'s own prometheus scrape config
/// already does per target (`endpoint_params.audience`): a token minted
/// without asking carries `aud: []`, and an audience-checked receiver
/// refuses that just as readily as the wrong one.
///
/// `scope` is opt-in for the same reason and travels with `audience`:
/// registration is not issuance, and the authorisation server grants no scope
/// the client never requested. A caller reaching a destination behind authelia's
/// `/api/authz/auth-request` needs `authelia.bearer.authz` here however
/// completely the client is registered for it — the failure `swarm-otel.nix`
/// records against its own client (a scopeless token refused at
/// introspection with "the requested scope is invalid, unknown, or
/// malformed") is the same one, one layer down.
fn token_request(
http: &reqwest::Client,
cfg: &QueueConfig,