Use crypto.randomBytes for session tokens; add deleteOtherSessions helper

Session tokens were derived from Math.random() + Date.now() — predictable
enough that a determined attacker could brute-force or guess valid
tokens for other users. Switch to crypto.randomBytes(32) (256 bits of
CSPRNG output, hex-encoded), the standard for opaque bearer tokens.

Also adds deleteOtherSessions(userId, keepToken) so the upcoming
password-change flow can log a user out of every other device when
they rotate their password.
This commit is contained in:
Keysat
2026-05-09 08:57:51 -05:00
parent d9c4e6c4a0
commit 53d2bade5c
+26 -4
View File
@@ -1,4 +1,5 @@
import bcryptjs from "bcryptjs";
import { randomBytes } from "node:crypto";
import { prisma } from "./prisma";
import { ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies";
import { cookies } from "next/headers";
@@ -23,14 +24,16 @@ export async function verifyPassword(
}
/**
* Create a session token for a user (30-day expiration)
* Create a session token for a user (30-day expiration).
*
* Token is 256 bits of CSPRNG output, hex-encoded (64 chars). Do not
* weaken this — predictable tokens enable cross-user impersonation,
* and under multi-user that means anyone-can-be-anyone if guessable.
*/
export async function createSession(
userId: string
): Promise<{ token: string; expiresAt: Date }> {
const token = Buffer.from(
`${userId}:${Date.now()}:${Math.random()}`
).toString("hex");
const token = randomBytes(32).toString("hex");
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days
@@ -76,6 +79,25 @@ export async function deleteSession(token: string): Promise<void> {
});
}
/**
* Revoke every session for a user except the optional `keepToken`.
* Used by password-change to log the user out of every other device
* when they rotate their password (defense against compromised
* sessions).
*/
export async function deleteOtherSessions(
userId: string,
keepToken: string | null,
): Promise<number> {
const result = await prisma.session.deleteMany({
where: {
userId,
...(keepToken ? { NOT: { token: keepToken } } : {}),
},
});
return result.count;
}
/**
* Get session from cookies object
*/