aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_token_hardening.py
Commit message (Collapse)AuthorAgeFilesLines
* fix(hub): present a node-audience token in the handshake, not the hub ↵Christophe Besson3 days1-4/+18
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | session token A member authenticated to a node in the MNP handshake with its hub *session* token — scope=user, valid at the hub API for hours. A node operator is in the threat model, so this handed them a live hub credential for the member: enough to enumerate the member's other groups, act as them, and (before the previous commit closed it) take the account over. The node genuinely needs a hub-signed membership assertion, so the fix is to make that a separate credential that opens nothing at the hub API. Two audiences signed by the one hub key (meshbay_common/tokens.py): - HUB_API_AUD — session tokens (login, device-auth, node-auth, refresh), used for hub calls and signaling. decode_access_token now binds this audience, so an MNP token cannot be replayed against the hub API. - MNP_AUD — a short-lived token a member presents to a node and nothing else, from POST /v1/nodes/mnp-token. authorize_token now binds this audience, so a session token presented to a node is refused. This closes the disclosure. The node's own self-decode (hub_client.py) reads its node token with audience=HUB_API_AUD. The client fetches the MNP token inside transport.connect() (and on every reconnect) using the session token, so callers are unchanged and signaling keeps using the session token. No regression to a long session: the MNP token is checked once, at the handshake, before any proof — a film already playing is not re-authenticated, so a 15-minute token does not interrupt a 4-hour film; reconnects refetch a fresh one. Denylist and membership checks are unchanged (the MNP token carries sub/jti/groups). Tests: authorize_token refuses a session/no-audience token and accepts an MNP token; the hub API refuses an MNP token; POST /v1/nodes/mnp-token is minted only for a member's own session. Verified red-before/green-after; common, node and hub suites green (the pre-existing test_cli_golden failure is an argparse/pytest prog artifact unrelated to this change). Still to do before deploy (B2): bump the MNP version and client.minimum so a stale desktop client is told to update rather than getting a handshake refusal, update docs/MESHBAY_DESIGN.md and MESHBAY_NODE_PROTOCOL.md, and validate against a real node locally, then deploy hub+node+SPA atomically. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(hub): require exp, sub and a known scope when decoding access tokensChristophe Besson3 days1-0/+114
One Ed25519 key signs four kinds of token — user access, node access, revocation broadcasts (no exp, no sub, and returned in the body of POST /v1/admin/revoke and pushed to every node) and MHP federation tokens (aud, sub=hub_id, no scope). decode_access_token required none of these, so a hub-signed token with no exp was accepted and separation between the types rested only on which fields each consumer happened to read. Require exp, sub and scope, and reject a scope that is not one of the two access scopes. A revocation or MHP token can no longer be mistaken for a session, and no hub-signed token without an expiry is honoured. A full RFC 5987 aud binding is deliberately not used: the node decodes its own hub-issued token without passing audience, so adding aud would make every already-deployed node reject its own token (InvalidAudienceError) — a coordinated, node-breaking change. scope gives the same purpose separation among the hub's own token types without it. This is non-breaking: every real access token already carries exp, sub and scope, so no session is forced to re-authenticate. test_token_hardening.py holds the refusals (no exp, no scope, unknown scope, a revocation token as bearer) and the paths that must keep working (a real login token, a node token); verified red against the pre-fix auth.py and green after. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>