diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/auth.py')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/auth.py | 65 |
1 files changed, 63 insertions, 2 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/auth.py b/packages/meshbay-hub/src/meshbay_hub/auth.py index d038027..0d0fd95 100644 --- a/packages/meshbay-hub/src/meshbay_hub/auth.py +++ b/packages/meshbay-hub/src/meshbay_hub/auth.py @@ -25,6 +25,8 @@ from cryptography.hazmat.primitives.hashes import SHA256 from cryptography.hazmat.primitives.kdf.argon2 import Argon2id from cryptography.hazmat.primitives.kdf.hkdf import HKDF +from meshbay_common.tokens import HUB_API_AUD, MNP_AUD + # Argon2id parameters — versioned for gradual migration _ARGON2_LANES = 4 _ARGON2_KEY_LEN = 32 @@ -230,17 +232,76 @@ def issue_access_token( "exp": now + ttl, "groups": groups or [], "scope": scope, + # This is a hub-API credential. The node handshake binds MNP_AUD and + # refuses it, so a session token disclosed to a node opens nothing at + # the hub (see meshbay_common.tokens). + "aud": HUB_API_AUD, + } + return jwt.encode(payload, _hub_sk_pem, algorithm="EdDSA") + + +def issue_mnp_token(user_id: str, groups: list[str] | None = None, + node_pk: str | None = None, ttl: int = 900) -> str: + """Issue the short-lived token a member presents to a node in the handshake. + + `aud=MNP_AUD`, so it is accepted by `authorize_token` and refused by the hub + API. It is checked once, at the handshake, before any proof — so a short + lifetime does not interrupt a long transfer or a film already playing; only a + fresh connection or a reconnect needs a fresh one. It carries the same + `sub`/`groups`/`jti` the node authorises and denylists on. + + `node` names the node this token is for (its base64 Ed25519 key), so it + cannot be replayed to another node the member also belongs to — the node + checks it in `authorize_token` (E10). The client knows the target node's key + before it connects and asks for a token bound to it. + """ + if _hub_sk_pem is None: + raise RuntimeError("Hub keypair not loaded") + now = int(time.time()) + payload = { + "iss": _hub_id, + "sub": user_id, + "jti": str(uuid.uuid4()), + "iat": now, + "exp": now + ttl, + "groups": groups or [], + "node": node_pk or "", + "scope": "user", + "aud": MNP_AUD, } return jwt.encode(payload, _hub_sk_pem, algorithm="EdDSA") def decode_access_token(token: str) -> dict: - """Verify and decode an access token. Raises on failure.""" + """Verify and decode an access token. Raises on failure. + + This is the **hub-API** decode. It binds `audience=HUB_API_AUD` and requires + `exp`, `sub` and `scope`. One Ed25519 key signs several kinds of token — + session tokens (aud=HUB_API_AUD), the MNP token a member presents to a node + (aud=MNP_AUD), revocation broadcasts (no `exp`/`sub`, handed to admins and + pushed to every node), and MHP federation tokens (aud=peer hub). Binding the + audience here means only a session token opens the hub API: an **MNP token + disclosed to a node cannot be replayed against the hub**, which is the whole + point of splitting the two (see meshbay_common.tokens). Requiring `exp` + refuses any hub-signed token with no expiry, and `scope` must still name one + of the two access scopes. + + The node handshake uses its own decode (`meshbay_common.handshake`), which + binds `MNP_AUD` instead; the node's own self-decode of its node token passes + `audience=HUB_API_AUD` (hub_client.py), so both sides move together. + """ if _hub_pk_pem is None: raise RuntimeError("Hub keypair not loaded") # Clock-skew tolerance (meshbay_common.handshake.JWT_LEEWAY_SECONDS): a # client whose clock is a little fast must still be able to call the API. - return jwt.decode(token, _hub_pk_pem, algorithms=["EdDSA"], leeway=60) + payload = jwt.decode( + token, _hub_pk_pem, algorithms=["EdDSA"], leeway=60, + audience=HUB_API_AUD, + options={"require": ["exp", "sub", "scope", "aud"]}, + ) + if payload.get("scope") not in ("user", "node"): + raise jwt.InvalidTokenError("unrecognised token scope") + return payload # ── Email encryption at rest ────────────────────────────────────────────────── |