Building a PostgreSQL API in JavaScript with Express and Sequelize (Part 3): token-based authorization
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 User
s 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 thedecoded
token. Any error triggers an immediate401
response
; otherwise, we reach into the decoded token for theuserId
to log in, stash it in ourrequest
, 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 usersController
— auth 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 aresponse
so it accepts token headers. It’s too short to merit its own separate file inmiddleware/
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 likeapp.post()
,app.patch()
, and so on.
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, andPOST
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 threePUT
requests to do it; the User will end up with a{ username: “j0sh” }
in both cases.
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 anasync
function, so we mustreturn await
and not justreturn
! - We create a post with a
title
and somecontent
— but alsorequest.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 thePost
, then send it with an appropriate201
code andcatch
anyerror
s 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:
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!