aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests')
-rw-r--r--packages/meshbay-hub/tests/test_federation.py64
-rw-r--r--packages/meshbay-hub/tests/test_public_groups_toggle.py25
2 files changed, 72 insertions, 17 deletions
diff --git a/packages/meshbay-hub/tests/test_federation.py b/packages/meshbay-hub/tests/test_federation.py
index 6035b0c..54f86a7 100644
--- a/packages/meshbay-hub/tests/test_federation.py
+++ b/packages/meshbay-hub/tests/test_federation.py
@@ -48,10 +48,23 @@ class Peer:
serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8,
serialization.NoEncryption())
- def envelope(self, jti: str | None = None) -> str:
+ def envelope(self, jti: str | None = None, audience: str | None = None) -> str:
+ """The shape `federation._issue_mhp_token` actually emits.
+
+ This used to omit `aud`, and so tested a token production never sends:
+ the verifier names an audience, and PyJWT refuses a token carrying
+ `aud` when the caller names none — which meant MHP could not complete
+ one authenticated request between two real hubs while this file stayed
+ green. A fixture narrower than the real thing tests the fixture.
+ """
+ # Read from the module rather than written down here: the receiving
+ # hub's id is what `aud` has to carry, and a copy of it in a test is a
+ # second place for it to drift from.
+ from meshbay_hub.auth import _hub_id
+
now = int(time.time())
return jwt.encode(
- {"iss": self.hub_id, "sub": self.hub_id,
+ {"iss": self.hub_id, "sub": self.hub_id, "aud": audience or _hub_id,
"jti": jti or str(uuid.uuid4()), "iat": now, "exp": now + 300},
self._sk_pem(), algorithm="EdDSA")
@@ -177,3 +190,50 @@ async def test_federation_cannot_revoke_a_user(client):
r = await client.post("/mhp/revoke", headers=peer.header(),
json={"token": peer.revocation("user", "some-user-id")})
assert r.status_code == 202 and r.json()["pruned"] == 0
+
+
+# ── The hub's own identity, and the audience it binds ────────────────────────
+#
+# MHP could not complete one authenticated request between two real hubs, and
+# nothing said so: `_issue_mhp_token` sets `aud`, `_verify_mhp_token` named no
+# audience, and PyJWT refuses a token carrying `aud` when the caller names
+# none. This file stayed green because its envelopes were built by hand
+# without one — a fixture narrower than what production emits tests the
+# fixture. Underneath that, `federation.py` bound `_hub_id` and `_hub_sk_pem`
+# at import, which is *before* `load_hub_keypair` runs: it signed with `None`
+# and called itself `meshbay.org` whatever the instance was named.
+
+
+def test_the_hub_issues_tokens_under_its_configured_identity(app):
+ """Driven through the real issuer, which is the half no test called."""
+ from meshbay_hub.api.federation import _issue_mhp_token
+ from meshbay_hub.auth import hub_id
+
+ assert hub_id() == "test-hub", "the fixture hub is not configured"
+
+ token = jwt.decode(_issue_mhp_token("peer-hub"),
+ options={"verify_signature": False})
+ assert token["iss"] == "test-hub", (
+ "the hub announced an identity that is not its own")
+ assert token["aud"] == "peer-hub"
+
+
+@pytest.mark.asyncio
+async def test_a_token_for_another_hub_is_refused(client):
+ """What the audience binding is for: a token captured here must not be
+ replayable at a peer, and one minted for a peer must not work here."""
+ admin = await _admin(client)
+ peer = Peer("peer-elsewhere.test")
+ await _register_peer(client, admin, peer)
+
+ r = await client.post(
+ "/mhp/directory",
+ headers={"Authorization":
+ f"Bearer {peer.envelope(audience='some-other-hub.test')}"},
+ json={"hub_id": peer.hub_id, "groups": []})
+ assert r.status_code == 401, r.text
+
+ r = await client.post(
+ "/mhp/directory", headers=peer.header(),
+ json={"hub_id": peer.hub_id, "groups": []})
+ assert r.status_code == 202, r.text
diff --git a/packages/meshbay-hub/tests/test_public_groups_toggle.py b/packages/meshbay-hub/tests/test_public_groups_toggle.py
index 0d36b99..e52fa85 100644
--- a/packages/meshbay-hub/tests/test_public_groups_toggle.py
+++ b/packages/meshbay-hub/tests/test_public_groups_toggle.py
@@ -212,24 +212,19 @@ async def test_disabled_hands_a_non_member_no_node(client, db_session):
def _mhp_token(hub_id):
- """A peer-hub JWT, signed with the running hub's own key.
+ """A peer-hub JWT — from the hub's own issuer, which is the point.
- `federation._issue_mhp_token` binds `_hub_sk_pem` at import time, before the
- lifespan loads it, so it cannot be used from a test. This signs directly.
+ This used to sign by hand, and its two comments recorded, accurately, the
+ reasons it had to: `_issue_mhp_token` bound `_hub_sk_pem` at import, before
+ the lifespan loads it, so it signed with `None`; and it sets an `aud` that
+ the verifier named no audience for, which PyJWT refuses outright. Both were
+ written down here as facts to route around rather than as the defects they
+ were — between them MHP could not complete one authenticated request
+ between two real hubs. Fixed at the source, so this can call it.
"""
- import time
- import uuid
+ from meshbay_hub.api.federation import _issue_mhp_token
- import jwt
- from meshbay_hub import auth as hub_auth
-
- now = int(time.time())
- # No `aud`: export_directory verifies without an expected audience, and PyJWT
- # rejects a token that carries `aud` when decode() is given none.
- return jwt.encode(
- {"iss": hub_id, "sub": hub_id,
- "jti": str(uuid.uuid4()), "iat": now, "exp": now + 300},
- hub_auth._hub_sk_pem, algorithm="EdDSA")
+ return _issue_mhp_token(hub_id)
@pytest.mark.asyncio