# 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](./access-control.md)) 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 `users` table. - Bind a connection-scoped identity to a WebSocket connection via the `identify` envelope, including profile metadata propagation. - Detect dead WebSocket connections via ping/pong sweeps and reap stale `ConnectedUser` rows. - 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](./access-control.md). - Online / away / busy status, voice presence, game activity → [presence](./presence.md). - The shape of WebSocket envelopes carrying identity claims → [websocket-envelopes](./websocket-envelopes.md). - Profile avatar bytes or per-user assets → product-client `profile-avatar` domain. ## Key concepts - **AuthUser** — server-persisted account: `id` (uuid), `username` (unique), `passwordHash`, `displayName`, `createdAt`. Defined in `server/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 the `oderId` claimed over WebSocket. - **Identify handshake** — first message a client sends on a WebSocket. Carries `oderId`, `displayName`, optional `description`, optional `profileUpdatedAt`, optional `connectionScope`. - **Connection scope** — opaque string (typically the signal URL the client connected through). Used together with `oderId` to 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:** ```json { "username": "string (required, unique)", "password": "string (required)", "displayName": "string (optional, defaults to username)" } ``` **Response (201):** ```json { "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: 1. Reads `oderId` (falls back to `connectionId` when absent). 2. Reads `connectionScope` (opaque routing key). 3. Reads / normalizes `displayName`, `description`, `profileUpdatedAt`. 4. Mutates the `ConnectedUser` row in `connectedUsers`. 5. If any of `displayName` / `description` / `profileUpdatedAt` changed, rebroadcasts `user_joined` to 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](./access-control.md)). `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 whose `lastPong` is older than 45 s is closed and removed from `connectedUsers`. `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 })` on `AuthUserEntity.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 `identify` claim is **not verified** against `AuthUserEntity`. Two clients can claim the same `oderId`; only `(oderId, connectionScope)` is used to deduplicate eviction. - `user_joined` is only re-broadcast on `identify` when at least one of `displayName` / `description` / `profileUpdatedAt` actually 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/users` in `server/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` (`users` table). - Migration: `server/src/migrations/1000000000000-InitialSchema.ts`. - WebSocket handshake: `server/src/websocket/handler.ts::handleIdentify` (line 112). - `ConnectedUser` shape: `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/login` endpoint). ### Key types - `AuthUserEntity` — server account row. - `ConnectedUser` — live WebSocket connection state, including `oderId`, `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/login` proxy, or the `toju-app` authentication 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`.** A `users` table with weak hashes is exposed to credential stuffing and online brute-force. - **`identify` is unauthenticated.** Any WebSocket can claim any `oderId`. The real authorization gate is `authorizeWebSocketJoin` on `join_server`, which checks membership / invite / password against the access-control tables — until that gate is crossed, an unverified `oderId` cannot 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 by `oderId` where 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 - `/register` and `/login` are O(1) lookups against a `UNIQUE` index on `username`. No caching layer. - `identify` is 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.id` and the claimed `oderId`.** A future hardening pass should require the client to prove possession of an `AuthUser` credential before the server accepts an `identify` payload 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](./access-control.md)** — consumes the `oderId` claimed via `identify` to authorize server joins, role lookups, and moderation actions. - **[presence](./presence.md)** — `identify` is the canonical channel for profile-metadata updates that presence broadcasts forward. - **[websocket-envelopes](./websocket-envelopes.md)** — owns the wire shape of `identify`, `user_joined`, and `access_denied`. - **[ipc-bridge](./ipc-bridge.md)** — the Electron Local API token store lives behind the same IPC boundary as other privileged operations. ## Changelog | Date | Change | |------|--------| | 2026-05-25 | Initial documentation |