From 53d2bade5c9507dfac20b514c0fd6e6676ae345e Mon Sep 17 00:00:00 2001 From: Keysat Date: Sat, 9 May 2026 08:57:51 -0500 Subject: [PATCH] Use crypto.randomBytes for session tokens; add deleteOtherSessions helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- proof-of-work/lib/auth.ts | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/proof-of-work/lib/auth.ts b/proof-of-work/lib/auth.ts index f389df7..069ded0 100644 --- a/proof-of-work/lib/auth.ts +++ b/proof-of-work/lib/auth.ts @@ -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 { }); } +/** + * 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 { + const result = await prisma.session.deleteMany({ + where: { + userId, + ...(keepToken ? { NOT: { token: keepToken } } : {}), + }, + }); + return result.count; +} + /** * Get session from cookies object */