Password Reset Emails In Your React App Made Easy with Nodemailer
Paige Niedringhaus
Posted on October 29, 2022
Password Resets in a MERN Application
Before I actually attempted to build in an email-based password reset for my MERN app, I thought it would be tougher to do. From everything I’d heard before, sending emails in a JavaScript application was painful, but I still wanted to attempt it.
For a few months, to hone my full stack JavaScript skills, I’ve been slowly building and adding on to a user registration service.
First, I built it with a React frontend, an Express / Node.js backend and a Docker-powered MySQL database. I used a docker-compose.yml
to start the whole app with one command (if you’d like to read more about my use of Docker for development, you can see this blog post).
After I’d gotten that working, I added in authorization to the app using Passport.js and JSON Web Tokens (JWTs). You can read about the fun (pain) of that here, if you’re curious. And it took me a while — I ran into a bunch of road blocks and obstacles that stalled me out multiple times. But grit and my inability to let go of a problem once it’s taken root in my brain prevailed, and I figured it out and moved on.
When I decided to tackle sending password reset links via email (just like real sites when users, myself included, inevitably forget their password), I figured I was in for more pain. It just couldn’t be too easy, no matter that practically every site out there has this very functionality. But I was wrong. And I’m so glad I was.
Today, I'll show you how to set up your own password reset system with help from Nodemailer.
Nodemailer: the magic bullet
Once I started googling around looking for solutions to my password reset feature, I came across a number of articles recommending Nodemailer.
When I visited the website, the first lines I read were:
Nodemailer is a module for Node.js applications to allow easy as cake email sending. The project got started back in 2010 when there was no sane option to send email messages, today it is the solution most Node.js users turn to by default. — Nodemailer
And you know what? It wasn’t kidding. Easy as cake isn’t too far wrong.
Of course, before I got started, I did a little more digging to make sure I was placing my faith in a decent piece of technology, and what I saw on npm and GitHub put my mind at ease.
Nodemailer has:
- Over 615,000 weekly downloads from NPM,
- Over 10,000 stars on Github,
- 206 releases to date,
- Over 2,500 dependent packages,
- And it’s been around in some form or fashion since 2010.
Ok, that seemed good enough for me to give it a shot in my own project.
Implementation of Nodemailer in my code (frontend and backend)
I didn’t need anything fancy for my password reset, just:
- A way to send an email to a user’s address.
- That email would contain a link that would redirect them to a protected page on my site where they could reset their password.
- Then they could log in using their new password.
- I also wanted the password reset link to expire after a certain time period for better security.
Here’s how I did it.
Frontend code (client folder): send reset email
I’ll start with the React code first, because I had to have a page where users could enter their email addresses and fire off the email with the reset link.
/* eslint-disable no-console */
import React, { Component } from 'react';
import TextField from '@material-ui/core/TextField';
import axios from 'axios';
import {
LinkButtons,
SubmitButtons,
registerButton,
homeButton,
forgotButton,
inputStyle,
HeaderBar,
} from '../components';
const title = {
pageTitle: 'Forgot Password Screen',
};
class ForgotPassword extends Component {
constructor() {
super();
this.state = {
email: '',
showError: false,
messageFromServer: '',
showNullError: false,
};
}
handleChange = name => (event) => {
this.setState({
[name]: event.target.value,
});
};
sendEmail = async (e) => {
e.preventDefault();
const { email } = this.state;
if (email === '') {
this.setState({
showError: false,
messageFromServer: '',
showNullError: true,
});
} else {
try {
const response = await axios.post(
'http://localhost:3003/forgotPassword',
{
email,
},
);
if (response.data === 'recovery email sent') {
this.setState({
showError: false,
messageFromServer: 'recovery email sent',
showNullError: false,
});
}
} catch (error) {
console.error(error.response.data);
if (error.response.data === 'email not in db') {
this.setState({
showError: true,
messageFromServer: '',
showNullError: false,
});
}
}
}
};
render() {
const { email, messageFromServer, showNullError, showError } = this.state;
return (
<div>
<HeaderBar title={title} />
<form className="profile-form" onSubmit={this.sendEmail}>
<TextField
style={inputStyle}
id="email"
label="email"
value={email}
onChange={this.handleChange('email')}
placeholder="Email Address"
/>
<SubmitButtons
buttonStyle={forgotButton}
buttonText="Send Password Reset Email"
/>
</form>
{showNullError && (
<div>
<p>The email address cannot be null.</p>
</div>
)}
{showError && (
<div>
<p>
That email address isn't recognized. Please try again or
register for a new account.
</p>
<LinkButtons
buttonText="Register"
buttonStyle={registerButton}
link="/register"
/>
</div>
)}
{messageFromServer === 'recovery email sent' && (
<div>
<h3>Password Reset Email Successfully Sent!</h3>
</div>
)}
<LinkButtons buttonText="Go Home" buttonStyle={homeButton} link="/" />
</div>
);
}
}
export default ForgotPassword;
Ok, I know this is a big code snippet but I’ll break it down.
If you want to copy/paste the actual code, you can see the whole repo here.
What you should really focus on here is the sendEmail()
function and the render()
method of the component. The rest is just setting initial state and variables, and styling of buttons and elements.
The render() method
render() {
const { email, messageFromServer, showNullError, showError } = this.state;
return (
<div>
<HeaderBar title={title} />
<form className="profile-form" onSubmit={this.sendEmail}>
<TextField
style={inputStyle}
id="email"
label="email"
value={email}
onChange={this.handleChange('email')}
placeholder="Email Address"
/>
<SubmitButtons
buttonStyle={forgotButton}
buttonText="Send Password Reset Email"
/>
</form>
{showNullError && (
<div>
<p>The email address cannot be null.</p>
</div>
)}
{showError && (
<div>
<p>
That email address isn't recognized. Please try again or
register for a new account.
</p>
<LinkButtons
buttonText="Register"
buttonStyle={registerButton}
link="/register"
/>
</div>
)}
{messageFromServer === 'recovery email sent' && (
<div>
<h3>Password Reset Email Successfully Sent!</h3>
</div>
)}
<LinkButtons buttonText="Go Home" buttonStyle={homeButton} link="/" />
</div>
);
}
Notice inside the render()
method, that I have a simple input box for the user to enter his/her email address, and a submit button which triggers the this.sendEmail()
function when clicked. Beyond that, I have a little error and success handling built in based on if the user hasn’t entered an email, if the server responds back the email was successfully sent or it wasn’t a recognized address.
The sendEmail() function
sendEmail = async (e) => {
e.preventDefault();
const { email } = this.state;
if (email === '') {
this.setState({
showError: false,
messageFromServer: '',
showNullError: true,
});
} else {
try {
const response = await axios.post(
'http://localhost:3003/forgotPassword',
{
email,
},
);
if (response.data === 'recovery email sent') {
this.setState({
showError: false,
messageFromServer: 'recovery email sent',
showNullError: false,
});
}
} catch (error) {
console.error(error.response.data);
if (error.response.data === 'email not in db') {
this.setState({
showError: true,
messageFromServer: '',
showNullError: false,
});
}
}
}
}
For all of my HTTP requests, I’m using the Axios library, which just makes it really easy to make AJAX calls to the server, even easier than the built in fetch() web API, in my opinion.
When the user enters his/her email, I make a POST
request to the server, and wait for a response. If the email address isn’t found, I can tell the user they entered it wrong or if they’re new, they can go to the register page and create an account, and if the address does match one in my database, they’ll get back a success message saying the password reset link has been sent to their email address.
Let’s move on to the backend code now.
Backend code (API Folder): send reset email
/* eslint-disable max-len */
/* eslint-disable no-console */
import crypto from 'crypto';
import User from '../sequelize';
require('dotenv').config();
const nodemailer = require('nodemailer');
module.exports = (app) => {
app.post('/forgotPassword', (req, res) => {
if (req.body.email === '') {
res.status(400).send('email required');
}
console.error(req.body.email);
User.findOne({
where: {
email: req.body.email,
},
}).then((user) => {
if (user === null) {
console.error('email not in database');
res.status(403).send('email not in db');
} else {
const token = crypto.randomBytes(20).toString('hex');
user.update({
resetPasswordToken: token,
resetPasswordExpires: Date.now() + 3600000,
});
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: `${process.env.EMAIL_ADDRESS}`,
pass: `${process.env.EMAIL_PASSWORD}`,
},
});
const mailOptions = {
from: 'mySqlDemoEmail@gmail.com',
to: `${user.email}`,
subject: 'Link To Reset Password',
text:
'You are receiving this because you (or someone else) have requested the reset of the password for your account.\n\n'
+ 'Please click on the following link, or paste this into your browser to complete the process within one hour of receiving it:\n\n'
+ `http://localhost:3031/reset/${token}\n\n`
+ 'If you did not request this, please ignore this email and your password will remain unchanged.\n',
};
console.log('sending mail');
transporter.sendMail(mailOptions, (err, response) => {
if (err) {
console.error('there was an error: ', err);
} else {
res.status(200).json('recovery email sent');
}
});
}
});
});
};
The backend code is a little more involved. This is where Nodemailer comes into play.
When the user hits the forgotPassword
route on the backend with the email address they entered, the first thing Sequelize (my SQL ORM) does is check if that email exists in my database. If it doesn’t the user gets notified they may have mistyped it, if it does exist, then a series of other events starts.
None of the following steps is very difficult, it’s just chaining them all together that was a little tricky, at first.
1. Generate an expiring token for the user
const token = crypto.randomBytes(20).toString('hex');
user.update({
resetPasswordToken: token,
resetPasswordExpires: Date.now() + 3600000,
});
The first step after confirming the email belongs to a user in the database, is generating a token that can be attached to the user’s account and setting a time limit for that token to be valid.
Node.js has a built in module called Crypto, which provides cryptographic functionality, which is a fancy way of saying, I can generate a unique hash token easily using the JavaScript crypto.randomBytes(20).toString('hex');
. Then, I save that new token to my user’s profile in the database under the column name resetPasswordToken
. I also set a timestamp for how long that token will be valid. I made mine valid for one hour after sending the link: Date.now() + 3600000
.
2. Create Nodemailer transport
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: `${process.env.EMAIL_ADDRESS}`,
pass: `${process.env.EMAIL_PASSWORD}`,
},
});
Next, I created the transporter which is actually the account sending the password reset email link.
I chose to use Gmail, because I use Gmail personally, and I created a new dummy account to send the emails. Since I don’t want to give out the credentials for that account to anyone, I put the credentials into a .env
file that is included in my .gitignore
so it never gets committed to GitHub or anywhere else.
The npm package dotenv is used to read the contents of the file and insert the email address and password for Nodemailer’s [createTransport()] function to pick up.
3. Create mail options
const mailOptions = {
from: 'mySqlDemoEmail@gmail.com',
to: `${user.email}`,
subject: 'Link To Reset Password',
text:
'You are receiving this because you (or someone else) have requested the reset of the password for your account.\n\n'
+ 'Please click on the following link, or paste this into your browser to complete the process within one hour of receiving it:\n\n'
+ `http://localhost:3031/reset/${token}\n\n`
+ 'If you did not request this, please ignore this email and your password will remain unchanged.\n',
};
The third step is creating the email template or mailOptions
as Nodemailer calls it that the user will see (and this is also where the verified email address they pass from the frontend input gets used).
There are whole third-party libraries for making great looking emails to go with the Nodemailer module, but I just wanted a bare bones email, so I made this one myself.
It contains the from
email address (mySqlDemoEmail[at]gmail.com, for me), the user’s email goes in the to
box, the subject
line is something along the lines of reset password link, and the text
is a simple string containing a little info and the website’s URL reset route including the token I created earlier, tacked on to the end. This will allow me to verify the user is who they say they are when they click the link and go to the site to reset their password.
4. Send Email
transporter.sendMail(mailOptions, (err, response) => {
if (err) {
console.error('there was an error: ', err);
} else {
res.status(200).json('recovery email sent');
}
});
The final step of this file actually putting together the pieces I created: the transporter
, the mailOptions
, the token
and using Nodemailer’s sendMail()
function. If it works, I’ll get back a 200 response, which I then use to trigger a success call to the client, and if it fails, I log out the error to see what went wrong.
Enabling Gmail to send reset emails
There’s an extra gotcha to be aware of when setting up the transporter email that all the emails are sent from, at least, when using Gmail.
In order to be able to send emails from an account, 2-Step verification must be disabled, and a setting titled ‘Allow less secure apps’ must be toggled to on. See screenshot below. To do this, I went to my settings here, and turned it on.
Now, I could send reset emails with no problems. If you’re having trouble, check Nodemailer’s FAQs for more help.
This is the screen you should see where you can turn ‘on’ less secure apps. Just one more reason to use some dummy email account instead of your actual Gmail account too.
Frontend code: update password screen
Great, now users should be getting reset emails in their inbox that look something like this.
It’s a simple email, but it does what I need it to do.
If you notice, the third line is a link to my website (running locally on port 3031), to another page called ‘Reset’, followed by the hashed token I generated back in step 1 with the Node.js crypto
module.
When a user clicks this link, they’re directed to a new page in the application entitled ‘Password Reset Screen’, which can only be accessed with a valid token. If the token has expired or is otherwise invalid, the user will see an error screen with links to go home or attempt to send a new password reset email.
Here’s the React code for the reset screen.
/* eslint-disable no-console */
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import axios from 'axios';
import TextField from '@material-ui/core/TextField';
import {
LinkButtons,
updateButton,
homeButton,
loginButton,
HeaderBar,
forgotButton,
inputStyle,
SubmitButtons,
} from '../components';
const loading = {
margin: '1em',
fontSize: '24px',
};
const title = {
pageTitle: 'Password Reset Screen',
};
export default class ResetPassword extends Component {
constructor() {
super();
this.state = {
username: '',
password: '',
updated: false,
isLoading: true,
error: false,
};
}
async componentDidMount() {
const {
match: {
params: { token },
},
} = this.props;
try {
const response = await axios.get('http://localhost:3003/reset', {
params: {
resetPasswordToken: token,
},
});
if (response.data.message === 'password reset link a-ok') {
this.setState({
username: response.data.username,
updated: false,
isLoading: false,
error: false,
});
}
} catch (error) {
console.log(error.response.data);
this.setState({
updated: false,
isLoading: false,
error: true,
});
}
}
handleChange = name => (event) => {
this.setState({
[name]: event.target.value,
});
};
updatePassword = async (e) => {
e.preventDefault();
const { username, password } = this.state;
const {
match: {
params: { token },
},
} = this.props;
try {
const response = await axios.put(
'http://localhost:3003/updatePasswordViaEmail',
{
username,
password,
resetPasswordToken: token,
},
);
if (response.data.message === 'password updated') {
this.setState({
updated: true,
error: false,
});
} else {
this.setState({
updated: false,
error: true,
});
}
} catch (error) {
console.log(error.response.data);
}
};
render() {
const {
password, error, isLoading, updated
} = this.state;
if (error) {
return (
<div>
<HeaderBar title={title} />
<div style={loading}>
<h4>Problem resetting password. Please send another reset link.</h4>
<LinkButtons
buttonText="Go Home"
buttonStyle={homeButton}
link="/"
/>
<LinkButtons
buttonStyle={forgotButton}
buttonText="Forgot Password?"
link="/forgotPassword"
/>
</div>
</div>
);
}
if (isLoading) {
return (
<div>
<HeaderBar title={title} />
<div style={loading}>Loading User Data...</div>
</div>
);
}
return (
<div>
<HeaderBar title={title} />
<form className="password-form" onSubmit={this.updatePassword}>
<TextField
style={inputStyle}
id="password"
label="password"
onChange={this.handleChange('password')}
value={password}
type="password"
/>
<SubmitButtons
buttonStyle={updateButton}
buttonText="Update Password"
/>
</form>
{updated && (
<div>
<p>
Your password has been successfully reset, please try logging in
again.
</p>
<LinkButtons
buttonStyle={loginButton}
buttonText="Login"
link="/login"
/>
</div>
)}
<LinkButtons buttonText="Go Home" buttonStyle={homeButton} link="/" />
</div>
);
}
}
ResetPassword.propTypes = {
// eslint-disable-next-line react/require-default-props
match: PropTypes.shape({
params: PropTypes.shape({
token: PropTypes.string.isRequired,
}),
}),
};
And here’s the three main pieces of this component that do the heavy lifting.
The initial componentDidMount() lifecycle method
async componentDidMount() {
const {
match: {
params: { token },
},
} = this.props;
try {
const response = await axios.get('http://localhost:3003/reset', {
params: {
resetPasswordToken: token,
},
});
if (response.data.message === 'password reset link a-ok') {
this.setState({
username: response.data.username,
updated: false,
isLoading: false,
error: false,
});
}
} catch (error) {
console.log(error.response.data);
this.setState({
updated: false,
isLoading: false,
error: true,
});
}
}
This method fires as soon as the page is reached. It extracts the token from the URL query parameters and passes it back to the server’s reset
route to verify the token is legit.
Then, the server either responds with an "a-ok", this token is valid and associated with the user or a "no", the token’s no good anymore for some reason.
The updatePassword() function
updatePassword = async (e) => {
e.preventDefault();
const { username, password } = this.state;
const {
match: {
params: { token },
},
} = this.props;
try {
const response = await axios.put(
'http://localhost:3003/updatePasswordViaEmail',
{
username,
password,
resetPasswordToken: token,
},
);
if (response.data.message === 'password updated') {
this.setState({
updated: true,
error: false,
});
} else {
this.setState({
updated: false,
error: true,
});
}
} catch (error) {
console.log(error.response.data);
}
};
This is the function that will fire if the user is authenticated and allowed to reset their password. It also accesses a specific route on the server called updatePasswordViaEmail()
(I did this because I gave users a separate route to update their password while logged in to the app, as well), and once the updated password has been saved to the database, a success message response is sent back to the client.
The render() method
render() {
const { password, error, isLoading, updated } = this.state;
if (error) {
return (
<div>
<HeaderBar title={title} />
<div style={loading}>
<h4>Problem resetting password. Please send another reset link.</h4>
<LinkButtons
buttonText="Go Home"
buttonStyle={homeButton}
link="/"
/>
<LinkButtons
buttonStyle={forgotButton}
buttonText="Forgot Password?"
link="/forgotPassword"
/>
</div>
</div>
);
}
if (isLoading) {
return (
<div>
<HeaderBar title={title} />
<div style={loading}>Loading User Data...</div>
</div>
);
}
return (
<div>
<HeaderBar title={title} />
<form className="password-form" onSubmit={this.updatePassword}>
<TextField
style={inputStyle}
id="password"
label="password"
onChange={this.handleChange('password')}
value={password}
type="password"
/>
<SubmitButtons
buttonStyle={updateButton}
buttonText="Update Password"
/>
</form>
{updated && (
<div>
<p>
Your password has been successfully reset, please try logging in again.
</p>
<LinkButtons
buttonStyle={loginButton}
buttonText="Login"
link="/login"
/>
</div>
)}
<LinkButtons buttonText="Go Home" buttonStyle={homeButton} link="/" />
</div>
);
}
The last piece of this component, is the render()
method. Initially, while the token is being verified for its validity, the loading
message shows.
If the link is invalid in some way, the error
message shows on the screen with links back to the home screen or forgot password page.
If the user is authorized to reset their password, they get the new password input with the updatePassword()
function attached to it, and once the server’s responded with success updating the password, the updated
boolean is set to true and the Your password has been successfully reset...
message is shown along with a login button.
Backend code: reset password & update password
Ok, I’m in the home stretch now. Here’s the last two routes on the server side you’ll need. These correspond to the two methods I just walked through on the client side in the React ResetPassword.js
component.
/* eslint-disable no-console */
/* eslint-disable max-len */
import Sequelize from 'sequelize';
import User from '../sequelize';
// eslint-disable-next-line prefer-destructuring
const Op = Sequelize.Op;
module.exports = (app) => {
app.get('/reset', (req, res) => {
User.findOne({
where: {
resetPasswordToken: req.query.resetPasswordToken,
resetPasswordExpires: {
[Op.gt]: Date.now(),
},
},
}).then((user) => {
if (user == null) {
console.error('password reset link is invalid or has expired');
res.status(403).send('password reset link is invalid or has expired');
} else {
res.status(200).send({
username: user.username,
message: 'password reset link a-ok',
});
}
});
});
};
This is the route that’s called on the componentDidMount()
lifecycle method on the client side. It checks the resetPasswordToken
passed from the link’s query parameters and date timestamp to ensure that everything’s good.
You’ll notice the resetPasswordExpires
parameter has an odd looking $gt: Date.now()
parameter. This is an operator alias comparator, which Sequelize allows me to use, and all the $gt:
stands for is "greater than" whatever it is being compared to. In this instance, it’s comparing the current time to the expiration time stamp saved to the database when the reset password email was sent, to make sure the password’s being reset less than an hour after the email was sent.
As long as both parameters are valid for that user, the client is sent a successful response and the user can proceed with the password reset.
/* eslint-disable no-console */
import bcrypt from 'bcrypt';
import Sequelize from 'sequelize';
import User from '../sequelize';
// eslint-disable-next-line prefer-destructuring
const Op = Sequelize.Op;
const BCRYPT_SALT_ROUNDS = 12;
module.exports = app => {
app.put('/updatePasswordViaEmail', (req, res) => {
User.findOne({
where: {
username: req.body.username,
resetPasswordToken: req.body.resetPasswordToken,
resetPasswordExpires: {
[Op.gt]: Date.now(),
},
},
}).then(user => {
if (user == null) {
console.error('password reset link is invalid or has expired');
res.status(403).send('password reset link is invalid or has expired');
} else if (user != null) {
console.log('user exists in db');
bcrypt
.hash(req.body.password, BCRYPT_SALT_ROUNDS)
.then(hashedPassword => {
user.update({
password: hashedPassword,
resetPasswordToken: null,
resetPasswordExpires: null,
});
})
.then(() => {
console.log('password updated');
res.status(200).send({ message: 'password updated' });
});
} else {
console.error('no user exists in db to update');
res.status(401).json('no user exists in db to update');
}
});
});
};
This is the second route that’s called when the user submits his/her password to be updated.
Once again, I find the user in the database and double check that the resetPasswordToken
also exists and is valid (the username
was passed back to the client from the reset route above and held in the app’s state until the update function was called), I hash the new password using my bcrypt
module (just like my Passport.js middleware does when a new user is being written into the database initially),update that user’s password
in the database with the new hash and set both the resetPasswordToken
and resetPasswordExpires
columns back to null, so the same link can’t be used more than once.
As soon as that’s complete, the server sends back a 200 response with the success message, Password updated
for the client.
And you’ve successfully reset a user’s password via email. Not too tough.
Conclusion
At first glance, resetting a user’s password via an email link seems a bit daunting. But Nodemailer helps make a major factor (the emailing bit) simple. And once that’s done it’s just a few routes on the server side and inputs on the client side to get that password updated for the user.
Check back in a few weeks, I’ll be writing about using Puppeteer and headless Chrome for end-to-end testing or something else related to web development.
If you’d like to make sure you never miss an article I write, sign up for my newsletter here: https://paigeniedringhaus.substack.com
Thanks for reading, I hope this gives you an idea of how to use Nodemailer to send password reset emails for a MERN application.
References & Further Resources
- Nodemailer
- Nodemailer, GitHub
- Nodemailer, npm
- MERN app with Nodemailer repo
Posted on October 29, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.