×
By the end of this chapter, you should be able to:
So how do we secure our API? We can do what we have been doing before and store information in the session and in cookies, but this can become problematic. This is especially true when you're working with lots of different servers (lots of small applications written in different languages) or when you're building mobile applications, which have severe limitations with cookies.
Instead of using cookies/sessions to authenticate users, we will be using tokens. This process involves creating an encrypted token on the server and sending it to the client where it will either be stored in a cookie or in localStorage
(more commonly done with localStorage
), and on every future request, the token will be placed in the header and will be decrypted on the server.
The server will store a secret key used to encrypt and decrypt the token so that there can not be tampering with the token in the client. This secret key is simply just a string of characters, but NEVER something you should make public when your application is in production.
You can read more about tokens versus cookies here.
The type of token that we will be using is a JSON Web Token, or JWT (pronounced "jot").
Before we continue, read through this introduction to JWTs so you can understand a bit more about what they are and how they work. JWTs are a means of securing information between two parties and consist of three parts:
You can find the complete code for this example here
The module we will be using to create secure tokens is jsonwebtoken
.
We will start with the application we built in the previous section. The code for that can be found here.
The first thing we need to do is install the jsonwebtoken
module, so in the Terminal let's make sure to run npm install jsonwebtoken
Let's make sure to add the following to our routes/users.js
const bcrypt = require("bcrypt"); const express = require("express"); const router = express.Router(); const db = require("../db"); const jsonwebtoken = require("jsonwebtoken"); const SECRET = "NEVER EVER MAKE THIS PUBLIC IN PRODUCTION!";
Notice here the secret is something we do not want to expose in production so we'll make this an environment variable, which you will learn about later! For now, let's create a JSON Web Token when the user successfully logs in. Here's what that looks like in our routes/users.js
// npm packages const bcrypt = require("bcrypt"); const express = require("express"); const router = express.Router(); const db = require("../db"); const jsonwebtoken = require("jsonwebtoken"); const SECRET = "NEVER EVER MAKE THIS PUBLIC IN PRODUCTION!"; router.post("/login", async (req, res, next) => { try { // try to find the user first const foundUser = await db.query( "SELECT * FROM users WHERE username=$1 LIMIT 1", [req.body.username] ); if (foundUser.rows.length === 0) { return res.json({ message: "Invalid Username" }); } // if the user exists, let's compare their hashed password to a new hash from req.body.password const hashedPassword = await bcrypt.compare( req.body.password, foundUser.rows[0].password ); // bcrypt.compare returns a boolean to us, if it is false the passwords did not match! if (hashedPassword === false) { return res.json({ message: "Invalid Password" }); } // let's create a token using the sign() method const token = jwt.sign( // the first parameter is an object which will become the payload of the token { username: foundUser.rows[0].username }, // the second parameter is the secret key we are using to "sign" or encrypt the token SECRET, // the third parameter is an object where we can specify certain properties of the token { expiresIn: 60 * 60 // expire in one hour } ); // send back an object with the key of token and the value of the token variable defined above return res.json({ token }); } catch (e) { return res.json(e); } });
Let's test this out!
http localhost:3000/users http POST localhost:3000/users/ username='elie' password='secret' http POST localhost:3000/users/login username='elie' password='secret' http POST localhost:3000/users/login username='elie' password='secretz'
Notice in the third request we get back a token! This token is simply just a string of characters that the client we are sending to would save (in the case of the browser, that would commonly be localStorage). Now that we're sending back tokens, let's verify them on future requests! Let's add the following to our routes/users.js
router.get("/secret", async function(req, res, next) { try { // let's try to get the token out of the header const authHeaderValue = req.headers.authorization; // let's verify that this was a token signed with OUR secret key const token = jwt.verify(authHeaderValue, SECRET); // if so - awesome! return res.json({ message: "You made it!" }); } catch (e) { // otherwise send back a status code of 401 (Unauthorized) with a message return res.status(401).json({ message: "Unauthorized" }); } });
Let's test this out!
http POST localhost:3000/users/login username='elie' password='secret' # to get a token http localhost:3000/users/secret Authorization:WHATEVER_TOKEN_YOU_JUST_GOT
The second response should give us an object with the key of message and a value of "You made it!". Let's try an incorrect request!
http localhost:3000/users/secret Authorization:hackedyou
We should now see a status code of 401 and a message of Unauthorized.
Imagine we wanted to make a couple more protected routes, it can be pretty tedious to have to repeat the extracting of a token from headers and verification - so let's put that in a helper function! As you have quite a few of these or want to re-use them in different files, it's nice to have a folder that contains this middleware. For now, we only need it in the users
routes so let's keep it simple. In our routes/users.js
// helpful middleware to make sure the user is logged in function ensureLoggedIn(req, res, next) { try { const authHeaderValue = req.headers.authorization; const token = jwt.verify(authHeaderValue, SECRET); return next(); } catch (e) { return res.status(401).json({ message: "Unauthorized" }); } } router.get("/secret", ensureLoggedIn, async function(req, res, next) { try { return res.json({ message: "You made it!" }); } catch (err) { return res.json(err); } }); module.exports = router;
So far we are able to protect routes that depend on having a valid JWT, but let's imagine we have a route that displays information about a user who is logged in. If we want to authorize only users with that username to be able to access their information there's a bit more we might have to do. Let's add the following to our routes/users.js
// helpful middleware to make sure the username stored on the token is the same as the request function ensureCorrectUser(req, res, next) { try { const authHeaderValue = req.headers.authorization; const token = jwt.verify(authHeaderValue, SECRET); if (token.username === req.params.username) { return next(); } else { return res.status(401).json({ message: "Unauthorized" }); } } catch (e) { return res.status(401).json({ message: "Unauthorized" }); } } router.get("/:username", ensureCorrectUser, async function(req, res, next) { try { return res.json({ message: "You made it!" }); } catch (err) { return res.json(err); } }); module.exports = router;
Notice here we are adding some middleware to check if the username we are storing on the token is the same as what is passed in the URL. If those are different that means a user with a different username is trying to access someone elses information.
You can find the complete code for this example here
One last point that you must know, is that very commonly the Authorization header does not come in this fashion like we've seen:
Authorization: Value
Instead, we first specify the type
of authorization, which in our case is called Bearer. There are other types of Authorization like Basic, which we will not be using. So you may see the Authorization header look like this:
Authorization: Bearer Value
What this means is instead of the following:
const authHeaderValue = req.headers.authorization;
We need to simply just split the string and get the second value separated by a space:
const authHeaderValue = req.headers.authorization.split(" ")[1];
When you're ready, move on to Input Validation with JSONSchema