Session Management¶
Statement¶
ClaimGuard sessions are stateless. After a successful login, the server issues a signed JWT (HMAC-SHA256, see Cryptography) with a fixed expiry; the client carries it on subsequent requests as a Bearer token. There is no server-side session store, no session cookie, and no session identifier separate from the JWT itself.
Logout is a client-side operation — the client discards the token. The
server does not maintain a revocation list, so a stolen token remains
valid until its exp timestamp. This is the standard trade-off of
stateless JWTs and is the largest gap on this page.
Implementation¶
Token issuance¶
- Endpoint:
POST /api/auth/login(server/src/routes/auth.js). - Payload:
{ id, email, app_role, org_ref, iat, exp }. - Signing:
jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRY })withjsonwebtoken.JWT_SECRETis mandatory at boot (server/src/middleware/auth.js); the server refuses to start without it. See Cryptography for the algorithm (HS256).
Token expiry¶
JWT_EXPIRYdefaults to24hand is configurable per environment via the standardsecrets.jsSecret Manager bootstrap (see Secrets management).- The default is the trade-off between user friction (re-login cadence) and blast radius of a stolen token.
Token transport¶
- Header:
Authorization: Bearer <jwt>is the supported transport. - No cookie storage. The application does not set or read authentication cookies. This avoids the CSRF surface that a cookie-based session would introduce.
- Refresh tokens are not used. The single 24h token is issued at login and the user re-authenticates after expiry.
Token verification¶
server/src/middleware/auth.js (requireAuth) verifies every request:
- Extract the Bearer token from
Authorization. jwt.verify(token, JWT_SECRET)— fails on signature mismatch,expexpiry, or malformed payload.- Re-check
is_activeagainst theuserstable. Disabled accounts are rejected with 401code: USER_DISABLEDregardless of token validity, so an admin disable takes effect on the user's next request — not at the next 24h boundary. - Populate
req.user = { id, email, display_name, full_name, app_role, org_ref }from the current row, not the JWT payload. Role and org changes propagate on the next request as well. - The downstream route then runs role checks (see Authorization) and org-scoped queries.
A failed verification returns 401; the caller must log in again.
Logout¶
- Endpoint:
POST /api/auth/logoutexists for symmetry, but the server-side action is essentially a no-op. The route handler comment inserver/src/routes/auth.jslines 135-139 calls this out: "If we add a sessions table in the future, we would invalidate the session here." - Client behavior: delete the locally stored JWT.
Concurrent sessions¶
Because tokens are stateless and not bound to a session ID, a single user can have multiple valid tokens simultaneously (e.g., one per device). There is no per-user session count or per-device session inventory.
What this means in practice¶
| Scenario | Behavior today |
|---|---|
| User logs out | Local JWT discarded; the token itself remains valid until exp. |
| Admin disables a user | Outstanding tokens are rejected on the next request — requireAuth re-checks is_active per call and returns 401 code: USER_DISABLED. |
| Admin changes a user's role | The new role takes effect on the user's next request. requireAuth rebuilds req.user from the current users row, so requireAdmin / requireSuperAdmin see the updated role without a re-login. |
| User changes password | Existing tokens remain valid until exp. Password change does not invalidate tokens. |
JWT_SECRET is rotated |
All tokens issued under the old secret immediately become invalid; every user must re-login. This is the operational reason JWT_SECRET rotation is comms-coordinated (see Cryptography roadmap A2.5). |
Status¶
implemented — verified 2026-04-29.
What's in place:
- Stateless JWTs signed with mandatory
JWT_SECRET(HS256, ≥32-byte secret). - 24h default expiry, configurable.
- Bearer-token transport; no cookie surface; no CSRF exposure on the auth path.
- No development-only or fallback session path.
- Per-request
is_activere-check and role refresh inrequireAuth— disable / demote takes effect on the user's next request rather than waiting for token expiry. Covered by an integration test inserver/tests/auth/users-crud.test.js.
Known gaps¶
- No server-side revocation of stolen tokens. Token theft is the
remaining window: a stolen token of a still-enabled user is valid
until
exp. Mitigated by the 24h expiry; a shorter-lived token + refresh model or arevoked_jtitable would close this more decisively. - No per-session inventory or "log out all devices" affordance for end users. Listed as a roadmap item for the first customer who asks.
- No rotating refresh-token / sliding-session model. Today the user re-logs at the 24h boundary; a refresh-token model would let the access token shrink to 5–15 minutes while keeping the user session continuous.
Roadmap¶
- Token revocation list — a small
revoked_jtitable consulted byrequireAuth, populated on logout / password-change / token-theft events. Trade-off: re-introduces a small amount of session state. - Refresh-token model with short-lived (5–15 min) access tokens and longer-lived refresh tokens, gated on the same revocation list.
- Per-user session inventory with a "log out everywhere" UX, practical only after the revocation-list model is in.
JWT_SECRETrotation cadence (A2.5) — the comms-coordinated playbook is now written (JWT_SECRET rotation runbook); the first rotation under that playbook lands at the next 90-day boundary or sooner on suspected leak. Rotation invalidates every token at once, so it cannot be silent.