fix: invalidate old password reset tokens when new one is requested (#24607)
* fix: invalidate old password reset tokens when new one is requested Security fix: Previously, old password reset tokens remained valid even after requesting a new one, creating a potential account takeover vulnerability. This change ensures that when a user requests a new password reset link, all previous valid tokens for that email are immediately invalidated. Changes: - Expire all existing valid tokens before creating new one - Add E2E test to verify old tokens are invalidated - Prevent potential account takeover scenario Co-Authored-By: anik@cal.com <adhabal2002@gmail.com> * Clean up code by removing blank line Removed unnecessary blank line in forgot-password.e2e.ts. * test: fix strict mode violation in password reset test Use getByRole to specifically target the heading element instead of text locator which was matching both the heading and button. Co-Authored-By: anik@cal.com <adhabal2002@gmail.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
anik@cal.com <adhabal2002@gmail.com>
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
1261247e94
commit
613e9f0788
@@ -88,7 +88,6 @@ test.describe("Forgot password", async () => {
|
||||
},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const updatedPassword = updatedUser.password!.hash;
|
||||
expect(await verifyPassword(newPassword, updatedPassword)).toBeTruthy();
|
||||
|
||||
@@ -97,4 +96,70 @@ test.describe("Forgot password", async () => {
|
||||
|
||||
await expect(page.locator(`text=Whoops`)).toBeVisible();
|
||||
});
|
||||
|
||||
test("Old tokens are invalidated when new reset link is requested", async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
|
||||
await page.goto("/auth/forgot-password");
|
||||
await page.waitForSelector("text=Forgot Password?");
|
||||
|
||||
await page.fill('input[name="email"]', `${user.username}@example.com`);
|
||||
await page.press('input[name="email"]', "Enter");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await page.waitForSelector("text=Reset link sent");
|
||||
|
||||
const firstRequest = await prisma.resetPasswordRequest.findFirstOrThrow({
|
||||
where: {
|
||||
email: user.email,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
expires: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
await page.goto("/auth/forgot-password");
|
||||
await page.waitForSelector("text=Forgot Password?");
|
||||
|
||||
await page.fill('input[name="email"]', `${user.username}@example.com`);
|
||||
await page.press('input[name="email"]', "Enter");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await page.waitForSelector("text=Reset link sent");
|
||||
|
||||
const secondRequest = await prisma.resetPasswordRequest.findFirstOrThrow({
|
||||
where: {
|
||||
email: user.email,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
expires: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
const firstRequestAfterSecond = await prisma.resetPasswordRequest.findUniqueOrThrow({
|
||||
where: {
|
||||
id: firstRequest.id,
|
||||
},
|
||||
select: {
|
||||
expires: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(firstRequest.id).not.toBe(secondRequest.id);
|
||||
expect(firstRequestAfterSecond.expires.getTime()).toBeLessThanOrEqual(new Date().getTime());
|
||||
|
||||
await page.goto(`/auth/forgot-password/${firstRequest.id}`);
|
||||
await expect(page.locator(`text=Whoops`)).toBeVisible();
|
||||
|
||||
await page.goto(`/auth/forgot-password/${secondRequest.id}`);
|
||||
await expect(page.getByRole("heading", { name: "Reset Password" })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,19 @@ const RECENT_PERIOD_IN_MINUTES = 5;
|
||||
|
||||
const createPasswordReset = async (email: string): Promise<string> => {
|
||||
const expiry = dayjs().add(PASSWORD_RESET_EXPIRY_HOURS, "hours").toDate();
|
||||
|
||||
await prisma.resetPasswordRequest.updateMany({
|
||||
where: {
|
||||
email,
|
||||
expires: {
|
||||
gt: new Date(),
|
||||
},
|
||||
},
|
||||
data: {
|
||||
expires: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
const createdResetPasswordRequest = await prisma.resetPasswordRequest.create({
|
||||
data: {
|
||||
email,
|
||||
|
||||
Reference in New Issue
Block a user