- A user registers for an account. The user is created, but the user still needs to be verified via an email confirmation. The user cannot login until their account is verified.
- A verification token is emailed to the user.
- The user receives the verification email in their inbox. A link is provided in the email that passes the verification token back into your application.
Creating Your Models
We’re going to need to make a couple of modifications to your database to support email verification.
Tracking Verification In Your User Model
To get rolling, we’re going to need a way to distinguish which users have been verified. To track verification, we’re going to add a new isVerified
flag to the User model. Notice that the default value for isVerified
is false
.
1 2 3 4 5 6 7 8 9 |
var userSchema = new mongoose.Schema({ name: String, email: { type: String, unique: true }, roles: [{ type: 'String' }], isVerified: { type: Boolean, default: false }, password: String, passwordResetToken: String, passwordResetExpires: Date }, schemaOptions); |
Create Token Verification Model
When a user signs up, we’re going to create a verification token within Mongo. We need a new model to handle our verification tokens.
1 2 3 4 5 |
const tokenSchema = new mongoose.Schema({ _userId: { type: mongoose.Schema.Types.ObjectId, required: true, ref: 'User' }, token: { type: String, required: true }, createdAt: { type: Date, required: true, default: Date.now, expires: 43200 } }); |
- Not surprisingly, you will need to provide the userId of the user the token is issued.
- More interesting, there is a powerful feature in Mongo called “expires” that sets a documents time to live, known as TTL. In the model above, the TTL expires attribute is set to 43200 seconds, meaning the verification token document will automatically delete itself after 12 hours. This means users will have 12 hours to activate their accounts before their verification tokens expire. If a user doesn’t confirm their account in time, they can request a new verification token.
Registering Additional Routes in Express
In addition to your normal Sign Up and Login functions, we’re going to need two new routes in Express.
1 2 |
app.post('/confirmation', userController.confirmationPost); app.post('/resend', userController.resendTokenPost); |
The first route will be used for token confirmation. The second route will be used in case a user needs to resend a new confirmation token.
Tying Everything Together In Node
The majority of work to implement email verification is done in Node.
Log In
First, we need to add a small bit of logic to our login function to ensure all users have been verified. If a user has not been verified, return a status code of (401) Unauthorized
with the appropriate message.
isVerified
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
/** * POST /login * Sign in with email and password */ exports.loginPost = function(req, res, next) { req.assert('email', 'Email is not valid').isEmail(); req.assert('email', 'Email cannot be blank').notEmpty(); req.assert('password', 'Password cannot be blank').notEmpty(); req.sanitize('email').normalizeEmail({ remove_dots: false }); // Check for validation erro var errors = req.validationErrors(); if (errors) return res.status(400).send(errors); User.findOne({ email: req.body.email }, function(err, user) { if (!user) return res.status(401).send({ msg: 'The email address ' + req.body.email + ' is not associated with any account. Double-check your email address and try again.'}); user.comparePassword(req.body.password, function (err, isMatch) { if (!isMatch) return res.status(401).send({ msg: 'Invalid email or password' }); // Make sure the user has been verified if (!user.isVerified) return res.status(401).send({ type: 'not-verified', msg: 'Your account has not been verified.' }); // Login successful, write token, and send back user res.send({ token: generateToken(user), user: user.toJSON() }); }); }); }; |
Sign Up
When a user signs up, instead of logging them in immediately we are going to email them a confirmation token to ensure they provided a real email.
crypto
and nodemailer
, to assist in token creation and emailing.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
var crypto = require('crypto'); var nodemailer = require('nodemailer'); /** * POST /signup */ exports.signupPost = function(req, res, next) { req.assert('name', 'Name cannot be blank').notEmpty(); req.assert('email', 'Email is not valid').isEmail(); req.assert('email', 'Email cannot be blank').notEmpty(); req.assert('password', 'Password must be at least 4 characters long').len(4); req.sanitize('email').normalizeEmail({ remove_dots: false }); // Check for validation errors var errors = req.validationErrors(); if (errors) { return res.status(400).send(errors); } // Make sure this account doesn't already exist User.findOne({ email: req.body.email }, function (err, user) { // Make sure user doesn't already exist if (user) return res.status(400).send({ msg: 'The email address you have entered is already associated with another account.' }); // Create and save the user user = new User({ name: req.body.name, email: req.body.email, password: req.body.password }); user.save(function (err) { if (err) { return res.status(500).send({ msg: err.message }); } // Create a verification token for this user var token = new Token({ _userId: user._id, token: crypto.randomBytes(16).toString('hex') }); // Save the verification token token.save(function (err) { if (err) { return res.status(500).send({ msg: err.message }); } // Send the email var transporter = nodemailer.createTransport({ service: 'Sendgrid', auth: { user: process.env.SENDGRID_USERNAME, pass: process.env.SENDGRID_PASSWORD } }); var mailOptions = { from: 'no-reply@yourwebapplication.com', to: user.email, subject: 'Account Verification Token', text: 'Hello,\n\n' + 'Please verify your account by clicking the link: \nhttp:\/\/' + req.headers.host + '\/confirmation\/' + token.token + '.\n' }; transporter.sendMail(mailOptions, function (err) { if (err) { return res.status(500).send({ msg: err.message }); } res.status(200).send('A verification email has been sent to ' + user.email + '.'); }); }); }); }); }; |
Token Confirmation
You are also going to need a Node function for confirming verification tokens. It’s important to remember that based on the TTL in our model that verification token will automatically delete themselves after a set period of time.
confirmationPost
below. For brevity’s sake, this confirmation form is not included in this tutorial.If you prefer, you can have the user automatically confirm the token by clicking the link, but the action below would need to become a
Get
, instead of a Post
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
/** * POST /confirmation */ exports.confirmationPost = function (req, res, next) { req.assert('email', 'Email is not valid').isEmail(); req.assert('email', 'Email cannot be blank').notEmpty(); req.assert('token', 'Token cannot be blank').notEmpty(); req.sanitize('email').normalizeEmail({ remove_dots: false }); // Check for validation errors var errors = req.validationErrors(); if (errors) return res.status(400).send(errors); // Find a matching token Token.findOne({ token: req.body.token }, function (err, token) { if (!token) return res.status(400).send({ type: 'not-verified', msg: 'We were unable to find a valid token. Your token my have expired.' }); // If we found a token, find a matching user User.findOne({ _id: token._userId, email: req.body.email }, function (err, user) { if (!user) return res.status(400).send({ msg: 'We were unable to find a user for this token.' }); if (user.isVerified) return res.status(400).send({ type: 'already-verified', msg: 'This user has already been verified.' }); // Verify and save the user user.isVerified = true; user.save(function (err) { if (err) { return res.status(500).send({ msg: err.message }); } res.status(200).send("The account has been verified. Please log in."); }); }); }); }; |
Resending Tokens
It’s inevitable that some users will not be able to verify their account before their token expires. We’re going to need a mechanism for reissuing confirmation tokens.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
/** * POST /resend */ exports.resendTokenPost = function (req, res, next) { req.assert('email', 'Email is not valid').isEmail(); req.assert('email', 'Email cannot be blank').notEmpty(); req.sanitize('email').normalizeEmail({ remove_dots: false }); // Check for validation errors var errors = req.validationErrors(); if (errors) return res.status(400).send(errors); User.findOne({ email: req.body.email }, function (err, user) { if (!user) return res.status(400).send({ msg: 'We were unable to find a user with that email.' }); if (user.isVerified) return res.status(400).send({ msg: 'This account has already been verified. Please log in.' }); // Create a verification token, save it, and send email var token = new Token({ _userId: user._id, token: crypto.randomBytes(16).toString('hex') }); // Save the token token.save(function (err) { if (err) { return res.status(500).send({ msg: err.message }); } // Send the email var transporter = nodemailer.createTransport({ service: 'Sendgrid', auth: { user: process.env.SENDGRID_USERNAME, pass: process.env.SENDGRID_PASSWORD } }); var mailOptions = { from: 'no-reply@codemoto.io', to: user.email, subject: 'Account Verification Token', text: 'Hello,\n\n' + 'Please verify your account by clicking the link: \nhttp:\/\/' + req.headers.host + '\/confirmation\/' + token.token + '.\n' }; transporter.sendMail(mailOptions, function (err) { if (err) { return res.status(500).send({ msg: err.message }); } res.status(200).send('A verification email has been sent to ' + user.email + '.'); }); }); }); }; |
Voila! Your Node App Now Has Email Verification
Your web application is now ensuring users register with real, operational email addresses. This should help in keeping out some of the riff-raff and make sure users can regain account access when needed.
Again, this tutorial was meant to provide a high-level guide and the implementation details will most likely vary based on the specifics of your Node application.
Josh Greenberg is a developer, partner, and founder at Codemoto based in Boulder, Colorado. I’ve been developing commercial web applications for the last twenty years. With a long history in C#, ASP.NET, MVC, I’ve been mostly focused on full-stack React and .NET Core development for the past few years.
This concise article was perfect for me getting email verification stitched into my MEAN app. Thanks for taking the time to put this together.
Just what I needed. Clearly written and it works! Helped me out big time.
Nice job. You just saved me a bunch of time. Much appreciated. Cheers!
Thanks much man. This saved me alot of time and effort. Good job.
Thank you Josh! This is exaclty what I was looking for.
Thanks for sharing this Josh. It really helped me get my email verification done.
This is the best tutorial I’ve found covering email verification. Love it.
cheers Josh, your instruction helped me a lot! Peter
Thank you very much Josh! This is what I am looking for!
Hello, this little tutorial has been most helpful. So concise and right on point. Little question however, do i need to install express-validator to ensure methods like
req.assert(’email’, ‘Email is not valid’).isEmail() will work?
if I make the required in the model, is it even necessary?
thanks for the article, it’s really useful for me.
please git hub repo for this project
Thankyou so much.
It helped me alot.
Just have one question though, how can I get the email in the parameters of the get request in the confirmation route?
perfect article !! thanks a lot
Crystal clear post. You are a real teaching genius man. Thank’s…
Thank you for your precise and and well explained tutorial ?.
Cheers!
Thank you so much for this article