Building a PostgreSQL API in JavaScript with Express and Sequelize (Part 3): token-based authorization

Josh Frank
15 min readJul 29, 2021

--

Photo credit: Buena Vista Pictures

Today I’m going to pick up where I left off with my basic JavaScript web API cookbook. We’ve set up two simple models and a working relationship between them, plus routes and controllers that handle it all. Today I’ll walk through adding token-based auth with bcrypt and jsonwebtokens, and we’ll briefly discuss how middleware helps us do that. I’m still pretty sick but dammit, I’m building a functional back end! Fork and clone the repository to follow along; our progress up to now is in the branch /part-2.

Background: a token of my extreme

Auth is a big subject and it’s not to be taken lightly. Hardly a month goes by without news of a major data breach — it’s so frequent that reporting on fake breaches has become a big business. Like everything else on this blog, this post is a hopelessly, pathetically brief overview that doesn’t do its subject justice. It’s not an authoritative reference; just a guide through building something deployable that works — and just because this approach works doesn’t mean you should blindly copy it for an app that authorizes, say, bank transfers or identifying information.

Auth relies on data we can verify (authenticate): “This is definitely something we need and trust, definitely not something suspicious, and we can prove it with math.” The approach we’ll take today is called token-based authentication because it involves storing that data in a token. Long ago in a galaxy far away, the standard was session-based authentication: auth data was stored in browser session cookies. Both these approaches are forms of client-side storage: the client, not the browser, is responsible for babysitting the data, whether that’s local storage in a desktop browser, iOS’s Keychain, SharedPreferences in Android, etc.

When I was a young warthog, almost everyone accessed the web from desktop browsers — but these days most people access the web from devices like smartphones and tablets, which don’t support cookies. Cookies are also notoriously vulnerable, so token-based auth is now standard, and that saves us a lot of front-end worry; it makes auth relatively simple whatever framework we decide on, or even if we decide to switch between frameworks.

Since JSON is easy and logical to work with when we’re using Express, the obvious choice here is JSON web tokens (JWT). We’ve already installed the jsonwebtoken package, so let’s set the groundwork/scaffolding out for our Express server to dispense tokens, then write controllers/routes to authenticate them!

9. Configure auth key

Imagine a JSON web token is a parcel from the post office or UPS. A parcel has a label with a lot of information on it in one corner, and some postage on another corner. Open it up, and there’s some stuff inside. A JSON web token has three parts:

  • The header (“shipping label”): a cryptographic hashing algorithm used to scramble and unscramble our signature, to make sure it reaches its destination securely and safely
  • The signature (“postage”): the scramble that verifies the parcel was properly sent and accounted for
  • The payload: the “stuff” inside the “parcel”

Cryptographic hashing is another one of those subjects that’s too deep and broad to really cover here; it’s the stuff careers and huge fortunes are made of. The CliffNotes version: a cryptographic hash is a machine, a power tool, an appliance, used to encode and decode sensitive information. There are a few standard ones (you’re not writing these yourself); we’re using one of the most common, SHA256. This appliance needs a secret key to work — just like the key you need to start your generator or lawnmower.

Configuring that secret key is easy: in your app/config folder, create a new file called auth.config.js which will live next door to database.config.js. Enter just the few lines below:

module.exports = {
secret: process.env.AUTH_SECRET
};

NEVER EVER reference auth secrets directly!

Remember, this secret key needs to be kept, uh, secret, because it’ll start your cryptographic hashing lawnmower anywhere, and allow anyone to authenticate tokens. You obviously don’t want the secret key available on a public Git repository for anyone to see. So it needs to go in .env, our development environment:

PORT = 3001
DB_HOST = "127.0.0.1"
DB_USERNAME = "postgres"
DB_PASSWORD = "postgres"
AUTH_SECRET = "1veG0tAS3cret"

10. Add password column to User with a migration

The whole point of this Auth experiment is to authenticate user passwords, and since I haven’t yet demonstrated adding to a database table with a migration, now might be a good time to add a password column. Enter the script for generating migrations we defined earlier, yarn db:g:migration add-password-to-user, and voilá — a migration by that name appears in the migrations folder. Update the up and down functions like so:

'use strict';module.exports = {  up: async (queryInterface, Sequelize) => {
return queryInterface.addColumn( "Users", "password", Sequelize.STRING );
},
down: async (queryInterface, Sequelize) => {
return queryInterface.removeColumn( "Users", "password" );
}
};

To migrate in up, use Sequelize’s addColumn() function passing the table name, column name and Sequelize datatype as arguments. removeColumn() does the opposite to undo the migration in down; no need for a datatype there. Now enter another script you wrote, yarn db:migrate and a confirmation message should pop up in your terminal. For extra peace of mind, SELECT * WHERE... in Postgres:

