Skip to content

Commit 306b5d2

Browse files
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 <patrik@payloadcms.com>
1 parent ca52a50 commit 306b5d2

File tree

4 files changed

+77
-2
lines changed

4 files changed

+77
-2
lines changed

packages/payload/src/auth/operations/forgotPassword.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,8 @@ export const forgotPasswordOperation = async <TSlug extends CollectionSlug>(
137137

138138
user.resetPasswordToken = token
139139
user.resetPasswordExpiration = new Date(
140-
collectionConfig.auth?.forgotPassword?.expiration || expiration || Date.now() + 3600000,
141-
).toISOString() // 1 hour
140+
Date.now() + (collectionConfig.auth?.forgotPassword?.expiration ?? expiration ?? 3600000),
141+
).toISOString()
142142

143143
user = await payload.update({
144144
id: user.id,

test/access-control/int.spec.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,43 @@ describe('Access Control', () => {
605605
expect(res).toBeTruthy()
606606
})
607607
})
608+
609+
describe('Auth - Local API', () => {
610+
it('should not allow reset password if forgotPassword expiration token is expired', async () => {
611+
// Mock Date.now() to simulate the forgotPassword call happening 1 hour ago (default is 1 hour)
612+
const originalDateNow = Date.now
613+
const mockDateNow = jest.spyOn(Date, 'now').mockImplementation(() => {
614+
// Move the current time back by 1 hour
615+
return originalDateNow() - 60 * 60 * 1000
616+
})
617+
618+
let forgot
619+
try {
620+
// Call forgotPassword while the mocked Date.now() is active
621+
forgot = await payload.forgotPassword({
622+
collection: 'users',
623+
data: {
624+
email: 'dev@payloadcms.com',
625+
},
626+
})
627+
} finally {
628+
// Restore the original Date.now() after the forgotPassword call
629+
mockDateNow.mockRestore()
630+
}
631+
632+
// Attempt to reset password, which should fail because the token is expired
633+
await expect(
634+
payload.resetPassword({
635+
collection: 'users',
636+
data: {
637+
password: 'test',
638+
token: forgot,
639+
},
640+
overrideAccess: true,
641+
}),
642+
).rejects.toThrow('Token is either invalid or has expired.')
643+
})
644+
})
608645
})
609646

610647
async function createDoc<TSlug extends CollectionSlug = 'posts'>(

test/auth/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ export default buildConfigWithDefaults({
4444
tokenExpiration: 7200, // 2 hours
4545
useAPIKey: true,
4646
verify: false,
47+
forgotPassword: {
48+
expiration: 300000, // 5 minutes
49+
},
4750
},
4851
fields: [
4952
{

test/auth/int.spec.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -932,5 +932,40 @@ describe('Auth', () => {
932932

933933
expect(reset.user.email).toStrictEqual('dev@payloadcms.com')
934934
})
935+
936+
it('should not allow reset password if forgotPassword expiration token is expired', async () => {
937+
// Mock Date.now() to simulate the forgotPassword call happening 6 minutes ago (current expiration is set to 5 minutes)
938+
const originalDateNow = Date.now
939+
const mockDateNow = jest.spyOn(Date, 'now').mockImplementation(() => {
940+
// Move the current time back by 6 minutes (360,000 ms)
941+
return originalDateNow() - 6 * 60 * 1000
942+
})
943+
944+
let forgot
945+
try {
946+
// Call forgotPassword while the mocked Date.now() is active
947+
forgot = await payload.forgotPassword({
948+
collection: 'users',
949+
data: {
950+
email: 'dev@payloadcms.com',
951+
},
952+
})
953+
} finally {
954+
// Restore the original Date.now() after the forgotPassword call
955+
mockDateNow.mockRestore()
956+
}
957+
958+
// Attempt to reset password, which should fail because the token is expired
959+
await expect(
960+
payload.resetPassword({
961+
collection: 'users',
962+
data: {
963+
password: 'test',
964+
token: forgot,
965+
},
966+
overrideAccess: true,
967+
}),
968+
).rejects.toThrow('Token is either invalid or has expired.')
969+
})
935970
})
936971
})

0 commit comments

Comments
 (0)