From 306b5d230040526acc7645037a3b0f6373c64289 Mon Sep 17 00:00:00 2001 From: Dan Ribbens Date: Wed, 11 Dec 2024 08:43:22 -0500 Subject: [PATCH] fix: forgotPassword set expiration time (#9871) The logic for creating a timestamp for use in resetPassword was not correctly returning a valid date. --------- Co-authored-by: Patrik Kozak --- .../src/auth/operations/forgotPassword.ts | 4 +- test/access-control/int.spec.ts | 37 +++++++++++++++++++ test/auth/config.ts | 3 ++ test/auth/int.spec.ts | 35 ++++++++++++++++++ 4 files changed, 77 insertions(+), 2 deletions(-) diff --git a/packages/payload/src/auth/operations/forgotPassword.ts b/packages/payload/src/auth/operations/forgotPassword.ts index 36df4979173..b30bdee6c5d 100644 --- a/packages/payload/src/auth/operations/forgotPassword.ts +++ b/packages/payload/src/auth/operations/forgotPassword.ts @@ -137,8 +137,8 @@ export const forgotPasswordOperation = async ( user.resetPasswordToken = token user.resetPasswordExpiration = new Date( - collectionConfig.auth?.forgotPassword?.expiration || expiration || Date.now() + 3600000, - ).toISOString() // 1 hour + Date.now() + (collectionConfig.auth?.forgotPassword?.expiration ?? expiration ?? 3600000), + ).toISOString() user = await payload.update({ id: user.id, diff --git a/test/access-control/int.spec.ts b/test/access-control/int.spec.ts index b4180b73902..df3055ffcd0 100644 --- a/test/access-control/int.spec.ts +++ b/test/access-control/int.spec.ts @@ -605,6 +605,43 @@ describe('Access Control', () => { expect(res).toBeTruthy() }) }) + + describe('Auth - Local API', () => { + it('should not allow reset password if forgotPassword expiration token is expired', async () => { + // Mock Date.now() to simulate the forgotPassword call happening 1 hour ago (default is 1 hour) + const originalDateNow = Date.now + const mockDateNow = jest.spyOn(Date, 'now').mockImplementation(() => { + // Move the current time back by 1 hour + return originalDateNow() - 60 * 60 * 1000 + }) + + let forgot + try { + // Call forgotPassword while the mocked Date.now() is active + forgot = await payload.forgotPassword({ + collection: 'users', + data: { + email: 'dev@payloadcms.com', + }, + }) + } finally { + // Restore the original Date.now() after the forgotPassword call + mockDateNow.mockRestore() + } + + // Attempt to reset password, which should fail because the token is expired + await expect( + payload.resetPassword({ + collection: 'users', + data: { + password: 'test', + token: forgot, + }, + overrideAccess: true, + }), + ).rejects.toThrow('Token is either invalid or has expired.') + }) + }) }) async function createDoc( diff --git a/test/auth/config.ts b/test/auth/config.ts index 66b349fabb8..66d5ddceffc 100644 --- a/test/auth/config.ts +++ b/test/auth/config.ts @@ -44,6 +44,9 @@ export default buildConfigWithDefaults({ tokenExpiration: 7200, // 2 hours useAPIKey: true, verify: false, + forgotPassword: { + expiration: 300000, // 5 minutes + }, }, fields: [ { diff --git a/test/auth/int.spec.ts b/test/auth/int.spec.ts index 9d969dc797f..88d886287d2 100644 --- a/test/auth/int.spec.ts +++ b/test/auth/int.spec.ts @@ -932,5 +932,40 @@ describe('Auth', () => { expect(reset.user.email).toStrictEqual('dev@payloadcms.com') }) + + it('should not allow reset password if forgotPassword expiration token is expired', async () => { + // Mock Date.now() to simulate the forgotPassword call happening 6 minutes ago (current expiration is set to 5 minutes) + const originalDateNow = Date.now + const mockDateNow = jest.spyOn(Date, 'now').mockImplementation(() => { + // Move the current time back by 6 minutes (360,000 ms) + return originalDateNow() - 6 * 60 * 1000 + }) + + let forgot + try { + // Call forgotPassword while the mocked Date.now() is active + forgot = await payload.forgotPassword({ + collection: 'users', + data: { + email: 'dev@payloadcms.com', + }, + }) + } finally { + // Restore the original Date.now() after the forgotPassword call + mockDateNow.mockRestore() + } + + // Attempt to reset password, which should fail because the token is expired + await expect( + payload.resetPassword({ + collection: 'users', + data: { + password: 'test', + token: forgot, + }, + overrideAccess: true, + }), + ).rejects.toThrow('Token is either invalid or has expired.') + }) }) })