The backend has no authentication or authorization. It trusts whatever identity the client sends, and does not check whether a caller is allowed to touch a given room.
Clerk runs in the frontend only (apps/web/src/main.tsx). No token ever reaches the backend — there is no JWT verification, no session, no Authorization handling anywhere in apps/backend/app.
This is not a regression from the Python migration. The Spring Boot backend had none either. It was raised during review of #7 and deliberately deferred to its own PR.
Where identity is taken on trust
| Site |
Problem |
apps/backend/app/schemas/rooms.py:12 |
createdBy comes from the request body, so a room can be created as any user |
apps/backend/app/schemas/submissions.py:11 |
userId comes from the request body, so a submission can be attributed to any user |
apps/backend/app/main.py:61 |
userId is a query parameter on /ws/room/{id}, so presence can be claimed as any user |
apps/backend/app/main.py:76 |
/ws/yjs/{room_id} has no check at all — anyone who knows a room id can read and write that room's shared editor document |
The Yjs socket is the one that matters most: room ids are UUIDs so they aren't guessable, but there is nothing stopping a client that has one from joining a session it was never invited to.
What needs to happen
- Verify a Clerk session token on the backend and derive the user from it, rather than reading an id off the request.
- Remove
createdBy and userId from request bodies and query strings. They become properties of the authenticated caller.
- Authorize room access — check membership before accepting either websocket and before room-scoped reads and writes.
- Decide what "allowed in a room" means. There is no concept of invitation or ownership beyond
rooms.created_by and the room_members table, and room_members is currently presence, not permission. That distinction needs a decision before any of this can be enforced.
Point 4 is the real design question. The rest follows from it.
Notes
- Websockets cannot send an
Authorization header from the browser. The token has to arrive as a query parameter or as a first message after connect, and be verified before accept(). Worth settling early, since it shapes the whole approach.
- The frontend already holds a Clerk session, so the client side is mostly a matter of attaching the token.
- Nothing is deployed yet, so there is no live exposure. This should land before anything is.
Raised in review of #7 (apps/backend/app/schemas/rooms.py).
The backend has no authentication or authorization. It trusts whatever identity the client sends, and does not check whether a caller is allowed to touch a given room.
Clerk runs in the frontend only (
apps/web/src/main.tsx). No token ever reaches the backend — there is no JWT verification, no session, noAuthorizationhandling anywhere inapps/backend/app.This is not a regression from the Python migration. The Spring Boot backend had none either. It was raised during review of #7 and deliberately deferred to its own PR.
Where identity is taken on trust
apps/backend/app/schemas/rooms.py:12createdBycomes from the request body, so a room can be created as any userapps/backend/app/schemas/submissions.py:11userIdcomes from the request body, so a submission can be attributed to any userapps/backend/app/main.py:61userIdis a query parameter on/ws/room/{id}, so presence can be claimed as any userapps/backend/app/main.py:76/ws/yjs/{room_id}has no check at all — anyone who knows a room id can read and write that room's shared editor documentThe Yjs socket is the one that matters most: room ids are UUIDs so they aren't guessable, but there is nothing stopping a client that has one from joining a session it was never invited to.
What needs to happen
createdByanduserIdfrom request bodies and query strings. They become properties of the authenticated caller.rooms.created_byand theroom_memberstable, androom_membersis currently presence, not permission. That distinction needs a decision before any of this can be enforced.Point 4 is the real design question. The rest follows from it.
Notes
Authorizationheader from the browser. The token has to arrive as a query parameter or as a first message after connect, and be verified beforeaccept(). Worth settling early, since it shapes the whole approach.Raised in review of #7 (
apps/backend/app/schemas/rooms.py).