Skip to content

Fix/sr ws auth tokenstore credentials expiry - #1413

Merged
GoodnessJohn merged 4 commits into
Haroldwonder:mainfrom
janetpius-cmd:fix/sr-ws-auth-tokenstore-credentials-expiry
Aug 28, 2026
Merged

Fix/sr ws auth tokenstore credentials expiry#1413
GoodnessJohn merged 4 commits into
Haroldwonder:mainfrom
janetpius-cmd:fix/sr-ws-auth-tokenstore-credentials-expiry

Conversation

@janetpius-cmd

Copy link
Copy Markdown
Contributor

Summary

  1. WS auth parity — WebSocket middleware now uses the shared verifyAccessToken() (algorithm/issuer/audience pinning + revocation check), plus a periodic sweep to disconnect already-open sockets on logout. Added tests.
  2. Redis-backed tokenStore — refresh families, access-token revocation, and login lockout now sync across instances via Redis + pub/sub when REDIS_URL is set, with in-memory fallback preserving all existing function signatures. Added a multi-instance test.
  3. Real per-user credential store — replaces the shared STUB_PASSWORD with a bcrypt-hashed users table; ADMIN_USER_IDS/AGENT_USER_IDS become bootstrap-only seeding. Added tests. Note: existing login tests using arbitrary userIds against the old stub will need updating to seed users first.
  4. CI exception-expiry gate — new scripts/check-exception-expiry.js parses Expires: dates in .trivyignore/deny.toml and fails CI once expired; wired into both security workflows, plus fixed trivy-scan to actually pass .trivyignore and added the missing cargo deny check advisories step. Diff was under 150 lines, so docs/CI_EXCEPTION_EXPIRY_README.md documents what was done.

Linked Issue

Closes #1289
Closes #1290
Closes #1291
Closes #1293

Type of Change

  • Bug fix
  • New feature
  • Refactor
  • Documentation
  • Chore / infra

Checklist

  • Tests added or updated for the change
  • Documentation updated (README, API docs, guides) where relevant
  • Changeset added (sdk/) if this touches a published package
  • Linked issue referenced above
  • This PR introduces a breaking change (if checked, describe migration steps below)

Breaking Change Notes

… auth

The Socket.IO auth middleware called a bare jwt.verify(token, secret) with
no algorithms/issuer/audience options and never checked isAccessTokenRevoked,
unlike the HTTP middleware's verifyAccessToken(). This let a token revoked
via POST /api/auth/logout stay fully valid over an already-open or newly
opened WebSocket connection until natural expiry, and left the WS layer
exposed to alg:none / HS-RS confusion that the HTTP layer already defends
against.

createAuthMiddleware now delegates to the shared verifyAccessToken() so
algorithm pinning, issuer/audience checks, and revocation checks apply
uniformly across HTTP and WebSocket. Added reverifyOrDisconnect() plus a
periodic sweep in websocket/index.ts so already-open sockets are dropped
once their token is revoked, not just new connections. Added tests proving
a token revoked via logout is rejected on both new and existing connections.
…d across instances

services/tokenStore.ts held refresh-token families, access-token revocation,
and login-lockout state entirely in per-process Maps, a known limitation
documented in AUTH_MATRIX.md with no tracked follow-up. In a horizontally
scaled deployment, a logout or reuse-detected revocation on instance A left
the user fully authenticated against instances B/C until natural token
expiry, and login lockout was bypassable by hitting a different instance.

tokenStore is refactored into a TokenStore class: the existing synchronous
Maps remain as a fast local L1 cache (so every exported function keeps its
original synchronous signature and callers are unaffected), backed by Redis
as the shared source of truth when REDIS_URL is configured. Every mutation
writes through to Redis with the same TTL semantics as before
(REFRESH_TOKEN_TTL_MS, LOGIN_LOCKOUT_MS, LOGIN_ATTEMPT_WINDOW_MS) and
publishes an event on a pub/sub channel that every instance (including local
dev with REDIS_URL unset, which no-ops) subscribes to and applies to its own
cache. Added ioredis as a dependency and a multi-instance integration test
that shares one backend between two TokenStore instances to prove
revocation, refresh-token reuse detection, and login lockout now propagate
across instances instead of staying per-process.
…al store

routes/auth.ts's verifyCredentials() compared every login attempt's password
against a single process.env.STUB_PASSWORD for every userId, so
"authentication" only proved the caller knew one shared secret, not who they
actually were. resolveRole() was similarly env-driven via
ADMIN_USER_IDS/AGENT_USER_IDS allowlists. Both were called out as known
limitations with no tracked remediation in AUTH_MATRIX.md, even though
downstream code (ensureOwnership, AML subject tracking, compliance
attribution) treats the JWT `sub` claim as a trustworthy per-user identity.

Adds db/userStore.ts: a users table (Postgres when DATABASE_URL is
configured, in-memory otherwise) holding a bcrypt password hash and role per
user. verifyCredentials()/resolveRole() in routes/auth.ts now delegate to
verifyUserCredentials()/getUserRole() behind the same call sites, so a valid
password for one identity can no longer authenticate as a different one.
ADMIN_USER_IDS/AGENT_USER_IDS are kept only as a one-time bootstrap/seed
mechanism for the first operator accounts — seeded once into the store and
never consulted again or allowed to overwrite a rotated password. Added
tests covering cross-user password isolation, role resolution, and
bootstrap seeding not clobbering rotated credentials.

Note: existing auth tests that log in as arbitrary userIds against a shared
STUB_PASSWORD will need to seed those users via upsertUser() first — that is
the intended behavioural change this fix makes.
VULNERABILITY_EXCEPTIONS.md promises "Exceptions reaching their expiration
date automatically fail CI builds until renewed or patched," but no
workflow or script implemented it. container-security.yml's trivy-scan job
never referenced .trivyignore at all, and dependency-security.yml's
cargo-audit-and-deny job never evaluated deny.toml's [advisories] list.
Both exception files are empty today, so the gap was latent, but the first
exception added under the documented policy would never have expired in CI.

Adds scripts/check-exception-expiry.js, which parses the "(Expires:
YYYY-MM-DD)" comment convention required by VULNERABILITY_EXCEPTIONS.md out
of .trivyignore and deny.toml and exits non-zero once any entry is past its
date. Wires it into container-security.yml (new check-exception-expiry job,
required by trivy-scan) and dependency-security.yml (new step ahead of
cargo audit/deny) so an expired exception blocks the build. Also fixes
trivy-scan to actually pass trivyignores: ".trivyignore" to the scan step,
and adds the missing `cargo deny check advisories` step so deny.toml's
[advisories] section is evaluated in CI at all.

Diff is under 150 lines; see docs/CI_EXCEPTION_EXPIRY_README.md for a
written summary of what was implemented and what was left out of scope.
@vercel

vercel Bot commented Aug 28, 2026

Copy link
Copy Markdown

@janetpius-cmd is attempting to deploy a commit to the Harold's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Aug 28, 2026

Copy link
Copy Markdown

@janetpius-cmd Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@GoodnessJohn
GoodnessJohn merged commit 3353a79 into Haroldwonder:main Aug 28, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants