Skip to content

Commit 06e6dda

Browse files
committed
feat(auth): add support for custom verification token generation
1 parent d213c91 commit 06e6dda

File tree

6 files changed

+120
-5
lines changed

6 files changed

+120
-5
lines changed

docs/authentication/email.mdx

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,11 @@ export const Customers: CollectionConfig = {
3232

3333
The following options are available:
3434

35-
| Option | Description |
36-
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
37-
| **`generateEmailHTML`** | Allows for overriding the HTML within emails that are sent to users indicating how to validate their account. [More details](#generateemailhtml). |
38-
| **`generateEmailSubject`** | Allows for overriding the subject of the email that is sent to users indicating how to validate their account. [More details](#generateemailsubject). |
35+
| Option | Description |
36+
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
37+
| **`generateEmailHTML`** | Allows for overriding the HTML within emails that are sent to users indicating how to validate their account. [More details](#generateemailhtml). |
38+
| **`generateEmailSubject`** | Allows for overriding the subject of the email that is sent to users indicating how to validate their account. [More details](#generateemailsubject). |
39+
| **`generateVerificationToken`** | Allows for overriding the verification token generation logic. [More details](#generateverificationtoken). |
3940

4041
#### generateEmailHTML
4142

@@ -90,6 +91,32 @@ export const Customers: CollectionConfig = {
9091
}
9192
```
9293

94+
#### generateVerificationToken
95+
96+
Function that allows for overriding the verification token generation logic. By default, Payload generates a random 40-character hex string. This function receives `{ req, user }` and must return a string that will be used as the verification token.
97+
98+
```ts
99+
import type { CollectionConfig } from 'payload'
100+
101+
export const Customers: CollectionConfig = {
102+
// ...
103+
auth: {
104+
verify: {
105+
// highlight-start
106+
generateVerificationToken: ({ req, user }) => {
107+
return `custom-${user.id}-${Date.now()}`
108+
},
109+
// highlight-end
110+
},
111+
},
112+
}
113+
```
114+
115+
<Banner type="info">
116+
**Note:** The verification token must be unique and secure. Avoid predictable
117+
patterns that could be exploited.
118+
</Banner>
119+
93120
## Forgot Password
94121

95122
You can customize how the Forgot Password workflow operates with the following options on the `auth.forgotPassword` property:

packages/payload/src/auth/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,12 @@ export type ClientUser = {
138138
} & BaseUser
139139

140140
export type UserSession = { createdAt: Date | string; expiresAt: Date | string; id: string }
141+
142+
export type GenerateVerificationToken<TUser = any> = (args: {
143+
req: PayloadRequest
144+
user: TUser
145+
}) => Promise<string> | string
146+
141147
type GenerateVerifyEmailHTML<TUser = any> = (args: {
142148
req: PayloadRequest
143149
token: string
@@ -297,13 +303,15 @@ export interface IncomingAuthType {
297303
| {
298304
generateEmailHTML?: GenerateVerifyEmailHTML
299305
generateEmailSubject?: GenerateVerifyEmailSubject
306+
generateVerificationToken?: GenerateVerificationToken
300307
}
301308
| boolean
302309
}
303310

304311
export type VerifyConfig = {
305312
generateEmailHTML?: GenerateVerifyEmailHTML
306313
generateEmailSubject?: GenerateVerifyEmailSubject
314+
generateVerificationToken?: GenerateVerificationToken
307315
}
308316

309317
export interface Auth

packages/payload/src/collections/operations/create.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,16 @@ export const createOperation = async <
252252
if (collectionConfig.auth && !collectionConfig.auth.disableLocalStrategy) {
253253
if (collectionConfig.auth.verify) {
254254
resultWithLocales._verified = Boolean(resultWithLocales._verified) || false
255-
resultWithLocales._verificationToken = crypto.randomBytes(20).toString('hex')
255+
256+
const verify = collectionConfig.auth.verify
257+
if (typeof verify === 'object' && typeof verify.generateVerificationToken === 'function') {
258+
resultWithLocales._verificationToken = await verify.generateVerificationToken({
259+
req,
260+
user: resultWithLocales,
261+
})
262+
} else {
263+
resultWithLocales._verificationToken = crypto.randomBytes(20).toString('hex')
264+
}
256265
}
257266

258267
doc = await registerLocalStrategy({

test/auth/config.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
88
import { devUser } from '../credentials.js'
99
import {
1010
apiKeysSlug,
11+
customVerificationTokenSlug,
12+
expectedVerificationToken,
1113
namedSaveToJWTValue,
1214
partialDisableLocalStrategiesSlug,
1315
publicUsersSlug,
@@ -262,6 +264,15 @@ export default buildConfigWithDefaults({
262264
},
263265
],
264266
},
267+
{
268+
slug: customVerificationTokenSlug,
269+
auth: {
270+
verify: {
271+
generateVerificationToken: async () => Promise.resolve(expectedVerificationToken),
272+
},
273+
},
274+
fields: [],
275+
},
265276
],
266277
onInit: async (payload) => {
267278
await payload.create({

test/auth/int.spec.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import { devUser } from '../credentials.js'
2222
import { initPayloadInt } from '../helpers/initPayloadInt.js'
2323
import {
2424
apiKeysSlug,
25+
customVerificationTokenSlug,
26+
expectedVerificationToken,
2527
namedSaveToJWTValue,
2628
partialDisableLocalStrategiesSlug,
2729
publicUsersSlug,
@@ -341,6 +343,60 @@ describe('Auth', () => {
341343
expect(afterToken).toBeNull()
342344
})
343345

346+
it('should allow custom verification token of a user', async () => {
347+
const emailToVerify = 'verify@me.com'
348+
const response = await restClient.POST(`/${customVerificationTokenSlug}`, {
349+
body: JSON.stringify({
350+
email: emailToVerify,
351+
password,
352+
roles: ['editor'],
353+
}),
354+
headers: {
355+
Authorization: `JWT ${token}`,
356+
},
357+
})
358+
359+
expect(response.status).toBe(201)
360+
361+
const userResult = await payload.find({
362+
collection: customVerificationTokenSlug,
363+
limit: 1,
364+
showHiddenFields: true,
365+
where: {
366+
email: {
367+
equals: emailToVerify,
368+
},
369+
},
370+
})
371+
372+
const { _verificationToken, _verified } = userResult.docs[0]
373+
374+
expect(_verified).toBe(false)
375+
expect(_verificationToken).toBe(expectedVerificationToken)
376+
377+
const verificationResponse = await restClient.POST(
378+
`/${customVerificationTokenSlug}/verify/${_verificationToken}`,
379+
)
380+
381+
expect(verificationResponse.status).toBe(200)
382+
383+
const afterVerifyResult = await payload.find({
384+
collection: customVerificationTokenSlug,
385+
limit: 1,
386+
showHiddenFields: true,
387+
where: {
388+
email: {
389+
equals: emailToVerify,
390+
},
391+
},
392+
})
393+
394+
const { _verificationToken: afterToken, _verified: afterVerified } =
395+
afterVerifyResult.docs[0]
396+
expect(afterVerified).toBe(true)
397+
expect(afterToken).toBeNull()
398+
})
399+
344400
describe('User Preferences', () => {
345401
const key = 'test'
346402
const property = 'store'

test/auth/shared.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,7 @@ export const partialDisableLocalStrategiesSlug = 'partial-disable-local-strategi
99
export const namedSaveToJWTValue = 'namedSaveToJWT value'
1010

1111
export const saveToJWTKey = 'x-custom-jwt-property-name'
12+
13+
export const customVerificationTokenSlug = 'custom-verification-token'
14+
15+
export const expectedVerificationToken = 'custom-verification-token-12345'

0 commit comments

Comments
 (0)