Using JWT (JSON Web Tokens) to authorize users and protect API routes

Actual JWT tokens, happy to be at your service.

So your backend has a few API routes that need protectin’ and some user’s that need authorizin’. Much like myself at one point, you’re probably wondering how this can be achieved. Thankfully, we have JSON Web Tokens (JWT) (among other things) for that.

What exactly is JWT? No better way to explain it than direct from the JWT website:

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

Arguably one of the largest use cases for JWT is authorization. We can generate a JWT token in the backend that is specific to a user, pass this JWT token to the frontend, and then our frontend can send this token alongside requests to access protected API routes.

JWT tokens can be given an expiration time. They can also be generated with no expiration, however I believe it’s best practice to make sure your tokens have an expiration and renew at certain intervals. This will mitigate the threat of one single token being stolen and used to access routes over-and-over again.

Let’s touch on the security of JWT tokens for a few more moments. When a JWT token is generated, there is a secret that is used to generate the token. Only the server should know this secret. If someone were to modify the data contained in the JWT, the server would fail to decode it. This means the server can trust any JWT that it can decode and verify. A hacker could also intercept network traffic between server and client to get the JWT token (much like they would with cookies). This can be prevented by always sending the token back and forth over HTTPS. It is mandatory that HTTPS should be used with JWT.

I would definitely recommend reading more in-depth in regards to the security of JWT tokens. This is very important, especially if your application contains sensitive data.

Now, time for some code.

You can find all the code found in this article on Github here. Clone it locally => npm install dependencies => enjoy!

Also note, that if you want to follow along completely, I will be using POSTtman to access the API routes. No reason other than it’s just what I know. You can find it here completely free: POSTman.

File Structure

Here is the basic file structure of this example:

Pretty straightforward. We have a very simple ‘dummy user’ set up in server/models/dummyUser.js that we will use to mock a user in a database, allowing us to ‘log in’ and generate a JWT token. More on that in a few.

User Model

Here is what the fake user data looks like:

We will import this module into the routes/api/userRoute.js file so we can access our mock user. Normally, a user would be pulled from a database, but for now, this works fine for the example.


Generating and verifying JWT Tokens

Now that we have all of that out of the way, lets get into the ‘meat & potatoes’ of JWT and how we use it. There are two JWT functions that will handle everything in this example:

jwt.sign()

jwt.sign(payload, secretkey, [options, callback])

The first function jwt.sign() will generate a JWT token, assign it to a user object, and then return that JWT token so we can pass it where ever we may need. It can be either asynchronous or synchronous depending if a callback is supplied. The payload parameter will be the user object in our case, the secretkey is made up by you, and it can be anything. The callback parameter is where we handle sending our token, and the options parameter will be where can set an expiration time among other things.

Note: It is important that in production you NEVER HAVE YOUR SECRET KEY VISIBLE like in this example. This is not production code, it is merely an example of how JWT works. Your secret key should be stored in an environment variable, like all sensitive information.

jwt.verify()

jwt.verify(token, secretkey, [options, callback])

The second asynchronous function jwt.verify() will verify the users token when a protected route is accessed. It takes in the token as one parameter, the secret key that you defined in the jwt.sign() function, and then you have the options and callback parameters. The callback will be where we can access and send protected data.

Putting it All Together

Let’s take a quick moment to look at an overview of routes/api/userRoutes.js:

Starting from the top we are just importing JWT and our mock user model. Pretty self explanatory if you’re familiar with node, so let’s move onto discussing the routes and accessing them with POSTman.

Now the real fun. Online 6 we have a POST route found at /user/login that handles our mock login system. We check to make sure the posted username and password match our mock user, and if so we generate a JWT token for the user starting on line 14 by:

  • Passing in our user object, that in this case comes from the mock user model in models/dummyUser.js
  • A secret key that in this case is privatekey
  • An options parameter { expiresIn: '1h' }
  • Finally a callback that contains the parameters (err, token)

If an err is returned in the callback, we are sending a Forbidden (403) code to signify that access is.. well forbidden.

If there is no err returned in the callback, we allow access to the token that JWT has generated. Now when we passed in the user object {user} , this is how we ‘attached’ a token to the user data. This lets us identify a specific JWT token with a user’s data.

Logging In

Here’s what it looks like when we access /user/login via POSTman:

Making a POST request to login

So in POSTman I am making a POST request to the /user/login route with form data. I am passing in a username and password key/value pair to simulate the mock user logging in.

Notice that the password and username match that of our sole mock user. Since everything matched and the user was ‘logged in’, the jwt.sign() function found in the login route returned a unique JWT token. Perfect, exactly what we want. This token is what will be used to access our protected routes. In a production application, this would be sent to a frontend client like React to be used when the client makes requests to protected backend routes.

