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.
Even though you might need to use a database in a real-world application, since we need to keep this article simple we won't be using any databases or email validation methods, such as sending an email with a validation code.
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, we'll be using a server-side rendering engine called Handlebars. This engine will render our HTML pages on the server side, due to which, we won't be needing any other front-end framework such as Angular or React.
Let's go ahead and install express-handlebars
:
$ npm install --save express-handlebars
We'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 we're going to build 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');
We will be using 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 hadlers 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');
});
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
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: '[email protected]',
// 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 - we are using this map 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.
Conclusion
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.