mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 14:21:46 +00:00
Reject WebSocket tokens minted before a password change
Stamp the user's password_version onto the ephemeral ws token and verify it on connect, closing the socket (4001) when it no longer matches, so a token issued before a password reset can't be replayed. Tokens minted without a version are treated as version 0, matching the JWT pv-claim semantics.
This commit is contained in:
@@ -11,15 +11,31 @@ interface TokenEntry {
|
||||
userId: number;
|
||||
purpose: string;
|
||||
expiresAt: number;
|
||||
/**
|
||||
* Snapshot of the user's `password_version` at mint time, used for the
|
||||
* defence-in-depth session gate on WebSocket connects. `undefined` for
|
||||
* tokens minted without a version (legacy/other purposes), which callers
|
||||
* treat as version 0 — mirroring the JWT `pv` claim semantics.
|
||||
*/
|
||||
pv?: number;
|
||||
}
|
||||
|
||||
export interface EphemeralTokenMeta {
|
||||
/** Bind the token to the user's current password_version (session gate). */
|
||||
pv?: number;
|
||||
}
|
||||
|
||||
const store = new Map<string, TokenEntry>();
|
||||
|
||||
export function createEphemeralToken(userId: number, purpose: string): string | null {
|
||||
export function createEphemeralToken(
|
||||
userId: number,
|
||||
purpose: string,
|
||||
meta?: EphemeralTokenMeta,
|
||||
): string | null {
|
||||
if (store.size >= MAX_STORE_SIZE) return null;
|
||||
const token = crypto.randomBytes(32).toString('hex');
|
||||
const ttl = TTL[purpose] ?? 60_000;
|
||||
store.set(token, { userId, purpose, expiresAt: Date.now() + ttl });
|
||||
store.set(token, { userId, purpose, expiresAt: Date.now() + ttl, pv: meta?.pv });
|
||||
return token;
|
||||
}
|
||||
|
||||
@@ -31,6 +47,22 @@ export function consumeEphemeralToken(token: string, purpose: string): number |
|
||||
return entry.userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `consumeEphemeralToken`, but also returns the `password_version` the
|
||||
* token was minted with. Used by the WebSocket handshake so a token issued
|
||||
* before a password change can be rejected even within its short TTL.
|
||||
*/
|
||||
export function consumeEphemeralTokenWithMeta(
|
||||
token: string,
|
||||
purpose: string,
|
||||
): { userId: number; pv?: number } | null {
|
||||
const entry = store.get(token);
|
||||
if (!entry) return null;
|
||||
store.delete(token);
|
||||
if (entry.purpose !== purpose || Date.now() > entry.expiresAt) return null;
|
||||
return { userId: entry.userId, pv: entry.pv };
|
||||
}
|
||||
|
||||
let cleanupInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
export function startTokenCleanup(): void {
|
||||
|
||||
Reference in New Issue
Block a user