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 bcryptjs from "bcryptjs";
|
||||||
|
import { randomBytes } from "node:crypto";
|
||||||
import { prisma } from "./prisma";
|
import { prisma } from "./prisma";
|
||||||
import { ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies";
|
import { ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies";
|
||||||
import { cookies } from "next/headers";
|
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(
|
export async function createSession(
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<{ token: string; expiresAt: Date }> {
|
): Promise<{ token: string; expiresAt: Date }> {
|
||||||
const token = Buffer.from(
|
const token = randomBytes(32).toString("hex");
|
||||||
`${userId}:${Date.now()}:${Math.random()}`
|
|
||||||
).toString("hex");
|
|
||||||
|
|
||||||
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days
|
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
|
* Get session from cookies object
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user