docs(swarm): trim the respond module doc under the 30-line block lint
The discovery narrative belongs in the PR body, which carries it. What stays at the line is what the code cannot say: that issuer_account must be absent and why reaching for Token::new_user reintroduces the bug, the BASE32HEX-vs-BASE32 distinction, and that nats-jwt is a test oracle rather than a runtime dependency.
This commit is contained in:
parent
188e27478a
commit
b22f0ecaa1
1 changed files with 22 additions and 36 deletions
|
|
@ -1,44 +1,30 @@
|
|||
//! Minting the reply the server expects on `msg.reply`.
|
||||
//!
|
||||
//! Two JWTs are involved and **neither** can be produced by `nats-jwt`, for
|
||||
//! the same underlying reason: its `Claims` struct has no `aud`, and both of
|
||||
//! these need one.
|
||||
//! Both JWTs are hand-built because neither is expressible through
|
||||
//! `nats-jwt`: its `Claims` has no `aud`, which the `authorization_response`
|
||||
//! wrapper needs (the asking server's id, so a captured reply cannot be
|
||||
//! replayed elsewhere in the cluster) and the user token needs (the account
|
||||
//! name). Its `IntoNatsClaims` also returns a closed
|
||||
//! `NatsClaims { User, Account }`.
|
||||
//!
|
||||
//! * the **`authorization_response`** wrapper — `aud` is the asking server's
|
||||
//! id, so a captured reply cannot be replayed at a different server in the
|
||||
//! cluster. Its `IntoNatsClaims` trait also returns the closed enum
|
||||
//! `NatsClaims { User, Account }`, so the claim type is inexpressible
|
||||
//! through it twice over.
|
||||
//! * the **user JWT** it carries — in **server-config mode** (which is what
|
||||
//! `nix/host-modules/swarm-nats.nix` renders: `accounts { AUTH, APP }`,
|
||||
//! no operator), `aud` is the *account name* the admitted client lands in,
|
||||
//! and `issuer_account` must be **absent**. `nats_jwt::Token::new_user`
|
||||
//! always sets `issuer_account` and can express no `aud` at all.
|
||||
//! ⛔ **`issuer_account` must be ABSENT from the user token.** It is an
|
||||
//! operator-mode field, and `nix/host-modules/swarm-nats.nix` renders
|
||||
//! *server-config* mode, where its mere presence makes the server refuse the
|
||||
//! client — `Error non operator mode account "AUTH": attempted to use
|
||||
//! issuer_account`, with the responder having answered `granted=true`.
|
||||
//! `nats_jwt::Token::new_user` always sets it, which is why reaching for that
|
||||
//! constructor again would reintroduce the bug.
|
||||
//!
|
||||
//! 🩸 That second one was found by running the thing, not by reading it. The
|
||||
//! responder answered `granted=true` and the server still refused the client,
|
||||
//! logging `Error non operator mode account "AUTH": attempted to use
|
||||
//! issuer_account`. Every unit test passed throughout: they assert the shape
|
||||
//! this module *intends*, and the field that broke it was one nobody had
|
||||
//! reason to assert was missing. `issuer_account` is an operator-mode field —
|
||||
//! it names the account when a *signing* key rather than the account identity
|
||||
//! key signed the token — and in config mode its mere presence is fatal.
|
||||
//! Signing follows `nats-jwt`'s own `sign()`: serialise the claims with `jti`
|
||||
//! **empty**, sha256 that, `BASE32HEX_NOPAD` the digest into `jti`,
|
||||
//! re-serialise, sign `"<b64url header>.<b64url claims>"` with the account
|
||||
//! key. ⚠️ `BASE32HEX`, not `BASE32` — one word, and the only symptom is a
|
||||
//! token the server rejects without saying why.
|
||||
//!
|
||||
//! So both are hand-built here, following the algorithm read out of
|
||||
//! `nats-jwt`'s own `sign()`: serialise the claims with `jti` **empty**,
|
||||
//! sha256 that, `BASE32HEX_NOPAD` the digest into `jti`, re-serialise, and
|
||||
//! sign `"<b64url header>.<b64url claims>"` with the account key.
|
||||
//!
|
||||
//! ⚠️ `BASE32HEX`, not `BASE32`. That single word is the kind of thing that
|
||||
//! produces a token the server rejects with no useful reason, and it is
|
||||
//! copied from the reference implementation rather than from memory — which
|
||||
//! is also why [`tests::hand_built_matches_the_reference`] exists: it builds
|
||||
//! a *user* token both ways and asserts byte equality, so this encoder is
|
||||
//! checked against `nats-jwt` on the shape that crate does model. Trusting it
|
||||
//! on the shapes `nats-jwt` does **not** model has to rest on something
|
||||
//! better than my reading of its source. `nats-jwt` is therefore a
|
||||
//! **dev-dependency**: it is this module's test oracle, not part of the path
|
||||
//! that runs in production.
|
||||
//! `nats-jwt` is therefore a **dev-dependency, not a runtime one**: it is this
|
||||
//! module's test oracle. [`tests::hand_built_matches_the_reference`] builds a
|
||||
//! user token both ways and requires byte equality, so the encoder is checked
|
||||
//! against the reference on the one shape that crate does model.
|
||||
|
||||
use data_encoding::{BASE32HEX_NOPAD, BASE64URL_NOPAD};
|
||||
use nkeys::KeyPair;
|
||||
|
|
|
|||
Loading…
Reference in a new issue