diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/api/nodes.py')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/api/nodes.py | 43 |
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 |