Finally, don’t forget to update your model models/user.js — just add that new password field in User.init():

'use strict';const { Model } = require( 'sequelize' );
module.exports = ( sequelize, DataTypes ) => {
class User extends Model {
static associate( models ) { ... }
};
User.init( {
username: DataTypes.STRING,
email: DataTypes.STRING,
password: DataTypes.STRING
}, {
sequelize,
modelName: 'User',
} );
return User;
};

11. Create middleware

Now it’s time to give Users the ability to get tokens (i.e. sign up) and write logic to process and authenticate tokens. But before we do, you, the mighty engineer, definitely want to set rules for doing this: at the very least you don’t want users with duplicate usernames, and you may want to give different abilities to different types of users: admins, moderators, instructors/students, etc. We’ll handle these tasks, and others, with middleware.

Middleware is a broad term that has no formal definition, but it generally refers to code that helps different parts of an app/program relate to each other. Here in Express, middleware is just “stuff we need to do somewhere in our request-response cycle.” Let’s create a folder in app called middleware and three new files inside it:

├── node_modules
├── app
│ └── config
│ └── controllers
│ └── middleware
│ │ └── index.js
│ │ └── verifySignup.js
│ │ └── authorizeJwt.js
│ └── migrations
│ └── models
│ └── routes
│ └── seeders
├── .env
├── .sequelizerc
├── index.js
├── app.js
├── package.json
└── yarn.lock

For the sake of this tutorial, in verifySignUp.js, for now all we’ll do is check for duplicate usernames upon signup:

const User = require( "../models" ).User;const checkForDuplicateUsername = ( request, response, next ) => {
User.findOne( { where: { username: request.body.username } } ).then( user => {
if ( user ) {
response.status( 400 ).send( { error: "Username already taken" } );
return;
}
next();
} );
}
module.exports = { checkForDuplicateUsername };

Middleware methods have access to the user’s request, the server’s response, and whatever comes next() wherever we happen to be in the request-response cycle when the middleware is run. In this case, all we’re doing is taking a quick poke around our User database to find a User with the same username as the one in the body.username that came along with a signup request. If we find one, we can’t sign that user up, so we’ll have to send a response.status( 400 ) with an error message; otherwise we move on to whatever’s next() on our request-response checklist.

Now let’s finally write the auth middleware, where the magic happens! In authJwt.js, we’ll finally use that jsonwebtoken package:

const jwt = require( "jsonwebtoken" );
const config = require( "../config/auth.config.js" );
function verifyToken( request, response, next ) {
let token = request.headers[ "x-access-token" ];
if ( !token ) return response.status( 403 ).send( { error: "No token provided" } );
jwt.verify( token, config.secret, ( error, decoded ) => {
if ( error ) return response.status( 401 ).send( { error: "Unauthorized" } );
request.userId = decoded.id;
next();
} );
}
module.exports = {
verifyToken: verifyToken
};

HTTP headers come with lots of information stored like this; that’s why they have so many devilishly clever uses I’ve written about before on this blog. (Remember the HTTP header is not the same as the token header!) Today, we’re interested in the HTTP header called x-access-token: this one’s present in every request that comes with a token, so if ( !request.headers[ “x-access-token” ] ), we need to respond with a 403 error.

If a request comes with a valid x-access-token we’ll use jsonwebtoken.verify() to claw our way over to the next() part of the request-response cycle. verify() takes three arguments:

  • the token to verify
  • the auth key we just configured
  • a callback function with two parameters: any error that pops up, and the decoded token. Any error triggers an immediate 401 response; otherwise, we reach into the decoded token for the userId to log in, stash it in our request, and press onward.

Finally we’re going to centralize and streamline our imports in middleware/index.js:

const authorizeJwt = require( "./authorizeJwt" );
const verifySignUp = require( "./verifySignUp" );
module.exports = { authorizeJwt, verifySignUp };

There are many more amazing features you can build in your API with help from middleware. When you do, centralizing middleware exports like this will help abstract all that middleware and improve your code’s readability.

Now’s a good point to save and git add/commit/push.

12. Create auth controllers

It’s time to build controllers that use bcryptjs to create, find and authorize users. We won’t be putting this stuff in the usersControllerauth needs its own separate authController.js, with actions that respond to POST requests (more on why later; I know it seems a little weird and silly right now). Our authController will have two actions and a corresponding route for each: one to signUp a new User and one to signIn. They’ll use two methods from bcrypt: hashSync() and compareSync(), respectively:

