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:
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user