Fills the five highest-value gaps under agents-docs/features/ so the index covers the system's main cross-context contracts. Each doc follows the feature-template structure and the AGENTS_FEATURES.md contract, with honest TODOs where coverage or behavior couldn't be confirmed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
11 KiB
Authentication
Area: authentication Status: Active Last updated: 2026-05-25
Overview
User identity in Toju is split into two surfaces. A small HTTP credential surface on the signaling server (/api/users/register, /api/users/login) registers and verifies user accounts persisted in TypeORM. A separate WebSocket identify handshake binds a self-asserted identity (oderId + display name + optional description) to a live WebSocket connection so the server can route envelopes. There is no server-issued session token: the client re-asserts identity on every reconnect, and other peers trust the claim as far as the signaling fabric does — i.e. only to the extent that subsequent authorization checks (see access-control) accept it.
The Electron desktop shell adds a third surface — the Local API token store in electron/api/auth-store.ts — which issues short-lived bearer tokens for the in-process HTTP server that hosts the docs site and OpenAPI bundle. That surface is internal to the desktop process and is documented here only because it shares the "authentication" name.
Responsibilities
- Register and authenticate user accounts against the signaling server's
userstable. - Bind a connection-scoped identity to a WebSocket connection via the
identifyenvelope, including profile metadata propagation. - Detect dead WebSocket connections via ping/pong sweeps and reap stale
ConnectedUserrows. - Mint and validate short-lived bearer tokens for the Electron Local API server.
This area does not own:
- Permissions, roles, bans, or membership state → access-control.
- Online / away / busy status, voice presence, game activity → presence.
- The shape of WebSocket envelopes carrying identity claims → websocket-envelopes.
- Profile avatar bytes or per-user assets → product-client
profile-avatardomain.
Key concepts
- AuthUser — server-persisted account:
id(uuid),username(unique),passwordHash,displayName,createdAt. Defined inserver/src/entities/AuthUserEntity.ts. - oderId — client-asserted user identifier sent on
identify. Used as the broadcast routing key. The server trusts it; there is no cryptographic binding between an AuthUser row and theoderIdclaimed over WebSocket. - Identify handshake — first message a client sends on a WebSocket. Carries
oderId,displayName, optionaldescription, optionalprofileUpdatedAt, optionalconnectionScope. - Connection scope — opaque string (typically the signal URL the client connected through). Used together with
oderIdto disambiguate multiple sockets per identity so stale-connection eviction does not loop across signal URLs. - Local API token — bearer token issued by the Electron desktop process, 24 h TTL, kept in-memory and pruned on access.
HTTP credential surface
Mounted at /api/users (see server/src/routes/index.ts:20). All payloads are JSON.
POST /api/users/register
Request:
{
"username": "string (required, unique)",
"password": "string (required)",
"displayName": "string (optional, defaults to username)"
}
Response (201):
{ "id": "uuid", "username": "string", "displayName": "string" }
Errors:
- 400
Missing username/password— either field absent. - 409
Username taken— username already exists.
POST /api/users/login
Request: { "username": "string", "password": "string" }
Response (200): { "id": "uuid", "username": "string", "displayName": "string" }
Errors:
- 401
Invalid credentials— no row matches, or stored hash differs.
No session cookie, JWT, or bearer token is issued — the response is purely informational. The client is expected to remember the username and re-present it via the WebSocket identify handshake on every reconnect.
WebSocket identify handshake
handleIdentify (server/src/websocket/handler.ts:112) processes the first envelope a client sends. It:
- Reads
oderId(falls back toconnectionIdwhen absent). - Reads
connectionScope(opaque routing key). - Reads / normalizes
displayName,description,profileUpdatedAt. - Mutates the
ConnectedUserrow inconnectedUsers. - If any of
displayName/description/profileUpdatedAtchanged, rebroadcastsuser_joinedto every server the user is currently in.
identify itself is unauthenticated — the server does not consult AuthUserEntity here. Authorization happens later, at join_server time, via authorizeWebSocketJoin (documented in access-control).
identify is the canonical channel for profile updates. Renaming yourself or updating your description means resending identify; the rebroadcast pushes the new profile to peers without disconnect/reconnect.
Heartbeat and dead-connection sweep
Defined in server/src/websocket/index.ts:19–75:
PING_INTERVAL_MS = 30_000— server pings every connection every 30 s.PONG_TIMEOUT_MS = 45_000— a connection whoselastPongis older than 45 s is closed and removed fromconnectedUsers.
lastPong is bumped on any inbound frame (not just pong), so an active client cannot be reaped while sending traffic. Eviction triggers handleLeaveServer for every server the connection had joined, which in turn emits user_left if no other connection of the same oderId still holds the server.
Electron Local API token store
electron/api/auth-store.ts mints opaque bearer tokens used by the Local API server (electron/api/router.ts) to gate calls to /api/auth/login and other authenticated routes. Tokens have a 24 h TTL and are kept in-memory only — they do not persist across desktop restarts. Pruning happens lazily on lookup.
This surface is not the same identity as the signaling server's AuthUser. It is a desktop-local affordance for the in-process HTTP server that serves docs and plugin APIs; the renderer never sees these tokens.
Business rules and invariants
- Usernames are unique at the database level (
@Column('text', { unique: true })onAuthUserEntity.username) AND pre-checked in the route handler. Comparison is case-sensitive. - Passwords are hashed with unsalted SHA-256 (
crypto.createHash('sha256'),routes/users.ts:8). There is no salting, no peppering, no iteration count, no Argon2/bcrypt. This is a known weakness — see Security below. - The signaling server never issues a session token. Identity is re-asserted on every reconnect via
identify. - The
identifyclaim is not verified againstAuthUserEntity. Two clients can claim the sameoderId; only(oderId, connectionScope)is used to deduplicate eviction. user_joinedis only re-broadcast onidentifywhen at least one ofdisplayName/description/profileUpdatedAtactually changed — duplicate identifies are silent.- Dead-connection sweep runs continuously. A client that goes silent for ≥ 45 s is treated as disconnected.
Technical implementation
Server (signaling)
- HTTP routes:
server/src/routes/users.ts, mounted at/api/usersinserver/src/routes/index.ts. - CQRS handlers:
server/src/cqrs/commands/handlers/registerUser.ts,server/src/cqrs/queries/handlers/getUserByUsername.ts,server/src/cqrs/queries/handlers/getUserById.ts. - Entity:
server/src/entities/AuthUserEntity.ts(userstable). - Migration:
server/src/migrations/1000000000000-InitialSchema.ts. - WebSocket handshake:
server/src/websocket/handler.ts::handleIdentify(line 112). ConnectedUsershape:server/src/websocket/types.ts.- Heartbeat sweep:
server/src/websocket/index.ts.
Product client
- Domain:
toju-app/src/app/domains/authentication/. - Service:
authentication.service.ts(login / register HTTP calls). - Components:
login.component.ts,register.component.ts. - Model:
authentication.model.ts.
Electron
- Local API token store:
electron/api/auth-store.ts. - Local API router:
electron/api/router.ts(/api/auth/loginendpoint).
Key types
AuthUserEntity— server account row.ConnectedUser— live WebSocket connection state, includingoderId,connectionScope,lastPong.
Testing
- E2E:
e2e/tests/auth/user-session-data-isolation.spec.ts— verifies session-level data isolation between users. - TODO: no unit specs were located for
server/src/routes/users.ts,handleRegisterUser,getUserByUsername/getUserById,handleIdentify, the Electron/api/auth/loginproxy, or thetoju-appauthentication services. - TODO: no happy-path login/register E2E exists today.
Security considerations
- Password hashing is unsalted SHA-256. Vulnerable to rainbow-table and parallel GPU attacks. Replacing with Argon2id or bcrypt is the obvious upgrade path and is currently a TODO.
- No rate limiting on
/login. Auserstable with weak hashes is exposed to credential stuffing and online brute-force. identifyis unauthenticated. Any WebSocket can claim anyoderId. The real authorization gate isauthorizeWebSocketJoinonjoin_server, which checks membership / invite / password against the access-control tables — until that gate is crossed, an unverifiedoderIdcannot do anything meaningful beyond joining the public lobby.- No reuse-prevention on
displayName. Two distinct accounts may carry the same display name. UI must therefore disambiguate byoderIdwhere identity actually matters. - Local API tokens never leave the desktop process and have a 24 h TTL — they are not a credential primitive for the signaling server.
Performance considerations
/registerand/loginare O(1) lookups against aUNIQUEindex onusername. No caching layer.identifyis O(serversJoinedByThisConnection) on profile change because of the rebroadcast loop; profile updates are rare so this is negligible.- Dead-connection sweep is O(connections) per
PING_INTERVAL_MS; trivially scalable for a single-process signaling server.
Known issues and limitations
- Unsalted SHA-256 password hashing. Highest-priority hardening target.
- No password reset, email confirmation, MFA, or account recovery.
- No audit log of register/login events.
- No binding between
AuthUserEntity.idand the claimedoderId. A future hardening pass should require the client to prove possession of anAuthUsercredential before the server accepts anidentifypayload that names that user — likely via a signed challenge. - No spec coverage for the HTTP credential surface or the identify handshake.
Related features
- access-control — consumes the
oderIdclaimed viaidentifyto authorize server joins, role lookups, and moderation actions. - presence —
identifyis the canonical channel for profile-metadata updates that presence broadcasts forward. - websocket-envelopes — owns the wire shape of
identify,user_joined, andaccess_denied. - ipc-bridge — the Electron Local API token store lives behind the same IPC boundary as other privileged operations.
Changelog
| Date | Change |
|---|---|
| 2026-05-25 | Initial documentation |