Test Driven Development: JWT Authentication in Express
We will build a minimal JWT Authentication API in Express. We will have TDD and MVC principles guide us and use a PostgreSQL database. I assume readers have basic NodeJS and PostgreSQL experience. I am providing a base for the project so that we can get started quickly.
Tech:
- ExpressJS
- Jest
- PostgreSQL
Download the Base Project or the Finished Project.
What is JWT authentication?
JWT Authentication is a way to secure websites and APIs. JWTs are web tokens containing a hashed JSON object and, in the case of authentication, user details.
Typically authentication with a JWT takes the following steps:
1. The user enters their credentials into a form and that data is sent to the server.
2. The server determines if the credentials are correct. If they are then the server will gather some important data about the user and put it into a JSON object,
3. That JSON object is then sent to the user or client, in our case, in the form of a cookie.
4. Now the client will be able to take the token inside the cookie and add it to the header of any HTTP requests the client makes.
5. The server will take the HTTP requests and validate the JWT in the cookie. If the server can confirm that the JWT is valid then the request will succeed.
How do you implement JWT authentication in Express?
In Express we will implement JWT authentication. We will use middleware to validate the tokens we create.
We will be developing the API using Test Driven Development. If you are new to TDD, it is where the tests for the code are written first and then code is written to make the tests pass. It follows the pattern of:
1. Write test
2. Test fails
3. Write code
4. Test passes
5. Repeat
This method encourages simple code design and ensures that the new code being written does not break existing code.
We will follow the MVC design pattern for the development of our Express app.
Database Schema
We will have a database called “jwt_auth_sample” the database will have a single table called account. I will leave setting up PostgreSQL and creating the database to you.
CREATE TABLE account (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL
)
Nice and minimal.
Building the API
I have a base for the API on GitHub. The base will have our folder structure and blank files ready to code in.
After downloading the base, make sure to run npm install.
First we need to make an .env file in the root of the project. Inside the file add the following definitions:
ENVIRONMENT=”LOCAL”
JWT_SECRET=”anythingyouwant”
PG_PASSWORD=”postgresql-password”
JWT_SECRET will be used to hash passwords and PG_PASSWORD is the password for your local database that you set up.
Project Design
Our project will be a standard API. Routes will forward requests to controllers which will pass data to the models which will interact with our database. We will have a model, controller, and router for accounts.
Our API needs to do three basic things:
- Sign up users
- Sign in users
- Protect routes / determine authorization
The Account Model
Lets start with the account model. Since we are following Test Driven Development we will begin with writing a test. I have provided some comments to explain the test code set up. I also have added placeholders for the tests we will be writing. I like to add tests as I think of them and have them automatically fail so I don't accidentally assume a feature is finished.
// account.test.js
// supertest is a module that helps with testing http requests
import express from 'express';
import request from 'supertest';
// Body parser helps read json data from requests and responses
// Cookie parser helps handle cookies from requests and responses
import bodyParser from 'body-parser';
import cookieParser from 'cookie-parser';
// For Mocking the database responses
import { Pool } from 'pg';
// For handling hashing
import bcrypt from 'bcrypt';
// allow files to load environment variables
import * as dotenv from 'dotenv';
// Initialize variables
let server = null;
let pool = null;
// use dotenv
dotenv.config();
// Set up Jest mock for postgresql connection
jest.mock('pg', () => {
const mPool = {
query: jest.fn(),
}
return {Pool: jest.fn(() => mPool)};
});
// Functions that need to run before any tests are ran
beforeAll(() => {
app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.use(cookieParser());
server = app.listen();
pool = new Pool();
});
// Functions that need to run after each test completes.
afterEach(async () => {
jest.clearAllMocks();
server.close();
});
describe('Account Model', () => {
// Look up a user by email
// Ability to create a user
describe('findByEmail()', () => {
// Need to return the email if found
// Need to make sure an empty array is returned if not found
test("returns an empty array when results are not found", async () => {
throw new Error("Test not yet implemented");
});
test("returns the email when results are found", async () => {
throw new Error("Test not yet implemented");
});
});
describe('createAccount()', () => {
// Cant create an account if email is already in use
// Test that the account is created
test("throws error if email used is already found in the database", async () => {
throw new Error("Test not yet implemented");
});
test("successfully creates an account", async () => {
throw new Error("Test not yet implemented");
});
});
});
Our account model will be simple. It needs to do two things.
- Find accounts
- Create accounts
Now we can run the tests by running npm run test. We should see a ton of red failed tests. Lets make our first test actually test something.
Our first test will be making sure our account model returns an empty array when the account does not exist.
Our model tests will follow these steps:
- Mock the database response.
- Call the method we are testing.
- Make sure the database was queried
- Make sure the method returns the correct information
test("returns an empty array when results are not found", async () => {
// First mock the database response
pool.query.mockResolvedValue({
rows: [],
});
// Call the function being tested
const findingEmail = await accountModel.findByEmail('test@example.com');
// The database should have been queried once
expect(pool.query).toBeCalledTimes(1);
expect(pool.query).toHaveBeenCalledWith("SELECT * FROM account WHERE email = 'test@example.com';");
// findByEmail should return an empty array
expect(findingEmail).toHaveLength(0);
});
After running the test our error should be something like “accountModel is not defined.” Now we can go define our account model.
// account.model.js
import pool from "../config/database.config.js";
class Account{
}
// export the Account class
export default Account;
```
In the test define accountModel at the top of the file and initialize accountModel in the test in the describe block.
import AccountModel from '../src/models/account.model';
describe('Account Model', () => {
const accountModel = new AccountModel();
});
Now instead of writing the entire findByEmail method we will only write enough of the method to pass the test. Inside the Account class in account.model.js:
async findByEmail(email) {
// return array of accounts matching an email
let { rows } = await pool.query(`SELECT * FROM account WHERE email = '${email}';`);
return rows
}
}
Run the tests and we should see our first green! Success! Our first test driven method.
Next we write the test that will make sure the correct information is returned when results are found.
test("returns the account when results are found", async () => {
// Mock the database response
pool.query.mockResolvedValue({
rows: [
{
email: 'test@example.com',
},
],
});
const email = 'test@example.com'
const findingEmail = await accountModel.findByEmail(email);
expect(pool.query).toBeCalledTimes(1);
expect(pool.query).toHaveBeenCalledWith("SELECT * FROM account WHERE email = 'test@example.com';");
expect(findingEmail).toHaveLength(1);
expect(findingEmail).toEqual(
expect.arrayContaining([expect.objectContaining({email: 'test@example.com'}),]
)
);
});
Another success. We can move on to creating the createAccount method. We cannot let users create an account if the email they're trying to use is already is associated with an account. Lets test for that.
test("throws error if email used is already found in the database", async () => {
pool.query.mockResolvedValue({
rows: [
{
email: 'test@example.com',
password: 'test',
},
],
});
const createAccount = await accountModel.createAccount('text@example.com');
expect(createAccount.message).toBe('Email already exists');
});
Running the test will give an error: “accountModel.createAccount is not a function.” Just as expected. Now to create the function and turn the test green.
async createAccount(payload) {
// Check to make sure an account with the same email doesnt exist
let emails = await pool.query(`SELECT * FROM account WHERE email = ‘${payload.email}’;`);
if (emails.rows.length) {
return new Error(‘Email already exists’)
}
}
Next we need to make sure the createAccount method calls the correct query when the supplied email is not found.
test("createAccount() successfully creates an account", async () => {
pool.query.mockResolvedValue({
rows: [],
});
let payload = {
email: 'test@example.com',
password: 'test',
};
await accountModel.createAccount(payload);
// Database gets queried twice, once to check if the email exists and then to insert the new account
expect(pool.query).toBeCalledTimes(2);
expect(pool.query).toHaveBeenCalledWith("SELECT * FROM account WHERE email = 'test@example.com';");
expect(pool.query).toHaveBeenCalledWith("INSERT INTO account (email, password) VALUES ('test@example.com', 'test');");
});
To make the test pass the createAccount method needs to be updated to insert the new account information.
async createAccount(payload) {
// Check to make sure an account with the same email doesnt exist
let emails = await pool.query(`SELECT * FROM account WHERE email = '${payload.email}';`);
if (emails.rows.length) {
return new Error('Email already exists')
}
// insert new account into the database
return await pool.query(`INSERT INTO account (email, password) VALUES ('${payload.email}', '${payload.password}');`)
That should do it. Our tests all pass. Our account model meets the bare minimum requirements to create and find an account.
The Account Router and Controller
Next up is the account controller. The account controller will be need to do two things.
- Sign up users
- Sign in users
Both methods will require an email and password as input. The controller should block any requests missing the required fields and also check to make sure the password for signing in is correct. Under the describe block for account model make a new describe block for account controller.
describe("Account Controller", () => {
describe("signup()", () => {
test("rejects missing email", async () => {
throw new Error('Test not implemented');
});
test("rejects missing password", async () => {
throw new Error('Test not implemented');
});
test("creates query for account and returns success", async () => {
throw new Error('Test not implemented');
});
});
describe("signin()", () => {
test("rejects missing email", async () => {
throw new Error('Test not implemented');
});
test("rejects missing password", async () => {
throw new Error('Test not implemented');
});
test('returns error if password is incorrect', async () => {
throw new Error('Test not implemented');
});
test('rsuccessfully authenticates user', async () => {
throw new Error('Test not implemented');
});
});
});
We will start with the signup() method. We should first test that requests missing an email should be rejected. Our tests for the controller will mostly follow this pattern.
- Set up the request body
- Test the route by sending it a request
- Compare the response
test("rejects missing email", async () => {
let body = {
password: 'test',
};
// Response should be a 400 Bad Request error with a message explaining why
const response = await request(app).post('/auth/signup').send(body);
expect(response.header['content-type']).toBe('text/html; charset=utf-8');
expect(response.statusCode).toBe(400);
expect(response.text).toEqual('Missing email');
});
When running our test, the error should be a 404 Not Found error.
In order to write the method that will get our test to pass we have some prerequisite work to do. We need to set up and write our authentication router and the controller. Starting with the router. The router is responsible for directing HTTP requests.
// authRouter.routes.js
import express from 'express';
import AccountController from '../controllers/account.controller.js';
let authRouter = express.Router();
let accountController = new AccountController()
authRouter.post('/signin/', function(request, response, next){
accountController.signIn(request, response, next)
});
authRouter.post('/signup/', function(request,response, next) {
accountController.signUp(request, response, next)
});
export default authRouter;
We need to import authRouter in app.js.
// app.js
import express from 'express';
// import routes
import authRouter from './src/routes/auth.routes.js'
const app = express();
// Use auth routes
app.use('/api/auth', authRouter);
console.info("Running... ")
app.get('/api/', (req, res) => {
res.json({ message: "Welcome to the sample auth application." });
})
export default app;
Now requests to example.com/api/auth will be handled by authRouter. Back in the account test file we need to add the router to the test. Import the router at the top of the file.
import authRouter from '../src/routes/auth.routes.js';
In the beforeAll block add
app.use(‘/auth’, authRouter);
Now in the account controller file lets add the methods we will be writing.
// account.controller.js
class AccountController {
// Sign up for an account
async signUp(request, response, next) {
}
// Sign in to the account
async signIn(request, response) {
}
}
export default AccountController;
Our test results should no longer be a Not Found error. This is good, our router works.
Now we’re ready to write the sign up method. We want to start by blocking any payloads that don't include a email.
async signUp(request, response, next) {
if (!request.body) {
return response.status(400).send('Missing request body');
}
}
Simple enough, do the same for password.
test("rejects missing password", async () => {
let body = {
email: 'test@example.com',
};
const response = await request(app).post('/auth/signup').send(body);
expect(response.header['content-type']).toBe('text/html; charset=utf-8');
expect(response.statusCode).toBe(400);
expect(response.text).toEqual('Missing password');
});
async signUp(request, response, next) {
if (!request.body) {
return response.status(400).send('Missing request body');
}
// Require password in the request body
if (!request.body.password) {
return response.status(400).send('Missing password');
}
}
If the test passes, move on to the next requirement. We need to make the signup method actually signup the user. The controller will need to pass the account information to the account model.
test("creates query for account and returns success", async () => {
let body = {
email: 'test@example.com',
password: 'test',
};
const response = await request(app).post('/auth/signup').send(body);
// The queries are not exactly necessary. They're already tested by the model tests.
expect(pool.query).toBeCalledTimes(2);
expect(pool.query).toHaveBeenCalledWith("SELECT * FROM account WHERE email = 'test@example.com';");
expect(pool.query).toHaveBeenCalledWith(expect.stringMatching(/INSERT INTO account \(email, password\) VALUES \('test@example.com', '[^']+'\);/));
expect(response.header['content-type']).toBe('text/html; charset=utf-8');
expect(response.statusCode).toBe(201);
expect(response.text).toEqual('Account created successfully');
});
In the controller we need to add in the functionality that will call the model to create the account. The controller needs to import the account model and have a constructor to initialize the model instance.
// account.controller.js
import Account from '../models/account.model.js';
class AccountController {
constructor() {
// Create a new model object to perform operations on the database table
this.account = new Account();
}
Under the check for missing passwords add in the final signup code that will create the account.
// Create the account with the data in the request
await this.account.createAccount({
email: request.body.email,
password: request.body.password
})
return response.status(201).send('Account created successfully');
Our test passes, BUT we have a huge problem. The password is not being hashed, it is being stored in plain text. Lets write a test to make sure the text being saved is not the same as the text being passed in.
test("does not call database with same password value as passed in", async () => {
let body = {
email: 'test@example.com',
password: 'test',
};
const response = await request(app).post('/auth/signup').send(body);
expect(pool.query).toBeCalledTimes(2);
expect(pool.query).toHaveBeenCalledWith("SELECT * FROM account WHERE email = 'test@example.com';");
expect(pool.query).not.toHaveBeenCalledWith("INSERT INTO account (email, password) VALUES ('test@example.com', 'test');");
expect(response.header['content-type']).toBe('text/html; charset=utf-8');
expect(response.statusCode).toBe(201);
expect(response.text).toEqual('Account created successfully');
});
I think we can come back and improve this. It makes sure the password isn’t the same but not necessarily hashed.
We will use bcrypt to hash the users password. at the top of the controller file import bcrypt.
import bcrypt from ‘bcrypt’;
Change the password line when calling create account to:
// Create the account with the data in the request
await this.account.createAccount({
email: request.body.email,
password: await bcrypt.hash(request.body.password,10), // Hash the password
})
Success! Our account controller and route are working! Now that users can sign up they also need a way to sign in. Lets write the tests. Signin needs to reject requests with missing emails and passwords.
test("rejects missing email", async () => {
let body = {
password: 'test',
};
const response = await request(app).post('/auth/signin').send(body);
expect(response.header['content-type']).toBe('text/html; charset=utf-8');
expect(response.statusCode).toBe(400);
expect(response.text).toEqual('Missing email');
});
In the controller:
// Sign in to the account
async signIn(request, response) {
// Respond with an error if body is missing required fields
// Require email in the request body
if (!request.body.email) {
return response.status(400).send('Missing email');
}
}
Test and implement checking for a password:
// Sign in to the account
async signIn(request, response) {
// Respond with an error if body is missing required fields
// Require email in the request body
if (!request.body.email) {
return response.status(400).send('Missing email');
}
}
test("signin() rejects missing password", async () => {
let body = {
email: 'test@example.com',
};
const response = await request(app).post('/auth/signin').send(body);
expect(response.header['content-type']).toBe('text/html; charset=utf-8');
expect(response.statusCode).toBe(400);
expect(response.text).toEqual('Missing password');
});
Now the next test we need is to determine if the password is correct. If the password is not correct the user cannot be authorized and the user will need to be be told that the password is wrong.
test('signin() returns error if password is incorrect', async () => {
let body = {
email: 'test@example.com',
password: 'test',
};
pool.query.mockResolvedValue({
rows: [
{
email: 'test@example.com',
password: 'test',
},
],
});
const response = await request(app).post('/auth/signin').send(body);
expect(response.header['content-type']).toBe('text/html; charset=utf-8');
expect(response.statusCode).toBe(403);
expect(response.text).toEqual('Incorrect password');
});
At the bottom of the signin method lets add logic to decrypt the password returned by the database. Lets also handle the case of the decryption failing.
// Get the relevant account from the database and compare that password to the password in the request body
let records = await this.account.findByEmail(request.body.email);
// Usue bcrypt to compare plain text to the hashed password
let isValidPassword = await bcrypt.compare(request.body.password, records[0].password);
// Invalid passwords respond with an error
if (!isValidPassword) {
return response.status(403).send('Incorrect password');
}
Now that we have tests to make sure the password is correct we need to authenticate the user. As mentioned in the beginning of this tutorial we will be using JWTs to authenticate users. We will test that the user’s credentials are saved in a JWT and test storing the token in the browser cookies.
test('signin() successfully authenticates user', async () => {
let body = {
email: 'test@example.com',
password: 'test',
};
pool.query.mockResolvedValue({
rows: [
{
email: 'test@example.com',
password: await bcrypt.hash('test',10),
},
],
});
const response = await request(app).post('/auth/signin').send(body);
expect(response.header['content-type']).toBe('text/html; charset=utf-8');
expect(response.header['set-cookie'][0]).toEqual(expect.stringMatching(/jwt-auth=[^']+HttpOnly; Secure; SameSite=Lax/));
expect(response.statusCode).toBe(200);
expect(response.text).toEqual('Account logged in');
});
In the controller signin method we will use a package called jsonwebtoken to help with creating and reading JWTs
import jwt from 'jsonwebtoken'
Now in the bottom of the signin function we will create the JWT and save the cookie. Any cookie that is storing a JWT to should have httpOnly set to true.
signIn() {
...
// Password is verified. Create a JWT with the account data
let token = jwt.sign(records[0], process.env.JWT_SECRET, {expiresIn: 3600 * 24});
// Store the JWT in a cookie
let cookieOptions = {expires: new Date(Date.now() + 3600 * 24), secure: process.env.ENVIRONMENT == "Local" ? false : true, httpOnly: true, sameSite: "Lax"}
// respond with success and add the cookie
return response.status(200).cookie('jwt-auth', token, cookieOptions).send('Account logged in');
}
Great! We have users that can signup and sign in, but how do we stop users from accessing private pages? We use middleware. Express allows developers to chain together functions to be called. Authentication is one use of this feature. Other good uses of middleware in Express are for logging and error handling.
I want to create at least two more routes to test the authentication middleware, one public route and one private route for logged in users only. Add two more describe blocks under the signin block
describe("testPublicRequest()", () => {
test('response success', async () => {
throw new Error('Not implemented');
});
});
describe("testPrivateRequest()", () => {
test('returns error without token', async () => {
throw new Error('Not implemented');
});
test('returns error with incorrect token', async () => {
throw new Error('Not implemented');
});
test('returns response succesfully', async () => {
throw new Error('Not implemented');
});
});
The public test will be simple. We just need to know the route works and returns what is expected. The private route will be a little harder. In addition, we need to test that only users that provide the correct token can log in.
Our public test:
test('testPublicRequest() response success', async () => {
const response = await request(app).get('/auth/test-public');
flushPromises();
expect(response.header['content-type']).toBe('text/html; charset=utf-8');
expect(response.statusCode).toBe(200);
expect(response.text).toEqual('Success');
});
In our authRouter add the route.
authRouter.get('/test-public/' , function(request,response) {
accountController.testPublicRequest(request, response)
});
The testPublicRequest method in the controller:
// test a unprotected route
async testPublicRequest(request, response) {
return response.status(200).send('Success');
}
Now the private route. We finally get to start working on the middleware. Lets write the next test for when the user has no token
test('testPrivateRequest() returns error without token', async () => {
const response = await request(app).get('/auth/test-private');
expect(response.header['content-type']).toBe('text/html; charset=utf-8');
expect(response.statusCode).toBe(403);
expect(response.text).toEqual('No access token provided');
});
Our first error is of course our route not being found. Lets add private to our routes file
import verifyAuth from '../middleware/verifyAuth.middleware.js';
authRouter.get('/test-private/', verifyAuth, function(request,response) {
accountController.testPrivateRequest(request, response)
});
Make note of the verifyAuth function being called. that is our middleware. Our next error is the lack of verifyAuth. Lets define and write verifyAuth.
// verifyAuth.middleware.js
// Middleware to lock protected routes
async function verifyAuth(request, response, next) {
// Use cookie-parser to retrieve the JWT from cookies
let token = request.cookies['jwt-auth'];
// if there was no token saved then the route cannot be accesed
if (!token) {
return response.status(403).send("No access token provided");
}
// Move on to the next function in the chain
next();
}
export default verifyAuth;
Success. Lets make sure that the middleware doesn't let anyone through to the next function in the chain with an incorrect token. In order to decode our token we will use jsonwebtoken. Inside the verifyAuth middleware:
import jwt from ‘jsonwebtoken’;
// verifyAuth.middleware.js
// Middleware to lock protected routes
async function verifyAuth(request, response, next) {
// Use cookie-parser to retrieve the JWT from cookies
let token = request.cookies['jwt-auth'];
// if there was no token saved then the route cannot be accesed
if (!token) {
return response.status(403).send("No access token provided");
}
try {
let decoded = jwt.verify(token, process.env.JWT_SECRET);
// JWT looks good. Add the data from the token to the request. The api will then be able to use that data
request.user = decoded;
}
catch (error) {
// if the JWT couldn't be verified then the route cannot be accesed
return response.status(403).send("Invalid access token");
}
// Move on to the next function in the chain
next();
}
export default verifyAuth;
If the token is incorrect, verification will not succeed and will throw an error. We handle the error by returning and not continuing to the next function in the chain.
This leaves us our final test. We need to make sure our user can actually access private routes.
test('returns response succesfully', async () => {
let body = {
email: 'chris@chrisgrime.com',
password: 'test',
};
pool.query.mockResolvedValue({
rows: [
{
email: 'chris@chrisgrime.com',
password: await bcrypt.hash('test',10),
},
],
});
const signInResponse = await request(app).post('/auth/signin').send(body);
let cookie = signInResponse.get('Set-Cookie')
const response = await request(app).get('/auth/test-private').set('Cookie', [...cookie]);
expect(response.header['content-type']).toBe('text/html; charset=utf-8');
expect(response.statusCode).toBe(200);
expect(response.text).toEqual('Success with access token');
});
Our last piece of code. The method in the controller to handle the request.
// test a protected route
async testPrivateRequest(request, response) {
return response.status(200).send('Success with access token');
}
Refactoring and Improving Tests
A key part of Test Driven Development is refactoring. We have our tests passing, which proves our functionality is there. Now is the time to start making improvements.
Our first improvement is to revisit the test for making sure passwords sent to the account model are hashed. We can confirm that the password is not the same which is helpful, but doesn't really test that the password is hashed.
We should really check that the password being sent to the database is in the format of a bcrypt hash. We can use regex in our expect statement. Each bcrypt hash (at least recent versions) starts with $2b2 it is then followed by the number of hashes, in our case 10, and another $. The hash is also always 60 characters. So following $2b$10$ we will have 53 more characters. We can use regex to at least make sure the password is in this format.
test("The password is hashed before being inserted into the database", async () => {
const accountController = new AccountController();
jest.spyOn(accountController.account, 'createAccount');
await accountController.signUp(req, res, next);
expect(accountController.account.createAccount).toHaveBeenCalled();
expect(accountController.account.createAccount).toHaveBeenCalledWith(
{"email": "test@example.com", "password":expect.stringMatching(/^\$2b\$10\$.{53}$/)}
);
});
The second improvement is important. We are writing raw SQL queries. There is no sanitation happening. This is not good and incredibly insecure. This leaves our application open to SQL injection. Before this tutorial is wrapped up we need to fix that.
The node-postgres package that the application uses supports a method called parameterized queries that will prevent SQL injections. This method takes the parameters that we pass to our query string and sanitizes them. We could also write our own SQL sanitizer, but I am not a security expert. Lets leave the security to the battle tested code provided by node-postgres.
An example of changing our queries is this
pool.query(SELECT * FROM account WHERE email = ‘test@example.com’;)
will become this
pool.query(SELECT * FROM account WHERE email = $1;, [‘test@example.com’])
Anywhere we are adding parameters directly into a query string needs to be modified.
We can start by finding all our query strings in account.test.js and updating them to be parameterized queries.
Here is a list of updated queries:
pool.query("SELECT * FROM account WHERE email = $1;", ['test@example.com']);
pool.query("INSERT INTO account (email, password) VALUES ($1, $2);", ['test@example.com', 'test']);
test with regex can be changed to
expect(pool.query).toHaveBeenCalledWith("INSERT INTO account (email, password) VALUES ($1, $2);", ['test@example.com', expect.anything()]);
Now we should have a ton of failing tests. Time to go update those files. This is where the beauty of the MVC architecture shines. We should only need to update the queries in our model file to have passing green tests once again.
I hope you were able to follow along and can now add JWT authentication to your NodeJS applications. The finished tutorial project is available on Github.
