screentinker/server/db
ScreenTinker f289609380 fix(auth): back break-glass recovery with a revocable, auditable grant
scripts/reset-admin.js mints a JWT carrying `recovery: true`, and middleware/auth.js
accepted that claim on its own with no database involvement. Three consequences:

- NOT REVOCABLE. The only way to invalidate an outstanding recovery token was to rotate
  JWT_SECRET, which logs out every user on the instance.
- NOT ENUMERABLE. Nobody could answer "is a recovery token outstanding right now?"
- NOT AUDITED. The synthetic id ('recovery-<nonce>') is not a users row, so every
  activity_log insert for it failed the user_id foreign key and was swallowed by a catch —
  a break-glass session left no trace at all.

A `recovery_grants` row per minted token turns all three around: DELETE revokes, SELECT
enumerates, expires_at bounds, and used_at + source_ip record when and from where it was
first exercised. The migration is additive and idempotent, so re-running is a no-op and a
code-only rollback just leaves an unused table.

The grant is session-scoped, NOT single-use-per-request. Recovery means many requests —
load the dashboard, list users, reset a password — so consuming the grant on the first
would make break-glass unusable, a worse outcome than the narrow replay window it closes.
Revocation and expiry are the controls; used_at is the audit stamp.

Also fixed, because it is the mechanism that hid this: logActivity now rewrites a
'recovery-*' id to a NULL user_id with the identity in `details`, so break-glass actions
are actually recorded instead of failing the FK; and a dropped audit row now logs a loud
[AUDIT-DROP] line naming the action and increments a counter, rather than vanishing into
console.error.

The token is written to a 0600 file instead of stdout — under systemd or Docker, printing
it meant journald captured a live admin credential well past its lifetime. Added --list
and --revoke-all.

In-flight recovery tokens minted before this change stop working; they live one hour and
were unrevocable, which is the problem being fixed. Minting already required a working DB,
so redeeming against one is not a new dependency.

test/session-token-resolution.test.js now mints a real grant for its recovery token, so
its assertions keep testing that break-glass is refused on those surfaces for lack of a
users row — not for the unrelated new reason that the token is invalid.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 14:49:03 -05:00
..
database.js fix(auth): back break-glass recovery with a revocable, auditable grant 2026-07-26 14:49:03 -05:00
schema.sql fix(android): server-provisioned settings PIN replaces hardcoded 0000 2026-07-09 19:05:24 -04:00
wal-checkpointer-worker.js fix(db): WAL checkpointer worker-death handling (respawn + inline-autocheckpoint fallback) 2026-07-06 23:50:18 -05:00
wal-checkpointer.js fix(db): WAL checkpointer worker-death handling (respawn + inline-autocheckpoint fallback) 2026-07-06 23:50:18 -05:00