Node/Express App

September 10, 2020

Authentication in Express.js - simple app

Introduction

In this article, we are going to make a simple app to demonstrate how you can handle authentication in Express.js. Since we will be using some basic ES6 syntaxes and the Bootstrap framework for UI design, it might help if you have some basic knowledge about those technologies.

Project Setup

First, let's create a new folder called, say, simple-web-app. Using the terminal, we'll navigate to that folder and create a skeleton Node.js project:

$ npm init

Now, we can install Express as well:

$ npm install --save express

To keep things simple, a server-side rendering engine called Handlebars will be used. This engine will render the HTML pages on the server side, due to which, it won't be needing any other fron-tend framework such as Angular or React.

Let's go ahead and install express-handlebars:

\$ npm install --save express-handlebars

I'll also be using two other Express middleware packages (body-parser and cookie-parser) to parse HTTP request bodies and parse the required cookies for authentication:

$ npm install --save body-parser cookie-parser

Implementation

The application will contain a "protected" page that only logged in users can visit, otherwise, they'll be redirected to the home page - prompting them to either log in or register.

To get started, let's import the libraries we've previously installed:

const express = require('express')
const exphbs = require('express-handlebars')
const cookieParser = require('cookie-parser')
const bodyParser = require('body-parser')

I will be using the Node's native crypto module for password hashing and to generate an authentication token - this will be elaborated a bit later in the article.

Next, let's create a simple Express app and configure the middleware we've imported, alongside the Handlebars engine:

const app = express()

// To support URL-encoded bodies
app.use(bodyParser.urlencoded({ extended: true }))

// To parse cookies from the HTTP Request
app.use(cookieParser())

app.engine(
  'hbs',
  exphbs({
    extname: '.hbs',
  })
)

app.set('view engine', 'hbs')

// Our requests handlers will be implemented here...

app.listen(3000)

👉 By default in Handlebars, the template extension should be .handlebars. As you can see in this code we have configured our handlebars template engine to support files with the .hbs shorter extension. Now let's create a few template files:

The layouts folder inside the view folder will hold your main layout, which will provide the base HTML for other templates.

Let's create the main.hbs, our main wrapper page:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>

    <link
      rel="stylesheet"
      href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
    />
  </head>
  <body>
    <div class="container">
      {{{body}}}
    </div>

    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
  </body>
</html>

Other templates will render inside the {{{body}}} tag of this template. We have the HTML boilerplate and the required CSS and JS files for Bootstrap imported in this layout.

With our main wrapper done, let's create the home.hbs page, where users will be prompted to log in or register:

<nav class="navbar navbar-expand-lg navbar-light bg-light">
    <a class="navbar-brand" href="#">Simple Authentication App</a>
</nav>

<div style="margin-top: 30px">
    <a class="btn btn-primary btn-lg active" href="/login">Login</a>
    <a class="btn btn-primary btn-lg active" href="/register">Register</a>
</div>

Then let's create a request handler to the path root path (/) to render the home template.

app.get('/', function (req, res) {
  res.render('home')
})

Let's start our app up and navigate to http://localhost:3000:

Account Registration

The information about an account is collected through a registration.hbs page:

