Skip to content

Expiring session tokens for persistent login (mobile / reconnect) #12

Description

@RagingRedRiot

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.rsAuthResult::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/)

  1. 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).
  2. 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.
  3. 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.
  4. 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.
  5. Revocation — invalidate tokens on logout, on password change/reset (UpdatePassword / ResetPassword), and provide an admin/self "log out all sessions". Account deletion already cascades.
  6. Cleanup — prune expired/revoked tokens via the time-based reaper (src/reaper.rs).

Frontend (frontend/)

  1. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions