aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/auth.py28
1 files changed, 26 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..309738c 100644
--- a/packages/meshbay-hub/src/meshbay_hub/auth.py
+++ b/packages/meshbay-hub/src/meshbay_hub/auth.py
@@ -235,12 +235,36 @@ def issue_access_token(
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.
+
+ `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.
+
+ 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.
+ """
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,
+ options={"require": ["exp", "sub", "scope"]},
+ )
+ if payload.get("scope") not in ("user", "node"):
+ raise jwt.InvalidTokenError("unrecognised token scope")
+ return payload
# ── Email encryption at rest ──────────────────────────────────────────────────