Basic HTTP auth in Express and Node.js

When you're looking for a quick and dirty way to add password protection to a website, basic HTTP auth is handy. This tutorial will go through implementing basic HTTP auth for a static website using Express.js in Node.js.

What is basic HTTP auth?

Basic HTTP auth is simple username and password login protection for a website. It is not recommended to use this in a real, production-level project as it is not very elegant, but it works for the simple task of password protecting a website.

Basic HTTP auth prompt

I implemented basic HTTP auth in this resumé website project.

Setting up our Express app

This is some basic code for setting up an Express app.

// ./server/index.js 
const express = require('express'); 
const Path = require('path'); 
const app = express(); 
const port = process.env.PORT || 8000; 

/**
 * Routes
 */
app.use('/', express.static(Path.join(__dirname, '../static'))); 
app.listen(port, () => {   console.log(`Listening on port ${port}`); });

Essentially what this code does is create an Express server that listens on port 8000 and serves a directory located in the project root in ./static. This will be the site we password protect.

Adding basic HTTP auth in Express

To add basic HTTP auth in Express, we are going to leverage a handy plugin that the Express team recommends. It's called basic-auth. There are similar packages out there (including ones with the name express) but this is the one that works and that the Express team recommends. We will also be using the tsscmp library, which stands for timing-safe string comparison. This will allow us to perform time-safe equality checks on our strings. Further reading on implementing time-safe string comparisons can be found in the blog post or by reading this library's source code.

Now let's add these to our project with Yarn:

yarn add tsscmp basic-auth

Or regular NPM:

npm install tsscmp basic-auth --save

Let's create a new file to modularize our auth middleware. We could just put it all in the same file, but to keep it neat, let's follow the recommended approach to developing Express.js middleware and get started:

// ./server/middleware/auth.js 
const auth = require('basic-auth'); 
const compare = require('tsscmp'); 
const basicAuth = (request, response, next) => { }; 

module.exports = basicAuth;

All Express.js middleware follows the same pattern where it's a function that executes with the provided 3 arguments:

  • request: Express Request object
  • response: Express Response object
  • next: function to continue

Middleware can either stop everything by executing a response, or it can allow the application to continue by calling next() which can allow the next middleware (if applicable) or final route handler to run.

// ./server/middleware/auth.js 
const auth = require('basic-auth'); 
const compare = require('tsscmp'); 

const basicAuth = (request, response, next) => {   
  const credentials = auth(request);   
  if (credentials && check(credentials.name, credentials.pass)) {     
    return next();   
  }   

  response.set('WWW-Authenticate', 'Basic realm="my website"');   
  return response.status(401).send("🙅🏻 nope"); 
}; 

module.exports = basicAuth;

This is our basic middleware, where we ask for credentials from the request, which will prompt the user to enter a username and password, then we check those credentials safely using our check() function (that we haven't yet implemented). Upon successful check, we call next() to allow the next middleware (or route handler) to run.

If our check fails, we respond with a 401 Unauthorized.

This is what the check() function looks like:

const compare = require('tsscmp'); 
const check = (name, pass) => {   
  let valid = true;   // Simple method to prevent short-circuit and use timing-safe compare   
  valid = compare(name, '[my_username]') && valid;   
  valid = compare(pass, '[my_password]') && valid;   
  return valid; 
};

It implements tsscmp to safely compare strings.

All together it looks like this:

// ./server/middleware/auth.js 
const auth = require('basic-auth'); 
const compare = require('tsscmp'); 

const check = (name, pass) => {   
  let valid = true;   // Simple method to prevent short-circuit and use timing-safe compare   
  valid = compare(name, '[my_username]') && valid;   
  valid = compare(pass, '[my_password]') && valid;   

  return valid; 
}; 

const basicAuth = (request, response, next) => {   
  const credentials = auth(request);
  if (credentials && check(credentials.name, credentials.pass)) {     
    return next();   
  }   

  response.set('WWW-Authenticate', 'Basic realm="my website"');   
  return response.status(401).send("🙅🏻 nope"); 
}; 

module.exports = basicAuth;

Then we import it and implement it in our main server file:

const basicAuth = require('./middleware/auth'); 
app.use(basicAuth);

The final code should look like this:

// ./server/index.js 
const express = require('express'); 
const Path = require('path'); 
const basicAuth = require('./middleware/auth'); 

const app = express(); 
const port = process.env.PORT || 8000; 

/**
 * Routes
 */
app.use(basicAuth); 
app.use('/', express.static(Path.join(__dirname, '../static'))); 

app.listen(port, () => {   
  console.log(`Listening on port ${port}`); 
});

Now when we run node server/index.js we should have a password-protected server running on port 8000.