Skip to content

feat: add support for custom verification token generation #13152

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 31 additions & 4 deletions docs/authentication/email.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ export const Customers: CollectionConfig = {

The following options are available:

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

#### generateEmailHTML

Expand Down Expand Up @@ -90,6 +91,32 @@ export const Customers: CollectionConfig = {
}
```

#### generateVerificationToken

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.

```ts
import type { CollectionConfig } from 'payload'

export const Customers: CollectionConfig = {
// ...
auth: {
verify: {
// highlight-start
generateVerificationToken: ({ req, user }) => {
return `custom-${user.id}-${Date.now()}`
},
// highlight-end
},
},
}
```

<Banner type="info">
**Note:** The verification token must be unique and secure. Avoid predictable
patterns that could be exploited.
</Banner>

## Forgot Password

You can customize how the Forgot Password workflow operates with the following options on the `auth.forgotPassword` property:
Expand Down
8 changes: 8 additions & 0 deletions packages/payload/src/auth/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,12 @@ export type ClientUser = {
} & BaseUser

export type UserSession = { createdAt: Date | string; expiresAt: Date | string; id: string }

export type GenerateVerificationToken<TUser = any> = (args: {
req: PayloadRequest
user: TUser
}) => Promise<string> | string

type GenerateVerifyEmailHTML<TUser = any> = (args: {
req: PayloadRequest
token: string
Expand Down Expand Up @@ -297,13 +303,15 @@ export interface IncomingAuthType {
| {
generateEmailHTML?: GenerateVerifyEmailHTML
generateEmailSubject?: GenerateVerifyEmailSubject
generateVerificationToken?: GenerateVerificationToken
}
| boolean
}

export type VerifyConfig = {
generateEmailHTML?: GenerateVerifyEmailHTML
generateEmailSubject?: GenerateVerifyEmailSubject
generateVerificationToken?: GenerateVerificationToken
}

export interface Auth
Expand Down
11 changes: 10 additions & 1 deletion packages/payload/src/collections/operations/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,16 @@ export const createOperation = async <
if (collectionConfig.auth && !collectionConfig.auth.disableLocalStrategy) {
if (collectionConfig.auth.verify) {
resultWithLocales._verified = Boolean(resultWithLocales._verified) || false
resultWithLocales._verificationToken = crypto.randomBytes(20).toString('hex')

const verify = collectionConfig.auth.verify
if (typeof verify === 'object' && typeof verify.generateVerificationToken === 'function') {
resultWithLocales._verificationToken = await verify.generateVerificationToken({
req,
user: resultWithLocales,
})
} else {
resultWithLocales._verificationToken = crypto.randomBytes(20).toString('hex')
}
}

doc = await registerLocalStrategy({
Expand Down
11 changes: 11 additions & 0 deletions test/auth/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { devUser } from '../credentials.js'
import {
apiKeysSlug,
customVerificationTokenSlug,
expectedVerificationToken,
namedSaveToJWTValue,
partialDisableLocalStrategiesSlug,
publicUsersSlug,
Expand Down Expand Up @@ -262,6 +264,15 @@ export default buildConfigWithDefaults({
},
],
},
{
slug: customVerificationTokenSlug,
auth: {
verify: {
generateVerificationToken: async () => Promise.resolve(expectedVerificationToken),
},
},
fields: [],
},
],
onInit: async (payload) => {
await payload.create({
Expand Down
56 changes: 56 additions & 0 deletions test/auth/int.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import { devUser } from '../credentials.js'
import { initPayloadInt } from '../helpers/initPayloadInt.js'
import {
apiKeysSlug,
customVerificationTokenSlug,
expectedVerificationToken,
namedSaveToJWTValue,
partialDisableLocalStrategiesSlug,
publicUsersSlug,
Expand Down Expand Up @@ -341,6 +343,60 @@ describe('Auth', () => {
expect(afterToken).toBeNull()
})

it('should allow custom verification token of a user', async () => {
const emailToVerify = 'verify@me.com'
const response = await restClient.POST(`/${customVerificationTokenSlug}`, {
body: JSON.stringify({
email: emailToVerify,
password,
roles: ['editor'],
}),
headers: {
Authorization: `JWT ${token}`,
},
})

expect(response.status).toBe(201)

const userResult = await payload.find({
collection: customVerificationTokenSlug,
limit: 1,
showHiddenFields: true,
where: {
email: {
equals: emailToVerify,
},
},
})

const { _verificationToken, _verified } = userResult.docs[0]

expect(_verified).toBe(false)
expect(_verificationToken).toBe(expectedVerificationToken)

const verificationResponse = await restClient.POST(
`/${customVerificationTokenSlug}/verify/${_verificationToken}`,
)

expect(verificationResponse.status).toBe(200)

const afterVerifyResult = await payload.find({
collection: customVerificationTokenSlug,
limit: 1,
showHiddenFields: true,
where: {
email: {
equals: emailToVerify,
},
},
})

const { _verificationToken: afterToken, _verified: afterVerified } =
afterVerifyResult.docs[0]
expect(afterVerified).toBe(true)
expect(afterToken).toBeNull()
})

describe('User Preferences', () => {
const key = 'test'
const property = 'store'
Expand Down
4 changes: 4 additions & 0 deletions test/auth/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@ export const partialDisableLocalStrategiesSlug = 'partial-disable-local-strategi
export const namedSaveToJWTValue = 'namedSaveToJWT value'

export const saveToJWTKey = 'x-custom-jwt-property-name'

export const customVerificationTokenSlug = 'custom-verification-token'

export const expectedVerificationToken = 'custom-verification-token-12345'