Verify Users in ExpressJS

Verify Users in ExpressJS

If you’re building an application, you likely want a lot of users on your platform. However, you don’t just want a large number of users, you want real and high-quality users who will interact with your app. You want to verify those users.

It’s common for bots or users with fake email addresses and no intention of seriously using your application to register. One way to deal with this at the start is by making sure to verify users.

This article is a tutorial on user verification in ExpressJS and a continuation of my Express web development series. I will be building on top of the concepts discussed in my previous article on handling password resets.

The setup and required packages are specified in that article but you will be able to see what packages are used in the code examples.

I’d suggest taking a look at the other articles in the series first, although you should be able to follow along with this one regardless. Check out the project on GitHub if you’d like to track it as the series progresses.

Models

Let’s first create the model that holds the verification tokens. Navigate to the models folder and create a file called ‘UserVerification.js’. The file should have the following content:

const { Schema, model } = require('mongoose')

const schema = new Schema({
  user : {
    type: Schema.Types.ObjectId,
    ref: 'User',
    required: true
  },
  token: {
    type: Schema.Types.String,
    required: true
  }
}, {
  timestamps: true
})

schema.index({ 'updatedAt': 1 }, { expireAfterSeconds: 300 })

const UserVerification = model('UserVerification', schema)

module.exports = UserVerification

The model schema contains a token that will be included in the verification link, and the user it’s associated with.

Create an index on the ‘updatedAt’ field that instructs MongoDB to delete the record after 5 minutes from the time the record is updated. 5 minutes is reasonable for testing but you’ll want to increase this to something more reasonable in production.

In the user model, add a boolean ‘verified’ property to the schema. Set the default to false as the user would not be verified upon registration.

const { Schema, model } = require('mongoose')

const saltRounds = 10

var userSchema = new Schema({
  name: {
    type: Schema.Types.String,
    required: [true, 'You must provide a name']
  },
  email: {
    type: Schema.Types.String,
    required: [true, 'Email address is required']
  },
  username: {
    type: Schema.Types.String,
    required: [true, 'Username is required']
  },
  password: {
    type: Schema.Types.String,
    required: [true, 'You must provide a password']
  },
  verified: {
    type: Schema.Types.Boolean,
    required: true,
    default: false
  }
})

...

const User = model('User', userSchema)

module.exports = User

Routes

Profile routes

The first route we have to create is the profile route. This route will simply render a template with the user’s profile details. Create a file in the routes folder named ‘profile.js’ and add a route that renders the ‘profile.html’ template.

const router = require('express').Router()

router.get('/profile', (req, res) => {
  if (!req.isAuthenticated()) return res.redirect('/login')
  return res.render('profile.html')
})

module.exports = router

User verification routes

Now let’s create the routes that will handle user verification. In the routes folder, create a file named ‘user-verification.js’. To start with, the file will have the following contents:

const router = require('express').Router()
const { v4 } = require('uuid')
const { User, UserVerification } = require('../models')
const { sendEmail } = require('../helpers')

/* Create routes here */

module.exports = router

Import the User and UserVerification models. Import the ‘sendMail’ helper function that we created in the previous article. This is simply a function that uses NodeMailer to send an email using the arguments passed to it.

Now let’s create the routes.

Create verification url

The first route is a get route ‘/verify’. This route is responsible for creating the verification URL and has the following contents:

router.get('/verify', async (req, res) => {
  if (!req.isAuthenticated()) return res.redirect('/login')
  if (req.user.verified) return res.redirect('back')

  const token = v4().toString().replace(/-/g, '')
  const verificationUrl = `${process.env.DOMAIN}/verify-confirm/${token}`

  await UserVerification.updateOne({ 
    user: req.user._id 
  }, {
    user: req.user._id,
    token: token
  }, {
    upsert: true
  })

  sendEmail({
    to: req.user.email,
    subject: 'Verify your email address',
    text: `Here's your email verification link: ${verificationUrl}`
  })

  req.flash('verify_success', 'Check your email address for your verification link. It may take a few minutes')
  res.redirect('/profile')
})

First, check if the user is authenticated. The user should only be able to request a verification link when they are logged in. If they aren’t, redirect them to the login page.

Check if the user is already verified. We don’t want to send a verification link if the user is already verified. If they are, redirect to the previous page.

Create the token and then the verification URL that contains the token.

Update the UserVerification record associated with the current user. Make sure to set the upsert option to ‘true’. We want to replace the current verification link so that only one can be active at a time, but we also want to create a new one if there isn’t one in the collection.

Send the email containing the user verification link, flash a success message urging the user to check their email address and then redirect to the user’s profile.

Verify user

The second route handles the link sent to the user:

router.get('/verify-confirm/:token', async (req, res) => {
  if (!req.isAuthenticated()) return res.redirect('/login')

  const token = req.params.token

  const userVerification = await UserVerification.findOne({
    user: req.user._id,
    token: token
  })

  if (userVerification) {
    await User.updateOne({ _id: req.user._id }, { verified: true })
    await UserVerification.deleteOne({ 
      user: req.user._id,
      token: token
    })
    sendEmail({
      to: req.user.email,
      subject: 'Verified',
      text: `Congratulations ${req.user.name}, your account is now verified!`
    })
    req.flash('verify_success', 'Congrats, you are now verified!')
  } else {
    req.flash('verify_error', 'Verification link is invalid or has expired.')
  }

  return res.redirect('/profile')
})

This route expects a token which we will validate later. First check whether the user is logged in, if not, redirect to the login page.

Extract the token from the url and query the UserVerification collection for a document with the current token and the current user.

If the document doesn’t exist, flash an error message stating that the link is invalid or expired.

If the document exists, update the user’s verified status to ‘true’ and delete the current UserVerification document in order to prevent the link from being clicked again (this would be pointless anyway but it’s good practice).

Send the user an email confirming their verification status and then flash a success message stating that the user has now been verified. Redirect to the user’s profile page afterwards.

Import routes

Go into the app’s entry folder and include the profile and user-verification routes with the following code:

app.use('/', require('./routes/profile'))
app.use('/', require('./routes/user-verification'))

Templates

There’s one new template that we need to create for this feature: the profile template.

{% extends 'base.html' %}

{% set title = 'Profile' %}

{% block content %}
  {% if messages.verify_success %}
    <div class="alert alert-success" role="alert">
      {{ messages.verify_success }}
    </div>
  {% endif %}
  {% if messages.verify_error %}
    <div class="alert alert-danger" role="alert">
      {{ messages.verify_error }}
    </div>
  {% endif %}
  <div>
    <h5>Hi, {{ user.name }}</h5>
    {% if not user.verified %}
      Your email is not verified, 
      <a class="btn btn-sm btn-warning" href="/verify">Verify Email</a>
    {% endif %}
  </div>
{% endblock %}

{% block scripts %}
{% endblock %}

This template renders the error or success message flashed in the previous request. We have a div that displays the user’s name and a button to generate the verification URL conditionally based on the user’s verified status.

Conclusion

In this article, I demonstrated how to verify users in your Express application. There are many reasons you may want to verify users: You may want to make sure you have active, human users on your app or maybe you want to restrict features that require users to be verified.

Whatever the reason, I hope that this article has provided sufficient guidance on the flow and execution of the verification process.

The next article will be about creating user following and follower relationships using many-to-many relationships in MongoDB.