Just to lightly touch on the expiration date, your application would need to have some sort of logic that checks for an expired token so that it can handle sending the user back to a log in page to be given a new fresh token. For example, think if you were logged into your bank account. After a few minutes of inactivity, you would usually be logged out and required to log back in. This works two-fold because A) it logs you out of your session in case you forget to yourself and B) it gives the app a chance to refresh whatever authorization it’s using.

Requesting Protected Routes

Alas, the final step to this whole JWT authorization flow. Let’s start with another POSTman gif to show what we will be accessing, then I will explain what’s going on.

First this is what happens if we try to access a protected route without a JWT token:

Accessing a protected route WITHOUT a JWT token returns 403 Forbidden

The 403 is also thrown when the token is invalid. So just as the code dictated in the /user/login GET route starting on line 24, when we fail to access a protected route with a JWT token, the callback in jwt.verify() returns err. This error lets us send out the 403 Forbidden bat signal to whatever failed to request the route.

On the flip-side, this is what it looks like when we get a 200 OK bat signal:

Granted access to a protected route with the JWT token passed in the Authorization header

Wait, what? You probably noticed I passed the JWT token in a header named Authorization with the GET request. You also probably noticed the added Bearer before the JWT token. Let me explain.

Authorization: <type> <credentials> is a pattern introduced by the W3C in HTTP 1.0. Sites that use this pattern are more than likely implementing OAuth 2.0 bearer tokens. The OAuth 2.0 Authorization framework sets another number of requirements to authorization secure. For example, requiring the use of HTTPS. Remember, HTTPS makes sending the token from the server to client more secure. Here is more info on the OAuth 2.0 Auth Framework.

So with that in mind, our Authorization header requires Bearer as the type, with the JWT token being the credentials. Knowing this, it makes the explanation for the checkToken() function found on line 45 make a little bit more sense. This function is passed into our protected route like so:

app.get('/user/login', checkToken, (req, res) => { //Callback });

  • In the checkToken() function, we check to make sure the token is not undefined, and then we split req.header into an array. This is because the Authorization header comes back as a string. For example: Bearer jh3uj3jedjd3.
  • We know that the split() method turns a string into an array of sub-strings. So we can safely assume that our now split header looks like ['Bearer', 'jh3uj3jedjd3']. That is why on line 50 we set const token = bearer[1] with an index of 1. It’s clear from the example that the token is at index 1 in the array.
  • After this, on line 52 set set req.token equal to the token we get from the Authorization header. Then we use next() to invoke the next route handler.
  • Finally, we handle an undefined header by sending a good ole’ fashion Forbidden 403.

Accessing the Protected Route

So, we’ve passed an Authorization header with the token to the protected route. We’ve verified the token to not be undefined, and we have a stripped away the Bearer string from the header in the checkToken() function, leaving us with just the token. Now, we need to use the last piece of the puzzle: jwt.verify() to gain access to the authorized data. Here’s how this works.

jwt.verify(token, 'privatekey', [options, callback]) will use req.token as the token parameter, in this case 'privatekey' as the secret key, and then our call back will look like: (err, authorizedData) => { //callback }. If the err parameter is returned, just like the others will signify to then return a Forbidden 403 response to let whoever know the token verification failed. If we pass an incorrect secret key here, we will always get back a 403 response code.

The authorizedData parameter is the bread and butter. This contains all of the protected data that we requested. Here is what the user data looks like when accessed successfully from POSTman:

We received this user data because this is what we passed as the payload parameter of the jwt.sign() function. Notice the 'iat' and 'exp' key/value pairs. iat refers to ‘issued at’ which is a default action. This is the time the token was issued at. It’s set to default unless noTimeStamp is declared. exp is the expiration option we passed in jwt.sign(). This is simply the time when the token expires. From here, you could send this data to your client, and do with it what you wish.


A quick example of using a fetch() from the client to request access to the protected route:

Imagine that when a user logged in, that the JWT token was generated and then passed to the client for storage. Pretend for a moment that authToken is the variable that stores the valid JWT token. Sending the Authorization header with the fetch request allows access to the protected route given the token passed is valid.

fetch('/user/data', {
method: 'GET',
headers: {
'Authorization': 'Bearer' + authToken
}
})
.then(res => res.json())
.then(data => { console.log(data) })
.catch(err => { console.log(err) })

Rejoice, the ride has come to a stop.

That’s that. We’ve come to the end of this wild JWT ride. Hopefully I have been pretty thorough, and if there is anything I missed or anything I didn’t get quite right, please let me know! It helps me a ton, especially as I personally write these articles to help me learn things more in-depth.

I know I did not go into how to use POSTman, and that was intentional. There are tons of videos and articles out there on how to use it. It is very simple so you’ll be up and running with it very quickly!

Thanks for reading! ✌️