How to Implement a User Password Reset Feature: From Token Issuance to Email Sending and Security Measures
Introduction
In the previous chapters, we implemented account registration, login, and logout using email and password with Next.js and Auth.js. While we now have the minimum authentication features, to operate a web service in practice we need to anticipate various cases and implement additional functionality.
- Verify that the registered email address is correct (so we can use it for various notifications and announcements)
- Password reset (for users who have forgotten their password)
- Issue one-time tokens to strengthen security with two-factor authentication
In this article series, we will implement one of these: “Password reset (for users who have forgotten their password)” over two articles.
Implementation plan for password reset
This time, we will newly prepare a screen where users can enter the email address for which they want to reset the password.
When the user enters their email address, we send an email containing a link to a screen where they can set a new password.
When the user opens that email and clicks the link, we display a screen where they can set a new password.
On that screen, the user enters a new password. After that, the server verifies the token and, if it is valid, updates the password.
The user then logs in using the new password.
Goal of this article
We will implement the process that sends the email. The process that actually resets the password after clicking the link in the email will be implemented in the next article.
Implementation flow
We will proceed with the implementation in the following steps:
- ① Enable storing tokens in the database
- ② Implement token creation
- ③ Implement email sending
- ④ Create the password reset request screen
- ⑤ Create a server action that sends the email after user input
- ⑥ Verify operation
① Enable storing tokens in the database
Create a new model so that issued tokens can be stored in the database. Add the following to the existing schema.prisma.
+ model PasswordResetToken {
+ id String @id @default(cuid())
+ email String
+ token String @unique
+ expires DateTime
+ @@unique([email, token])
+ }
In addition to the token, we also store which email address it is for and when it expires.
After writing the code, generate types for use in TypeScript with the following command:
$ npx prisma generate
Reflect the schema definition in the database so that a table named PasswordResetToken is created.
$ npx prisma db push
Confirm that the table has been created in the database.
$ npx prisma studio
② Token creation process
First, install uuid, which we will use when generating tokens.
npm i uuid
npm i --save-dev @types/uuid
This is a convenient library for generating random strings.
Add the following code to the lib/tokens.ts file.
+ export const generatePasswordResetToken = async (email: string) => {
+ const token = uuidv4()
+ const expires = new Date(new Date().getTime() + 60 * 60 * 1000)
+
+ //TODO: Delete if token already exists
+
+ const passwordResetToken = await db.passwordResetToken.create({
+ data: {
+ email,
+ token,
+ expires,
+ },
+ })
+
+ return passwordResetToken
+ }
The expiration time is set to one hour from the time the token is created.
60(seconds) * 60(minutes) * 1000(milliseconds)
Next, create a data/password-reset-token.ts file and add the following code:
import db from '@/lib/db'
export const getPasswordResetTokenByToken = async (token: string) => {
try {
const passwordResetToken = await db.passwordResetToken.findUnique({
where: { token },
})
return passwordResetToken
} catch {
return null
}
}
export const getPasswordResetTokenByEmail = async (email: string) => {
try {
const passwordResetToken = await db.passwordResetToken.findFirst({
where: { email },
})
return passwordResetToken
} catch {
return null
}
}
Both functions retrieve tokens registered in the database, but we provide two versions because the argument differs: one takes email and the other token. If a token exists, it is returned; if not, null is returned.
Go back to the lib/tokens.ts file and delete any existing token if it exists. This ensures that when multiple password reset tokens are issued, only the latest token remains valid.
import { v4 as uuidv4 } from 'uuid'
+ import { getPasswordResetTokenByEmail } from '@/data/password-reset-token'
import db from '@/lib/db'
export const generateVerificationToken = async (email: string) => {
const token = uuidv4()
const expires = new Date(new Date().getTime() + 60 * 60 * 1000)
+ const existingToken = await getPasswordResetTokenByEmail(email)
+
+ if (existingToken) {
+ await db.passwordResetToken.delete({
+ where: { id: existingToken.id },
+ })
+ }
const verificationToken = await db.verificationToken.create({
data: {
email,
token,
expires,
},
})
return verificationToken
}
③ Implement email sending using the created token
First, set up Sendgrid.
Follow the “Managing API Keys” section in this guide to obtain an API key and add it to your .env file.
SENDGRID_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Sendgrid provides a library for node.js, and using it makes the email sending process smoother.
npm i @sendgrid/mail
The usage of the above library is explained here:
Implement the email sending process in the lib/mail.ts file.
+ export const sendPasswordResetEmail = async (email: string, token: string) => {
+ const resetLink = `${domain}/auth/new-password?token=${token}`
+
+ await sendgrid.send({
+ from: 'test-taro@shinagawa-web.com',
+ to: email,
+ subject: 'Password Reset Notification',
+ html: `<p>Please click the link below to reset your password.<br><a href="${resetLink}">Click here</a></p>`,
+ })
+ }
export const sendPasswordResetEmail = async (email: string, token: string) => {
It takes the email address and token as arguments.
const resetLink = `${domain}/auth/new-password?token=${token}`
This generates the URL for the link that the user will click when they open the email.
By including the token in this link, the server can verify the token when the link is accessed.
await sendgrid.send({
from: 'test-taro@shinagawa-web.com',
to: email,
subject: 'Password Reset Notification',
html: `<p>Please click the link below to reset your password.<br><a href="${resetLink}">Click here</a></p>`,
})
The sender of the email is defined with from. This must be a domain that has been verified with your email sending service. In my case, I use my company’s domain.
The html key defines the email body in HTML format. We use an anchor tag to set the link.
④ Create a server action that sends the email after user input
So far, we have implemented partial processes:
- Token creation
- Email sending that takes a token
Now we will combine these and implement a server action that receives the email address actually entered by the user and runs the entire flow.
First, create a Zod schema.
Add the following code to the schema/index.ts file.
+ export const ResetSchema = z.object({
+ email: z.string().email({
+ message: 'Email is required',
+ }),
+ })
This schema validates the user’s email address input.
Next, implement the server action using the schema you just created.
Add the following code to actions/reset.ts.
'use server'
import * as z from 'zod'
import { getUserByEmail } from '@/data/user'
import { sendPasswordResetEmail } from '@/lib/mail'
import { generatePasswordResetToken } from '@/lib/tokens'
import { ResetSchema } from '@/schema'
export const reset = async (values: z.infer<typeof ResetSchema>) => {
const validatedFields = ResetSchema.safeParse(values)
if (!validatedFields.success) {
return { error: 'Please correct the input' }
}
const { email } = validatedFields.data
const existingUser = await getUserByEmail(email)
if (!existingUser) {
return { error: 'Email address does not exist.' }
}
try {
const passwordResetToken = await generatePasswordResetToken(email)
await sendPasswordResetEmail(
passwordResetToken.email,
passwordResetToken.token,
)
return {
success:
'Sent 'Password Reset Notification' to the registered email address.',
}
} catch (error) {
console.error('Error sending password reset email:', error)
return { error: 'An error occurred.' }
}
}
Here is an explanation of the code:
const validatedFields = ResetSchema.safeParse(values)
if (!validatedFields.success) {
return { error: 'Please correct the input' }
}
First, we validate the email address entered by the user.
If the email format is incorrect, we return an error.
const { email } = validatedFields.data
const existingUser = await getUserByEmail(email)
if (!existingUser) {
return { error: 'Email address does not exist.' }
}
We check that the entered email address is already registered, and if it is not, we return an error.
The target of password reset must be an email address that has already been used to register an account.
try {
const passwordResetToken = await generatePasswordResetToken(email)
await sendPasswordResetEmail(
passwordResetToken.email,
passwordResetToken.token,
)
return {
success:
'Sent 'Password Reset Notification' to the registered email address.',
}
} catch (error) {
console.error('Error sending password reset email:', error)
return { error: 'An error occurred.' }
}
We use the generatePasswordResetToken() function we created earlier to generate a token.
Then we send the email using the user’s email address and the token.
If the email sending process completes successfully, we return a success message to the client side.
⑤ Create the password reset request screen
Now we will create the password reset request screen where the user can enter their email address.
First, create the input form.
The input form implementation is similar to the account registration and login forms we have already implemented.
Create a file components/auth/reset-form.tsx and add the following code:
'use client'
import { zodResolver } from '@hookform/resolvers/zod'
import { useState, useTransition } from 'react'
import { useForm } from 'react-hook-form'
import * as z from 'zod'
import { reset } from '@/actions/reset'
import { CardWrapper } from '@/components/auth/card-wrapper'
import { FormError } from '@/components/form-error'
import { FormSuccess } from '@/components/form-success'
import { Button } from '@/components/ui/button'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { ResetSchema } from '@/schema'
export const ResetForm = () => {
const [error, setError] = useState<string | undefined>('')
const [success, setSuccess] = useState<string | undefined>('')
const [isPending, startTransition] = useTransition()
const form = useForm<z.infer<typeof ResetSchema>>({
resolver: zodResolver(ResetSchema),
defaultValues: {
email: '',
},
})
const onSubmit = (values: z.infer<typeof ResetSchema>) => {
setError('')
setSuccess('')
startTransition(() => {
reset(values).then((data) => {
setError(data?.error)
setSuccess(data?.success)
})
})
}
return (
<CardWrapper
headerLabel="Password Reset"
buttonLabel="Return to Login Screen"
buttonHref="/auth/login"
>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<div className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
{...field}
disabled={isPending}
placeholder="nextjs@example.com"
type="email"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormError message={error} />
<FormSuccess message={success} />
<Button disabled={isPending} type="submit" className="w-full">
Send Email
</Button>
</form>
</Form>
</CardWrapper>
)
}
Here is an explanation of the code:
import { reset } from '@/actions/reset'
((※ Partially omitted))
const onSubmit = (values: z.infer<typeof ResetSchema>) => {
setError('')
setSuccess('')
startTransition(() => {
reset(values).then((data) => {
setError(data?.error)
setSuccess(data?.success)
})
})
}
This is the process that runs after the user enters their email address and clicks “Send email”.
We pass values (the email address here) to the server action we created earlier and execute it.
We then receive either a success or error message as the result and store it with useState.
<FormError message={error} />
<FormSuccess message={success} />
This is where we display whichever message (success or error) we received.
Next, we implement the screen using the form we just created.
Create the /app/auth/reset/page.tsx file and add the following code:
import { ResetForm } from '@/components/auth/reset-form'
const ResetPage = () => {
return <ResetForm />
}
export default ResetPage
This completes the creation of the password reset request screen.
Next, we will provide a path for users to reach this screen. On the login screen, we will display a link that navigates to the password reset request screen.
Add the following code to components/auth/login-form.tsx.
+ import Link from 'next/link'
((※ Partially omitted))
</FormControl>
<FormMessage />
+ <Button
+ size="sm"
+ variant="link"
+ asChild
+ className="px-0 font-normal"
+ >
+ <Link href="/auth/reset">Forgot your password? Click here</Link>
+ </Button>
</FormItem>
)}
/>
A link is now displayed right next to the password input form.
Finally, configure routing.
With the current middleware settings, the new screen cannot be accessed while logged out, so we add paths to allow access.
export const authRoutes: string[] = [
'/auth/login',
'/auth/register',
'/auth/new-verification',
+ '/auth/new-password',
+ '/auth/reset',
]
Here we add not only the input screen but also the “New Password Setting Screen” that we will create in the next article. This screen also needs to be accessible while logged out, so it must be added.
⑤ Operation check
Using an email address that has already been registered, access the “Password Reset Request Screen” and enter the email address.
When you send the email, you should receive an email.
If you click the link, you will probably see a 404 page.
Check the URL and confirm that it is:
http://localhost:3000/auth/new-password?token=xxxxxxxxxxxxxxxxxxxxxxxxx
If the path is /auth/new-password, which is the new password setting screen, then it is OK.
Conclusion
In this article, we implemented token generation and email sending so that users can enter the email address for which they want to reset the password and actually receive an email.
There were many similarities with the implementation in “Implementing Email Verification ① Sending Emails”. Depending on how you write the code, you could also generalize some functions. If I get the chance, I would like to write an article about reducing effort through such generalization and share it.
Next article
Questions about this article 📝
If you have any questions or feedback about the content, please feel free to contact us.Go to inquiry form
Related Articles
Robust Authorization Design for GraphQL and REST APIs: Best Practices for RBAC, ABAC, and OAuth 2.0
2024/05/13Chat App (with Image/PDF Sending and Video Call Features)
2024/07/15Practical Component Design Guide with React × Tailwind CSS × Emotion: The Optimal Approach to Design Systems, State Management, and Reusability
2024/11/22Management Dashboard Features (Graph Display, Data Import)
2024/06/02Let's Build an Article Posting API with NestJS ─ Basics of Introducing Prisma and Implementing CRUD
2025/04/12Improving the Reliability of a NestJS App ─ Logging, Error Handling, and Testing Strategy
2024/09/11Deepening DB Design with NestJS × Prisma ─ Models, Relations, and Operational Design
2024/09/12NestJS × React × Railway: Implementing a Blog UI and Deploying to Production
2024/10/25






