Bootcamp
Search…
3.5.5: Custom Middleware for Authentication

Introduction

Currently we construct auth logic separately for every route where we authenticate users. We'll refactor our app to authenticate users automatically on every request. We'll also be able to get the logged-in user from the database on every request.

Express Middleware

Express.js middleware is a function or set of functions that can be configured to run before or after a request. We will use middleware to refactor our user authentication code to run before our routes. Read more on middleware here.

app.use

Like app.get, app.use sets a callback that will run when a request arrives. Unlike app.get, the app.use callback runs on every request, regardless of the URL path. Add the following code to your app before your routes:
app.use((request, response, next) => {
console.log('Every request:', request.path);
next();
});
This code runs for requests to routes you have and requests that have no route. Try the path /something-that-doesnt-exist.

next

next is a function that invokes the next middleware bound to app. next must be called if the current middleware does not end the request-response cycle, otherwise the request will hang. More on how to use next here.
app.use((request, response, next) => {
if (request.path == '/some-path') {
response.status(404).send('sorry');
return;
}
next();
});

Cookie-Checking Middleware Implementation

Let's implement refactored cookie-checking functionality that will authenticate incoming cookies on every request.

Refactor Hash Logic into Helper Function

First we'll put the hashing code into its own function.
// remember to include the lib at the top of the file
import jsSha from 'jssha';
​
// initialize salt as a global constant
const SALT = process.env.SALT;
​
// ...
​
const getHash = (input) => {
// create new SHA object
const shaObj = new jsSha('SHA-512', 'TEXT', { encoding: 'UTF8' });
​
// create an unhashed cookie string based on user ID and salt
const unhashedString = `${input}-${SALT}`;
​
// generate a hashed cookie string using SHA object
shaObj.update(unhashedString);
​
return shaObj.getHash('HEX');
};

Write Custom Middleware to Verify Hash and Set Request Flag

The way we'll let the route know whether or not the user has logged in or not is to set a new key in the request object called isUserLoggedIn.
app.use((request, response, next) => {
// set the default value
request.isUserLoggedIn = false;
​
// check to see if the cookies you need exists
if (request.cookies.loggedIn && request.cookies.userId) {
// get the hased value that should be inside the cookie
const hash = getHash(request.cookies.userId);
​
// test the value of the cookie
if (request.cookies.loggedIn === hash) {
request.isUserLoggedIn = true;
}
}
​
next();
});

Customise Route Logic Based on User Auth

When writing middleware we can decide to set any key or set of keys into the request object, get a hold of it in the route, and write logic based on that key.
For user auth, we'll look for the isUserLoggedIn key in the route and customise logic based the boolean value.
user has many recipes
In this example we want to render the form to create a new recipe. A user is associated with a recipe by their user ID, thus a user must be logged in before they can complete the form. When an unauthenticated user visits a page that requires auth, common behaviour is to either render a 403 page or redirect the user to a public page.
app.get('/recipes', (request, response) => {
if (request.isUserLoggedIn === false) {
response.status(403).send('sorry');
return;
}
// ...
});

Run Middleware on Selected Routes

We may not want to run our middleware on every request. If we only want to run our middleware on selected routes, we can "chain" our middleware with an existing route's middleware as per the following. The checkAuth function in this example is the callback in app.use above. checkAuth runs for the given route before the anonymous middleware declared after. More on middleware chaining here.
const checkAuth = (request, response, next) => {
// logic that calls next() or not
// ...
};
​
// ...
​
app.get('/recipes', checkAuth, (request, response) => {
if (request.isUserLoggedIn === false) {
response.status(403).send('sorry');
return;
}
// ...
});

Supplying Logged-In User Data to Routes

In certain apps we may want to know logged-in user information on every request. We can query the user table in our middleware to provide logged-in user data to every route. We can also consider putting this data into a cookie.

Modify Auth Middleware to Query for Logged-In User Data

After we've verified the hashed cookie we can query the database based on the user ID in the cookie. When we retrieve the logged-in user's data we can store it in the request like we did above, enabling access to the logged-in user's data in the successive middleware.
Consider the order in which the next functions are called in the below example. Note that only 1 next is ever called. In Express, it's possible for 2 nexts to be called, but this will invoke the successive middleware twice, possibly causing an error when the code tries to end the request-response cycle for the 2nd time for a given request.
On line 34 we put the whole user record into the request. Best practice is to remove the user's password (or any other very sensitive data) before sharing the user record with successive middleware.
import pg from 'pg';
const { Pool } = pg;
​
// ...
​
const pool = new Pool(pgConnectionConfigs);
​
// ...
​
app.use((request, response, next) => {
// set the default value
request.isUserLoggedIn = false;
​
// check to see if the cookies you need exists
if (request.cookies.loggedIn && request.cookie.userId) {
// get the hased value that should be inside the cookie
const hash = getHash(request.cookies.userId);
​
// test the value of the cookie
if (request.cookies.loggedIn === hash) {
request.isUserLoggedIn = true;
​
// look for this user in the database
const values = [request.cookies.userId];
​
// try to get the user
pool.query('SELECT * FROM users WHERE id=$1', values, (error, result) => {
if (error || result.rows.length < 1) {
response.status(503).send('sorry!');
return;
}
​
// set the user as a key in the request object so that it's accessible in the route
request.user = result.rows[0];
​
next();
});
​
// make sure we don't get down to the next() below
return;
}
}
​
next();
});
See the example user login app for more middle ware examples: https://github.com/rocketacademy/userauth-express-bootcamp​