const database = require( "../models" );
const config = require( "../config/auth.config.js" );
const User = database.User;
const jwt = require( "jsonwebtoken" );
const bcrypt = require( "bcryptjs" );
exports.signUp = ( request, response ) => {
return User.create( {
username: request.body.username,
email: request.body.email,
password: bcrypt.hashSync( request.body.password, 8 )
} )
.then( newUser => response.status( 201 ).send( newUser ) )
.catch( error => response.status( 500 ).send( error ) );
}
exports.signIn = ( request, response ) => {
const signInError = { accessToken: null, error: "Invalid username or password" };
return User.findOne( { where: { username: request.body.username } } )
.then( user => {
if ( !user ) return response.status( 401 ).send( signInError );
const validPassword = bcrypt.compareSync( request.body.password, user.password );
if ( !validPassword ) return response.status( 401 ).send( signInError );
const token = jwt.sign( { id: user.id }, config.secret, { expiresIn: 86400 } );
response.status( 200 ).send( { id: user.id, username: user.username, accessToken: token } );
} )
.catch( error => response.status( 500 ).send( error ) );
}

A new User signs up with a request that includes a desired username and password in the request.body. All we have to do to is call Sequelize’s User.create() with that information — but notice we’re NOT create()ing with password directly! Instead we’re hashing it with bcrypt.hashSync(), which takes as arguments the plaintext password and a number (the salt length). bcryptjs auto-generates some salt according to the length specified: salt is extra information used to scramble data even further and make it extra-secure. Salt length is a compromise between speed and security; salt length of 8 or 10 is a good balance for most cases.

NEVER EVER commit plaintext passwords to a database!

Just as you need to take measures protect your auth secret, you also need to adopt habits to protect user passwords — and that means never, ever making a database record of a password without hashing it. Never, never ever! Many breaches have occurred because engineers ignored this advice — engineers at firms like Facebook, Robinhood, Github, and Twitter who should really know better! Users deserve better! Make this solemn vow now and save yourself before it’s too late.

After trying to create a new User with a properly hashed password, we send the new User over in the response with a happy 201 code if it was successfully created, and catch the error and send a grumpy 500 code if User creation failed for some reason. Middleware doesn’t enter the picture quite yet.

To signIn, we obviously need to return User.findOne() instead of User.create(). First let’s check if the user doesn’t exist and respond with a signInError if it doesn’t. Then, because we’ve made sure the database always stores only properly hashed passwords, we use bcrypt.compareSync() to unscramble/decode the hashed password in the database and compare it to the plaintext password in the request; if they don’t match, respond with the same signInError.

If the user from request exists and the password from request is correct: jackpot, it’s token time! Create a token with jwt.sign(), which takes three arguments:

  • The payload to send (the stuff in the parcel),
  • Our auth key
  • Any config options — some cool stuff you can do here, like setting a token expiration date. This token { expiresIn: 86400 } seconds (24 hours).

If all goes well, wrap it up by sending a victorious 201 response with the newly-minted accessToken included. If anything went wrong along the way, we catch the error and send it along in a terrible, horrible, no-good, very bad 500 response.

Finally, don’t forget to include our authController in our exports from controllers/index.js:

const auth = require( "./authController.js" );
const users = require( "./usersController.js" );
const posts = require( "./postsController.js" );
module.exports = {
auth,
posts,
users
};

Save and git add/commit/push your progress.

13. Refactor app/routes and add auth routes

We’ve now built the authController and the middleware necessary to incorporate auth fully into our app! The missing pieces are routes that work with our authController and usersController to give our app functionality. When we left off last time, our app/routes/index.js file looked like this:

const postsController = require( "../controllers" ).posts;
const usersController = require( "../controllers" ).users;
module.exports = app => {
// Post routes //
app.get( "/posts/:postId", postsController.show );
// User routes //
app.get( "/users/:userId", usersController.show );
};

Our app will hopefully grow, perhaps a great deal, and when it does this index.js file will get really big and cumbersome. Let’s streamline a bit and split our routes by model, so that our file structure looks a bit like this:

├── node_modules
├── app
│ └── config
│ └── controllers
│ └── middleware
│ └── migrations
│ └── models
│ └── routes
│ │ └── authRoutes.js
│ │ └── index.js
│ │ └── postRoutes.js
│ │ └── userRoutes.js
│ └── seeders
├── .env
├── .sequelizerc
├── index.js
├── app.js
├── package.json
└── yarn.lock

The three new routes files won’t look very different from the index.js file we’re refactoring: each will just export one function with a single parameter app. Let’s start with our authRoutes because they look a bit different from the others. See below I’ve highlit the two ways we can use middleware in Express:

