| Field | Value |
|---|---|
| Severity | High (session management — token cannot be revoked) |
| Surface | Auth / API |
| Status | Open |
| Discovered | 2026-04-20 |
| Discovered by | Manual (curl + Playwright) |
Logout flow:
DELETE /api/auth/session— Next.js clears theauth_tokencookie. Response:{"message":"Session cleared"}.- Immediately after logout, the original Bearer token from before logout is tested:
$ curl -sS -H "authorization: Bearer $TOKEN_BEFORE_LOGOUT" \
https://chamaconnect.io/api/proxy/users/current-user
{"message":"Successfully retrieved logged in user","status":"success","data":{ ... }}
# HTTP 200 — token still valid even after "logout"- No
/api/proxy/users/logoutendpoint exists — confirmed 404. - No
expclaim in the JWT (BUG-016) — the token never expires on its own. - Cookie
Max-Age: 604800(7 days) — the httpOnly cookie is the only time-bound element, but the underlying JWT has no expiry and the backend has no token blocklist.
Net result: "logging out" only removes the browser cookie. If the token was previously:
- Returned in the signin response body (BUG-013),
- Stored in
localStorageby a frontend bug, - Intercepted by a MITM on a non-HSTS network,
- Extracted by BUG-028/042 tools from another session,
…then the attacker retains permanent access to the account. There is no way for the real user or a platform admin to revoke a live token.
This is a direct consequence of BUG-016 (no exp claim) compounded by the absence of a server-side blocklist:
- A user who suspects their account was compromised (e.g. got a password-reset notification they didn't trigger) cannot lock out the attacker by changing their password or logging out — the attacker's stolen token stays valid indefinitely.
- A platform admin who deactivates a user (
isActive: false) does not invalidate outstanding tokens if the middleware doesn't re-checkisActiveon every request. - Any device theft, session handover, or XSS incident leaves the victim with no recovery path short of contacting support and having the account deleted.
On a financial platform where tokens give access to contribution records, approvals, and (via BUG-027/040) platform-wide configuration mutations, permanent token validity is an existential risk.
The backend is a stateless JWT verifier with no token blocklist (Redis SET or DB table). The Next.js layer's "logout" only deletes the browser cookie — it cannot invalidate what the backend has already signed. Combined with no exp claim, there is nothing to time-box the token's validity.
Three changes needed together:
- Add
expclaim to every issued JWT (fixes BUG-016 — required here):
const token = jwt.sign(payload, process.env.JWT_SECRET!, { expiresIn: '15m' });- Add a refresh-token flow so users don't have to re-login every 15 minutes:
// POST /api/proxy/auth/refresh
// Validates a long-lived (7-day, httpOnly, Secure, SameSite=Strict) refresh token
// Returns a new 15-min access token
// Invalidates the old refresh token (one-time use)- Maintain a token blocklist for logout and forced revocation:
// On logout — add jti (from BUG-016 fix, add jti claim) to a Redis SET with TTL = exp - now
await redis.set(`revoked:${payload.jti}`, '1', 'EX', ttlSeconds);
// On every authenticated request — check the blocklist
const revoked = await redis.exists(`revoked:${payload.jti}`);
if (revoked) return unauthorized(res, 'Token has been revoked');- On password change / account deactivation: revoke all outstanding tokens for that user (store
userId → tokenIssuedBeforetimestamp in DB; any token withiat < issuedBeforeis rejected).
- Sign in, capture the Bearer token. Sign out. Try the old token →
401 Token has been revoked. - Sign in on device A. Change password on device B. Try the device-A token →
401. - Admin deactivates user. User's outstanding token →
401with"Account deactivated". - Regression test in
/recon/tests/token-revocation.spec.ts.