55 lines
1.6 KiB
TypeScript
55 lines
1.6 KiB
TypeScript
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
|
|
export const PASSWORD_RESET_TOKEN_TTL_MS = 1000 * 60 * 60;
|
|
const PASSWORD_RESET_TOKEN_HASH_PREFIX = "sha256:";
|
|
|
|
export function hashPasswordResetToken(verifyToken: string) {
|
|
return `${PASSWORD_RESET_TOKEN_HASH_PREFIX}${createHash("sha256").update(verifyToken).digest("hex")}`;
|
|
}
|
|
|
|
export function createPasswordResetTokenUpdate(verifyToken: string, now = new Date()) {
|
|
return {
|
|
verifyToken: hashPasswordResetToken(verifyToken),
|
|
verifyTokenCreatedAt: now,
|
|
};
|
|
}
|
|
|
|
export function createPasswordResetVerifyToken() {
|
|
return randomBytes(20).toString("hex");
|
|
}
|
|
|
|
export function isPasswordResetTokenValid(
|
|
user: { verifyToken?: string | null; verifyTokenCreatedAt?: Date | string | null },
|
|
verifyToken: string,
|
|
now = new Date(),
|
|
) {
|
|
if (!user.verifyToken || !verifyToken || !user.verifyTokenCreatedAt) {
|
|
return false;
|
|
}
|
|
|
|
const createdAt = new Date(user.verifyTokenCreatedAt).getTime();
|
|
if (!Number.isFinite(createdAt) || now.getTime() - createdAt > PASSWORD_RESET_TOKEN_TTL_MS) {
|
|
return false;
|
|
}
|
|
|
|
const storedToken = user.verifyToken.startsWith(PASSWORD_RESET_TOKEN_HASH_PREFIX)
|
|
? user.verifyToken
|
|
: user.verifyToken;
|
|
const candidate = user.verifyToken.startsWith(PASSWORD_RESET_TOKEN_HASH_PREFIX)
|
|
? hashPasswordResetToken(verifyToken)
|
|
: verifyToken;
|
|
|
|
return timingSafeStringEqual(storedToken, candidate);
|
|
}
|
|
|
|
function timingSafeStringEqual(a: string, b: string) {
|
|
const aBuffer = Buffer.from(a);
|
|
const bBuffer = Buffer.from(b);
|
|
|
|
if (aBuffer.byteLength !== bBuffer.byteLength) {
|
|
return false;
|
|
}
|
|
|
|
return timingSafeEqual(aBuffer, bBuffer);
|
|
}
|