<div class="row justify-content-md-center" style="margin-top: 30px">
  <div class="col-md-4">
    {{#if message}}
    <div class="alert {{messageClass}}" role="alert">
      {{message}}
    </div>
    {{/if}}

    <form method="POST" action="/register">
      <div class="form-group">
        <label for="firstNameInput">First Name</label>
        <input
          name="firstName"
          type="text"
          class="form-control"
          id="firstNameInput"
        />
      </div>

      <div class="form-group">
        <label for="lastNameInput">Last Name</label>
        <input
          name="firstName"
          type="text"
          class="form-control"
          id="lastNameInput"
        />
      </div>
      <div class="form-group">
        <label for="emailInput">Email address</label>
        <input
          name="email"
          type="email"
          class="form-control"
          id="emailInput"
          placeholder="Enter email"
        />
      </div>

      <div class="form-group">
        <label for="passwordInput">Password</label>
        <input
          name="password"
          type="password"
          class="form-control"
          id="passwordInput"
          placeholder="Password"
        />
      </div>

      <div class="form-group">
        <label for="confirmPasswordInput">Confirm Password</label>
        <input
          name="confirmPassword"
          type="password"
          class="form-control"
          id="confirmPasswordInput"
          placeholder="Re-enter your password here"
        />
      </div>

      <button type="submit" class="btn btn-primary">Login</button>
    </form>
  </div>
</div>

In this template, we have created a form with registration fields of the user which is the First Name, Last Name, Email Address, Password and Confirm Password and set our action as the /register route. Also, we have a message field in which we will display error and success messages for an example if passwords do not match, etc.

Let's create a request handle to render the registration template when the user visit http://localhost:3000/register:

app.get('/register', (req, res) => {
  res.render('register')
})

Due to security concerns, it is a good practice to hash the password with a strong hashing algorithm like SHA256. By hashing passwords, we make sure that even if our password database might be compromised, the passwords aren't simply sitting there in plain sight in text format.

👉 An even better method than just simple hashing is to use salt, like with the bcrypt algorithm. For more information on securing authentication, check out Implementing User Authentication the Right Way. In this article, however, we'll keep things a bit simpler.

const crypto = require('crypto')

const getHashedPassword = (password) => {
  const sha256 = crypto.createHash('sha256')
  const hash = sha256.update(password).digest('base64')
  return hash
}

When the user submits the registration form, a POST request will be sent to the /register path.

That being said, we now need to handle that request with the information from the form and persist our newly created user. Typically, this is done by persisting the user in a database, but for the sake of simplicity, we'll store users in a JavaScript array.

Since each server restart will reinitialize the array, we'll hardcode a user for testing purposes to be initialized every time:

const users = [
  // This user is added to the array to avoid creating a new user on each restart
  {
    firstName: 'John',
    lastName: 'Doe',
    email: 'johndoe@email.com',
    // This is the SHA256 hash for value of `password`
    password: 'XohImNooBHFR0OVvjcYpJ3NgPQ1qq73WKhHvch0VQtg=',
  },
]

app.post('/register', (req, res) => {
  const { email, firstName, lastName, password, confirmPassword } = req.body

  // Check if the password and confirm password fields match
  if (password === confirmPassword) {
    // Check if user with the same email is also registered
    if (users.find((user) => user.email === email)) {
      res.render('register', {
        message: 'User already registered.',
        messageClass: 'alert-danger',
      })

      return
    }

    const hashedPassword = getHashedPassword(password)

    // Store user into the database if you are using one
    users.push({
      firstName,
      lastName,
      email,
      password: hashedPassword,
    })

    res.render('login', {
      message: 'Registration Complete. Please login to continue.',
      messageClass: 'alert-success',
    })
  } else {
    res.render('register', {
      message: 'Password does not match.',
      messageClass: 'alert-danger',
    })
  }
})

The received email, firstName, lastName, password, and confirmPassword are validated - passwords match, email isn't already registered, etc.

If each validation is successful we hash the password and store information inside the array and redirect the user to the login page. Otherwise, we will re-render the registration page with the error message.

Now, let's visit the /register endpoint to validate that it's working correctly:

Account Login

With registration out of the way, we can implement the login functionality. Let's start by making the login.hbs page:

<div class="row justify-content-md-center" style="margin-top: 100px">
  <div class="col-md-6">
    {{#if message}}
    <div class="alert {{messageClass}}" role="alert">
      {{message}}
    </div>
    {{/if}}
    <form method="POST" action="/login">
      <div class="form-group">
        <label for="exampleInputEmail1">Email address</label>
        <input
          name="email"
          type="email"
          class="form-control"
          id="exampleInputEmail1"
          placeholder="Enter email"
        />
      </div>
      <div class="form-group">
        <label for="exampleInputPassword1">Password</label>
        <input
          name="password"
          type="password"
          class="form-control"
          id="exampleInputPassword1"
          placeholder="Password"
        />
      </div>
      <button type="submit" class="btn btn-primary">Login</button>
    </form>
  </div>
</div>

And then, let's create a handler for that request as well:

app.get('/login', (req, res) => {
  res.render('login')
})

This form will send a POST request to the /login when the user submits the form. Though, another thing we'll be doing is sending an authentication token for the login. This token will be used to identify the user and each time they send an HTTP request, this token will be sent as a cookie:

const generateAuthToken = () => {
  return crypto.randomBytes(30).toString('hex')
}

With our helper method, we can create a request handler for the login page:

// This will hold the users and authToken related to users
const authTokens = {}

app.post('/login', (req, res) => {
  const { email, password } = req.body
  const hashedPassword = getHashedPassword(password)

  const user = users.find((u) => {
    return u.email === email && hashedPassword === u.password
  })

  if (user) {
    const authToken = generateAuthToken()

    // Store authentication token
    authTokens[authToken] = user

    // Setting the auth token in cookies
    res.cookie('AuthToken', authToken)

    // Redirect user to the protected page

    res.redirect('/protected')
  } else {
    res.render('login', {
      message: 'Invalid username or password',
      messageClass: 'alert-danger',
    })
  }
})

👉 In this request handler, a map called authTokens is used to store authentication tokens as the key and the corresponding user as the value, which allows a simple token to user lookup. You can use a database like Redis, or really, any database to store these tokens - this map is used for simplicity.

Hitting the /login endpoint, we'll be greeted with:

We're not quite done yet though. We'll need to inject the user to the request by reading the authToken from the cookies upon receiving the login request. Above all the request handlers and below the cookie-parser middleware, let's create our own custom middleware for injecting users to the requests:

app.use((req, res, next) => {
  // Get auth token from the cookies
  const authToken = req.cookies['AuthToken']

  // Inject the user to the request
  req.user = authTokens[authToken]

  next()
})

Now we can use req.user inside our request handlers to check if the user is authenticated via a token.

Finally, let's create a request handler to render the protected page - protected.hbs:

<nav class="navbar navbar-expand-lg navbar-light bg-light">
  <a class="navbar-brand" href="#">Protected Page</a>
</nav>

<div>
  <h2>This page is only visible to logged in users</h2>
</div>

And a request handler for the page:

app.get('/protected', (req, res) => {
  if (req.user) {
    res.render('protected')
  } else {
    res.render('login', {
      message: 'Please login to continue',
      messageClass: 'alert-danger',
    })
  }
})

As you can see, you can use req.user to check if the user is authenticated. If that object is empty, the user is not authenticated.

Another way to require authentication on routes is to implement it as middleware, which can then be applied to routes directly as they're defined with the app object:

const requireAuth = (req, res, next) => {
  if (req.user) {
    next()
  } else {
    res.render('login', {
      message: 'Please login to continue',
      messageClass: 'alert-danger',
    })
  }
}

app.get('/protected', requireAuth, (req, res) => {
  res.render('protected')
})

Authorization strategies can also be implemented in this way by assigning roles to users and then checking for the correct permissions before the user accesses the page.

SumUp User authentication in Express is pretty simple and straightforward. We've used Node's native crypto module to hash passwords of registered users as a basic safety feature, and created a protected page, visible only to users authenticated with a token.

The source code for this project can be found on GitHub.

Happy Coding!

Up next