fix(swarm): name the account by aud, never issuer_account

The responder answered granted=true and the server still refused every
client:

  Error non operator mode account "AUTH": attempted to use issuer_account

nats_jwt::Token::new_user always sets issuer_account, which is an
operator-mode field naming the account when a signing key rather than the
account identity key signed the token. In server-config mode - what this
module renders - its mere presence is fatal, and the account is named by
the claims' aud instead.

nats-jwt can express neither aud nor the omission, so the user JWT is now
hand-built by the same signer as the response wrapper, and nats-jwt moves
to dev-dependencies as the encoder's test oracle.

Every unit test passed throughout: they assert fields that must be
present, and the defect was a field that must be absent.
This commit is contained in:
atlas 2026-08-14 22:26:02 +02:00 committed by mara
commit 188e27478a
4 changed files with 144 additions and 36 deletions

View file

@ -39,15 +39,19 @@ futures = "0.3"
# is not, and hand-rolling a key format on an auth path is how you get a
# CRC bug nobody reviews.
nkeys = "0.4"
# NATS JWT claim types + signing. The reply this responder sends is a *signed
# user JWT*, whose `jti` is base32(sha256(claims)) and whose header must say
# `ed25519-nkey` - format details with no feedback loop until the server
# rejects the token. Its own deps (data-encoding, nkeys, serde, serde_json,
# sha2) are already in the tree, so this costs no new transitive weight.
nats-jwt = "0.3"
# The jti digest. Already in the tree via nats-jwt; named directly because
# this crate computes one itself for the response wrapper.
# The jti digest: base32hex(sha256(claims)) over every JWT this crate signs.
sha2 = "0.10"
[dev-dependencies]
# A TEST ORACLE, not part of the production path. Neither JWT this crate emits
# is expressible through it - `Claims` has no `aud`, which the response wrapper
# needs (the server id) and the user token needs (the account name), and
# `Token::new_user` always sets `issuer_account`, which a non-operator server
# rejects outright. So both are hand-built, and this crate is what the encoder
# is checked *against*: `respond::tests::hand_built_matches_the_reference`
# builds a user token both ways and requires byte equality, on the one shape
# nats-jwt does model.
nats-jwt = "0.3"
[lints]
workspace = true