diff options
Diffstat (limited to 'packages/meshbay-hub/tests/test_mnp_token.py')
| -rw-r--r-- | packages/meshbay-hub/tests/test_mnp_token.py | 92 |
1 files changed, 92 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_mnp_token.py b/packages/meshbay-hub/tests/test_mnp_token.py new file mode 100644 index 0000000..3aa3115 --- /dev/null +++ b/packages/meshbay-hub/tests/test_mnp_token.py @@ -0,0 +1,92 @@ +"""The MNP token: a member's credential to a node, useless at the hub API. + +A member hands whatever token it presents to every node it connects to (the MNP +handshake). That must not be the hub session token, which opens the hub API — +otherwise a node operator holds a live credential for the member. `POST +/v1/nodes/mnp-token` mints a short-lived, node-audience token for that purpose; +these tests pin that it authorises to a node and is refused by the hub API. +""" + +import pytest + +from meshbay_common.handshake import HandshakeError, authorize_token +from meshbay_common.tokens import MNP_AUD + + +async def _session_token(client, username="mnp_user_test"): + await client.post("/v1/users/register", json={ + "username": username, "email": f"{username}@test.local", "auth_key": "k" * 44}) + r = await client.post("/v1/users/login", json={ + "username": username, "auth_key": "k" * 44}) + return r.json()["access_token"] + + +@pytest.mark.asyncio +async def test_mnp_token_endpoint_needs_a_session(client): + # No Authorization header at all — FastAPI rejects the required header (422), + # like every other authenticated route; the point is it is not minted anonymously. + r = await client.post("/v1/nodes/mnp-token") + assert r.status_code in (401, 403, 422) + + +@pytest.mark.asyncio +async def test_mnp_token_is_minted_for_a_member(client): + tok = await _session_token(client) + r = await client.post("/v1/nodes/mnp-token", + headers={"Authorization": f"Bearer {tok}"}) + assert r.status_code == 200 + assert r.json().get("mnp_token") + + +@pytest.mark.asyncio +async def test_mnp_token_is_refused_at_the_hub_api(client): + """The whole point: the credential a node receives opens nothing at the hub.""" + tok = await _session_token(client, "mnp_api_test") + mnp = (await client.post("/v1/nodes/mnp-token", + headers={"Authorization": f"Bearer {tok}"})).json()["mnp_token"] + # Presenting it to a hub endpoint fails. + r = await client.get("/v1/users/me", headers={"Authorization": f"Bearer {mnp}"}) + assert r.status_code == 401 + + +@pytest.mark.asyncio +async def test_a_session_token_is_refused_by_a_node_but_the_mnp_token_is_not(client): + """The mirror image, at the node's decode: the session token (what the API + accepts) is refused by `authorize_token`, and the MNP token is accepted.""" + from meshbay_hub.auth import hub_public_key_pem + + # Make the member a member of a group so the MNP token carries it. + tok = await _session_token(client, "mnp_node_test") + H = {"Authorization": f"Bearer {tok}"} + gid = (await client.post("/v1/groups", headers=H, json={ + "name": "g", "visibility": "private", "join_policy": "invite"})).json()["group_id"] + mnp = (await client.post("/v1/nodes/mnp-token", headers=H)).json()["mnp_token"] + pk = hub_public_key_pem() + + # The session token is refused by the node handshake (wrong audience). + with pytest.raises(HandshakeError): + authorize_token(tok, pk, group_id=gid) + # The MNP token authorises the member to the node. + peer = authorize_token(mnp, pk, group_id=gid) + assert peer.group_id == gid + + +@pytest.mark.asyncio +async def test_the_mnp_token_is_bound_to_the_node_it_names(client): + """E10: a token minted for node A is refused by node B, so an operator who + captures a member's token cannot replay it to another of the member's nodes.""" + from meshbay_hub.auth import hub_public_key_pem + + tok = await _session_token(client, "mnp_bind_test") + H = {"Authorization": f"Bearer {tok}"} + gid = (await client.post("/v1/groups", headers=H, json={ + "name": "g", "visibility": "private", "join_policy": "invite"})).json()["group_id"] + # A token bound to node A's key. + mnp = (await client.post("/v1/nodes/mnp-token", headers=H, + json={"node_pk": "node-A-pk"})).json()["mnp_token"] + pk = hub_public_key_pem() + # Node B refuses it; node A accepts it. + with pytest.raises(HandshakeError, match="this node"): + authorize_token(mnp, pk, group_id=gid, node_pk_b64="node-B-pk") + peer = authorize_token(mnp, pk, group_id=gid, node_pk_b64="node-A-pk") + assert peer.group_id == gid |