diff options
Diffstat (limited to 'packages/meshbay-hub/tests')
| -rw-r--r-- | packages/meshbay-hub/tests/test_federation.py | 64 | ||||
| -rw-r--r-- | packages/meshbay-hub/tests/test_public_groups_toggle.py | 25 |
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 |