Fix session lifetime: 2h sliding idle timeout, not 30 days

The JWT's exp claim says 30 days, and I took that as the session
lifetime. It is only an outer ceiling. The server also keeps a per-token
whitelist entry in Valkey (jwt:{accountId}:{jti}) whose TTL is
JWT_TIMEOUT_SECONDS — 7200s on this instance — and JwtStrategy.validate
re-sets it on every authenticated request. Two hours idle and the token
is rejected with 29 days still on exp.

Proven, not inferred: the token from yesterday returned 401 at 13.8h old.
The live instance publishes the values unauthenticated at
GET /api/v3/config/public — JWT_TIMEOUT_SECONDS 7200,
JWT_SHOW_TIMEOUT_WARNING_SECONDS 3600, the latter being exactly the
one-hour UI prompt that prompted this investigation.

refresh-session turns out not to be special: it extends through the same
guard as any other route, and uniquely only in returning the remaining
TTL. So the keepalive uses GET /api/v3/me instead, and the server stays
GET-only; the one POST in the repo is in scripts/probe.mjs, where it
reports the idle budget.

JWT_EXTENDED_TIMEOUT_SECONDS (~1 month) exists in the config schema but
is vestigial: privateDevice has no references in the current NestJS
source, and generateJwtAndAddToWhitelist never overrides the TTL.

Also fixes a real breakage this surfaced: TypeScript parameter
properties are rejected by Node's type stripping, so `npm run dev` and
`npm test` both failed on any file reaching them. Rewritten as explicit
fields, and noted in CLAUDE.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-12 13:07:22 +02:00
parent 35125b7683
commit d657ece436
14 changed files with 408 additions and 57 deletions

View File

@@ -79,6 +79,8 @@ current material**; lessons are the older format.
| One file's metadata | `GET /api/v3/file/{fileRecordId}` |
| File bytes | `GET /api/v3/file/download/{fileRecordId}/{fileName}` |
| News | `GET /api/v3/news` |
| Instance settings (no auth) | `GET /api/v3/config/public` |
| Remaining idle budget | `POST /api/v3/authentication/refresh-session``{expiresInSeconds}` |
### Gotchas that cost real time
@@ -115,6 +117,16 @@ ids as `{buffer:{type:'Buffer',data:[...]}}` rather than hex strings — a leak
from the legacy Mongo serialisation. `normalizeObjectId` in `src/render.ts`
converts them.
**The JWT's `exp` is not the session lifetime.** A server-side whitelist entry
in Valkey (`jwt:{accountId}:{jti}`) expires after `JWT_TIMEOUT_SECONDS` — 7200 s
on this instance — and every authenticated request re-sets it. Two hours idle
and the token is rejected with 29 days still on `exp`. The live values are
public at `GET /api/v3/config/public`. Full write-up in `docs/AUTH.md`.
**`GET /api/v3/config/public` is unauthenticated and useful.** 78 keys of
instance configuration, including the session timeouts and feature flags. Handy
for checking deployed settings without a token.
**`Content-Disposition` on downloads is malformed.** It comes back as
`attachment;; filename="…"` — note the doubled semicolon — and the filename is
percent-encoded inside the quotes. Parse defensively.
@@ -132,4 +144,5 @@ the Etherpad-style editor, not the document text.
## Re-verifying after an upstream release
`npm run probe` re-checks every assumption above against the live instance and
prints what it finds, including days left on the token.
prints what it finds — both token clocks included: days until hard expiry, and
seconds of idle budget remaining.