const { verifySignUp } = require( "../middleware" );
const authController = require( "../controllers" ).auth;
module.exports = app => {
// Headers //
app.use( ( request, response, next ) => {
response.header(
"Access-Control-Allow-Headers",
"x-access-token, Origin, Content-Type, Accept"
);
next();
} );

// Routes //
app.post( "/signup", [ verifySignUp.checkForDuplicateUsername ], authController.signUp );
app.post( "/signin", authController.signIn );
};
  • We can call middleware methods directly with a new method, app.use(). This middleware function sets a response so it accepts token headers. It’s too short to merit its own separate file in middleware/ so we’ll just stick it here.
  • We can also just pass an array of middleware functions as the second argument in post(). Super easy! It shouldn’t surprise you by now to see Express has methods for HTTP verbs like app.post(), app.patch(), and so on.
A rocker switch is idempotent: pressing “on” when the switch is already on… doesn’t do anything

Why do auth routes respond to POST requests?

It seems strange that auth routes are POST requests; after all, we’re not adding any record to the database. The reason? Auth signup/signin requests are non-idempotent, and POST is intended for non-idempotent requests.

A request is idempotent if it has the same effect no matter how many times it’s made. One PUT request to change { username: “josh” } to { username: “j0sh” } has the same effect as three PUT requests to do it; the User will end up with a { username: “j0sh” } in both cases.

This article does a good job explaining idempotence.

postRoutes and userRoutes each look very similar, just a single function with app as an argument and just listens for routes with controller actions as in index.js before:

// postRoutes.jsconst postsController = require( "../controllers" ).posts;module.exports = app => {
app.get( "/posts/:postId", postsController.show );
};
// userRoutes.jsconst usersController = require( "../controllers" ).users;module.exports = app => {
app.get( "/users/:userId", usersController.show );
};

Finally we can reduce index.js to this!

module.exports = app => {
require( "../routes/authRoutes" )( app );
require( "../routes/postRoutes" )( app );
require( "../routes/userRoutes" )( app );
};

14. Adding auth to other routes

curl can’t make POST requests so we’ll need a REST client to test our authRoutes. REST clients help us test a back end by making requests to it that would normally come from a front end (which we haven’t built yet). The most popular seems to be Postman; I prefer ARC on a Mac because it’s open-source.

Below is the view after making a POST request to localhost:3000/signup in ARC. It’s equivalent to a back end calling: fetch( "localhost:3000/signup", { method: “POST”, headers: { “Content-Type”: “application/json” }, body: JSON.stringify( { "username": "josh", "password": "1234" } ) } )

The REST client displays the JSON that results from our POST request, and we can see what’s clearly a hashed password and not “1234”. Copy that password and paste it into a POST request to localhost:3000/signin. That’s equivalent to a back end calling: fetch( "localhost:3000/signin", { method: “POST”, headers: { “Content-Type”: “application/json”, "x-access-token": "<hashed password>" }, body: JSON.stringify( { "username": "josh", "password": "1234" } ) } )

The JSON that results from this POST request includes an access token. Your client will hold onto this token, and your app can use it to verify that user “josh” is signed in before performing a controller action!

Best of all, Express makes it so easy to use auth middleware! Let’s see just how easy by creating a PATCH method to “/posts”:

// postsController.jsexports.create = async ( request, response ) => {
return await Post.create( {
title: request.body.title,
content: request.body.content,
userId: request.userId
}, {} )
.then( newPost => Post.findByPk( newPost.id, {} )
.then( newPost => response.status( 201 ).send( newPost ) )
.catch( error => response.status( 400 ).send( error ) )
);
}

Nothing out of a medieval tome, but still lots to note:

  • Our create function is an async function, so we must return await and not just return!
  • We create a post with a title and some content— but also request.userId, which only comes from our middleware!
  • Sequelize also gives us a second options hash after the object we’re creating — I’ve left it empty here but it’s great for serialization.
  • We do some chaining with .then() because we need to make sure things happen in the right order: first create the Post, then send it with an appropriate 201 code and catch any errors along the way.

If all goes well, all we need to do now is just add some middleware in an array to app.post() and…

// postRoutes.jsconst { authorizeJwt } = require( "../middleware" );
const postsController = require( "../controllers" ).posts;
module.exports = app => {
app.get( "/posts/:postId", postsController.show );
app.post( "/posts", [ authorizeJwt.verifyToken ], postsController.create );
};

…we should now have a POST route to /posts which only works with an x-access-token header:

Here’s what happens when we have.a valid token…
…and when we don’t have a valid token

Next round

This back end now has some basic token-based auth! This is all awesome, but it isn’t really a back end until we deploy it. So I’d like to finish this tutorial with one more article covering deployment with Heroku; it’s definitely smooth sailing from here considering what we’ve been through already, but it can be frustrating without keeping an eye out for a few common problems.

Until then, happy coding, and remember: handle secrets and passwords responsibly! Never ever expose them on a database or repository!

--

--

Josh Frank
Josh Frank

Written by Josh Frank

Oh geez, Josh Frank decided to go to Flatiron? He must be insane…

No responses yet