aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/auth.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/auth.py')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/auth.py63
1 files changed, 47 insertions, 16 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/auth.py b/packages/meshbay-hub/src/meshbay_hub/auth.py
index 309738c..e182ba0 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,6 +232,36 @@ 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,
+ 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.
+ """
+ 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 [],
+ "scope": "user",
+ "aud": MNP_AUD,
}
return jwt.encode(payload, _hub_sk_pem, algorithm="EdDSA")
@@ -237,22 +269,20 @@ def issue_access_token(
def decode_access_token(token: str) -> dict:
"""Verify and decode an access token. Raises on failure.
- `exp`, `sub` and `scope` are **required**, and `scope` must name one of the
- two access scopes. 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`). Without these
- requirements a token with no `exp` was accepted, and separation between the
- types rested only on which fields each consumer happened to read. Requiring
- `scope` here turns a revocation or MHP token away before it can be mistaken
- for a session, and requiring `exp` refuses any hub-signed token that never
- expires.
+ 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.
- 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.
+ 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")
@@ -260,7 +290,8 @@ def decode_access_token(token: str) -> dict:
# client whose clock is a little fast must still be able to call the API.
payload = jwt.decode(
token, _hub_pk_pem, algorithms=["EdDSA"], leeway=60,
- options={"require": ["exp", "sub", "scope"]},
+ audience=HUB_API_AUD,
+ options={"require": ["exp", "sub", "scope", "aud"]},
)
if payload.get("scope") not in ("user", "node"):
raise jwt.InvalidTokenError("unrecognised token scope")