aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/api/nodes.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/api/nodes.py')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/nodes.py43
1 files changed, 41 insertions, 2 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/nodes.py b/packages/meshbay-hub/src/meshbay_hub/api/nodes.py
index 7478173..403f450 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/nodes.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/nodes.py
@@ -11,15 +11,54 @@ from pydantic import BaseModel
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
-from meshbay_hub.api.deps import get_current_user
+from meshbay_hub.api.deps import get_current_user, require_user_scope
from meshbay_hub.api.middleware import limiter
from meshbay_hub.api.netutil import client_ip
-from meshbay_hub.auth import issue_access_token
+from meshbay_hub.auth import issue_access_token, issue_mnp_token
from meshbay_hub.db.engine import get_db
from meshbay_hub.db.models import GroupMember, IPLog, Node, User
router = APIRouter(prefix="/v1/nodes", tags=["nodes"])
+
+class MnpTokenRequest(BaseModel):
+ # The base64 Ed25519 key of the node this token is for. The token is bound
+ # to it (E10), so it cannot be replayed to another node. The client knows it
+ # from `/v1/groups/{id}/nodes` before it connects.
+ node_pk: str = ""
+
+
+@router.post("/mnp-token")
+@limiter.limit("60/minute")
+async def mnp_token(
+ request: Request,
+ body: MnpTokenRequest | None = None,
+ current_user: User = Depends(require_user_scope),
+ db: AsyncSession = Depends(get_db),
+):
+ """Mint the short-lived token a member presents to a node in the MNP handshake.
+
+ Asked for with the member's own session token (require_user_scope, so a node
+ daemon token cannot mint one). The result carries the member's current group
+ membership, `aud=MNP_AUD` and the target node's key, so it authorises the
+ member to **that** node only and is refused by the hub API and by any other
+ node. Short-lived on purpose; the client refetches it for a new connection or
+ a reconnect, and it is checked only at the handshake, so a film already
+ playing is never interrupted by its expiry.
+
+ The hub does not verify the node key it is handed — binding the token to it
+ only *restricts* the token to whatever node holds that key, which is the one
+ the client is connecting to; a wrong key yields a token no node will accept.
+ """
+ rows = await db.execute(
+ select(GroupMember.group_id).where(GroupMember.user_id == current_user.id))
+ group_ids = [gid for (gid,) in rows.all()]
+ return {
+ "mnp_token": issue_mnp_token(current_user.id, groups=group_ids,
+ node_pk=(body.node_pk if body else "")),
+ "expires_in": 900,
+ }
+
NODE_AUTH_TIMESTAMP_WINDOW = 60 # seconds