Summary
Add an expiring token credential so a client can persist its session locally and re-establish an authenticated connection without re-entering a username/password on every connect. This is primarily a UX/reliability need for mobile, where the page is suspended and the WebSocket drops frequently, but it benefits every client.
This issue is documentation/planning only — capturing scope to return to later.
Current behavior
Authentication happens once per connection, in the WebSocket prelude (src/server.rs): the client sends Auth { username, password }, the auth actor verifies it with Argon2id (src/auth.rs → AuthResult::Ok { user_id }), and the server replies AuthOk { is_admin }. There is no session token and no persistence:
- The frontend (
frontend/src/lib/Login.svelte, frontend/src/lib/ws/connection.ts) does not store credentials. The reconnect loop re-opens the socket with backoff, but the prelude still requires a fresh Auth, so a dropped connection or a reopened app forces the user to log in again.
- Caching the raw password client-side to auto-reauth would be the wrong fix (a long-lived plaintext secret in storage).
An expiring token is the standard way to make a session safe to persist and cheap to resume.
Why a token beats persisting the password. A short-lived, expiring token saved as the session and reused on each connect is both more reliable and safer than storing the password itself and re-sending it on every (re)connect:
- The long-lived secret (the password) is sent over the wire exactly once, at initial login — not on every reconnect. Fewer transmissions of the real credential is a smaller exposure surface.
- A token can expire, rotate, and be revoked independently; a stored password can't be invalidated without changing the actual password.
- A leaked token is bounded (short lifetime, single device, revocable); a leaked stored password compromises the account itself.
- The server can verify a token cheaply (hash lookup) instead of running the full Argon2id verification on every reconnect.
Proposed scope
Backend (src/)
- Token issuance — on a successful
Auth (and NewUser), mint a session token and return it to the client (e.g. extend AuthOk, or a dedicated SessionToken { token, expires_at } event).
- Token auth in the prelude — accept a new prelude command, e.g.
AuthToken { token }, alongside Auth / NewUser. A valid, unexpired token authenticates the session and yields the same AuthOk { is_admin } outcome.
- Storage — a new DB table (sqlx migration), e.g.
session_tokens:
user_id
token_hash (store a hash, never the raw token — same principle as credentials)
expires_at, created_at, last_used_at
- optional device/browser label
- optional
revoked flag (or rely on deletion)
ON DELETE CASCADE from users, so deleting a user invalidates their tokens.
- Expiry, rotation, refresh — tokens expire; decide on a sliding refresh (extend/rotate on use) vs. fixed lifetime + explicit refresh. Rotation on use limits the blast radius of a leaked token.
- Revocation — invalidate tokens on logout, on password change/reset (
UpdatePassword / ResetPassword), and provide an admin/self "log out all sessions". Account deletion already cascades.
- Cleanup — prune expired/revoked tokens via the time-based reaper (
src/reaper.rs).
Frontend (frontend/)
- Persist + resume — store the token (e.g.
localStorage), send AuthToken automatically on connect/reconnect, and fall back to the login screen only when the token is missing/expired/rejected. Clear it on logout and on NoAuth.
Interplay with just-in-time authorization
The repo re-checks admin status at the moment of each privileged action rather than trusting a session flag (see README "Just-in-time authorization"). A token must only establish identity (user_id); it must not freeze privileges. A demoted admin's still-valid token keeps working as a normal user, losing admin authority on the next privileged command — exactly as a password session does today.
Security considerations
- A token is a bearer credential — treat it like a password: store only a hash server-side, redact it in logs (mirror the existing
Password/PasswordHash Debug redaction), and require HTTPS in production.
localStorage is reachable by XSS; weigh storage options and lifetime. Short-ish expiry + rotation limits exposure.
- Consider a per-token device label so a user can review/revoke individual sessions.
Relationship to #11 (Web Push)
This is likely a prerequisite for #11 to feel right: clicking a push notification should open Relay and land in the relevant room without a login wall. That requires a persisted, resumable session — i.e. this token system. The two features should be designed to fit together (persisted token → silent reconnect → deep-link to room).
Acceptance sketch (for when this is picked up)
- After logging in once, closing and reopening the app (or a dropped/reconnected socket) resumes the session without re-entering credentials, until the token expires.
- Logout and password change invalidate the token; a stale token cleanly falls back to the login screen.
- Tokens are stored hashed, expire, and are reaped.
Related
Summary
Add an expiring token credential so a client can persist its session locally and re-establish an authenticated connection without re-entering a username/password on every connect. This is primarily a UX/reliability need for mobile, where the page is suspended and the WebSocket drops frequently, but it benefits every client.
This issue is documentation/planning only — capturing scope to return to later.
Current behavior
Authentication happens once per connection, in the WebSocket prelude (
src/server.rs): the client sendsAuth { username, password }, the auth actor verifies it with Argon2id (src/auth.rs→AuthResult::Ok { user_id }), and the server repliesAuthOk { is_admin }. There is no session token and no persistence:frontend/src/lib/Login.svelte,frontend/src/lib/ws/connection.ts) does not store credentials. The reconnect loop re-opens the socket with backoff, but the prelude still requires a freshAuth, so a dropped connection or a reopened app forces the user to log in again.An expiring token is the standard way to make a session safe to persist and cheap to resume.
Why a token beats persisting the password. A short-lived, expiring token saved as the session and reused on each connect is both more reliable and safer than storing the password itself and re-sending it on every (re)connect:
Proposed scope
Backend (
src/)Auth(andNewUser), mint a session token and return it to the client (e.g. extendAuthOk, or a dedicatedSessionToken { token, expires_at }event).AuthToken { token }, alongsideAuth/NewUser. A valid, unexpired token authenticates the session and yields the sameAuthOk { is_admin }outcome.session_tokens:user_idtoken_hash(store a hash, never the raw token — same principle ascredentials)expires_at,created_at,last_used_atrevokedflag (or rely on deletion)ON DELETE CASCADEfromusers, so deleting a user invalidates their tokens.UpdatePassword/ResetPassword), and provide an admin/self "log out all sessions". Account deletion already cascades.src/reaper.rs).Frontend (
frontend/)localStorage), sendAuthTokenautomatically on connect/reconnect, and fall back to the login screen only when the token is missing/expired/rejected. Clear it on logout and onNoAuth.Interplay with just-in-time authorization
The repo re-checks admin status at the moment of each privileged action rather than trusting a session flag (see README "Just-in-time authorization"). A token must only establish identity (
user_id); it must not freeze privileges. A demoted admin's still-valid token keeps working as a normal user, losing admin authority on the next privileged command — exactly as a password session does today.Security considerations
Password/PasswordHashDebug redaction), and require HTTPS in production.localStorageis reachable by XSS; weigh storage options and lifetime. Short-ish expiry + rotation limits exposure.Relationship to #11 (Web Push)
This is likely a prerequisite for #11 to feel right: clicking a push notification should open Relay and land in the relevant room without a login wall. That requires a persisted, resumable session — i.e. this token system. The two features should be designed to fit together (persisted token → silent reconnect → deep-link to room).
Acceptance sketch (for when this is picked up)
Related
src/auth.rs, prelude insrc/server.rs,frontend/src/lib/Login.svelte,frontend/src/lib/ws/.