Token-Based Authentication

Tyroo West
CodeX
Published in
15 min readSep 1, 2019

A thorough guide to implementing user authentication in your application

Photo by Matt Artz on Unsplash

In this tutorial, we will cover token-based authentication.

The general concept behind token-based authentication is the ability for users to obtain a token in exchange for their user credentials. This token authorizes the user to access protected resources without the need for querying for credentials.

Once the token is obtained, the user is now authenticated and can access protected resources for a specified amount of time.

In other words…

Hotel and Room Keys

Typically when you check into a hotel you are given a room key. Think of the room key as your token. This room key gives you the user, access to the protected resources, such as the gym and the pool.

You are authorized to use the gym and pool for a specified amount of time: the duration of your staty at the hotel. Once you have checked out of the hotel, you are no longer authorized to access those protected resources.

What is a cookie? 🍪

Cookies are small pieces of data sent from a website and are stored in the user’s web browser while the user is browsing that website. Every time the user loads that website back, the browser sends that stored data back to website or server, to distinguish user’s previous activity.

Getting Started

In this tutorial, the emphasis is on token-based authentication therefore, we will not be using a database. In another tutorial, I will revisit this topic to create a MERN Stack Web application using token-based authentication.

Side note…

I wrote this post from a particular perspective. At the time of this post, I was a teaching assistant for a coding bootcamp and noticed a lot of students struggling debugging their console errors. In this guide, we analyze and resolve common errors. With that said…

Let’s Get Started

The 3 most important tools we will be using for this project are the following:

bcrypt: A library to help you hash passwords.

json-webtoken: A JSON object that is defined as a safe way to represent a set of information between two parties.

dotenv: A zero-dependency module that loads environment variables from a .env file into process.env.

cookie-parser: Parse Cookie header and populate req.cookies with an object keyed by the cookie names. Optionally you may enable signed cookie support by passing a secret string, which assigns req.secret so it may be used by other middleware.

Now, lets set up our app:

Assuming you have Node.js and Git installed, run the following commands in your terminal to get started.

$ cd Desktop
$ mkdir authentication && cd authentication
$ git init
$ npm init -y
$ npm i express bcrypt dotenv jsonwebtoken nodemon morgan cookie-parser uuid
$ mkdir controllers utilities routes config models && touch server.js
$ touch config/dbUsers.json
$ echo -e 'node_modules \n.env'> .gitignore
$ echo -e 'PORT=portNumber \nSALTROUNDS=number \nSECRET=string'> .env

In this tutorial config and models is not needed. However, if we were using a database the config directory would contain our database connection and our model's directory would contain the models of our data.

In your .env file, change the following:

portNumber to a port number, typically 3001.
number to an integer.
string to a "string", this is just a secret signature we will user later on.

The last change is to update our package.json file and add a script for nodemon. In package.json add the following:

"scripts": {
"nodemon": "nodemon server.js"
}

Heres a summary of what we’ve just done.

Default project structure
Starter Application
  1. made a directory called authentication on your desktop and navigated to the directory
  2. we initialize the project with git
  3. we initialize the project with a package.json file with default settings
  4. we install our dependencies
  5. we create our empty server file and the needed directories
  6. created a Users.json file, that would contain our mock database of users
  7. created a file called .ignore and write names of files/folders to ignore
  8. create a file called .env that will hold theenvironment vairables that will be used throughout our project by using process.env .

Let's start the server.js file by running npm run nodemon from the terminal.

let's add some code to our express app. I will not explain in detail what each line of code does, however, the answer to these question can quickly be found with a bit of google foo! Moving on…

// In server.jsconst { config } = require('dotenv');
const express = require('express');
const app = express();
const logger = require('morgan');
const cookieParser = require('cookie-parser');
const userRoutes = require('./routes/user');
// LOADS ENVIRONMENT VARIABLES
config({ debug: process.env.DEBUG });
// LOGS HTTP METHODS
app.use(logger('dev'));
// PARSES JSON
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
// PARSES COOKIES
app.use(cookieParser(process.env.SECRET));
// ROUTES
app.use('/users', userRoutes);
// LISTENS ON PORT
app.listen(process.env.PORT, () =>
console.log(`App running on http://localhost:${process.env.PORT}`)
);

Congrats! We have our first error! 🎉

We’ll get to that soon enough. But before then, a few things to note. In our code, we are using destructuring:

es5
require('dotenv').config({ debug: process.env.DEBUG });
es6const {config} = require('dotenv');
config({ debug: process.env.DEBUG });

Now, for the Error… 🧐

At first, this can be overwhelming, but if we look closely…

Error One
Error: Cannot find module './routes/user'
  1. What does our error say? Error: Cannot find module './routes/user'
  2. What does this error mean? Cannot find our module userRoutes
  3. Why are we getting this error? ur we are requiring a variable that references a file which does not exist yet
  4. How can we fix this error? Create a file called user.js inside of the routes directory

Side Note: what does app.use('/users', userRoutes) do?

app.use('/users', userRoutes) prefixes all of our routes inside the routes/user.js with /users .

// In routes/user.jsconst router = require('express').Router();
const {login, logout, signup, cookieCheck, getUsers} = require('../controllers/user');
router.get('/all', getUsers);
router.get('/authorized', cookieCheck);
router.post('/login', login);
router.get('/logout', logout);
router.post('/signup', signup);
module.exports = router;

What we are doing here is requiring & exporting the, and using our controller functions to handle our routes. These functions getUser, cookieCheck, login, logout, signup are used to do something on these particular routes. We will review these in a second. But first…

We now have another Error! 🤭

Error Two
Error: Cannot find module ‘../controllers/user’

Take some time to think about how to answer the following questions:

  1. What does our error say?
  2. What does this error mean?
  3. Why is this error happening?
  4. How can we fix this error?

Answer: our controller functions are not defined.

To resolve this error, let's create a file called user.js inside of the controllers directory and insert the following code:

// In controllers/user.jsconst Users = require('../config/dbUsers.json');const getUsers = async (req, res) => {
res.json(Users);
};
const login = async (req, res) => {
res.send('you hit the login route');
};
const logout = async (req, res) => {
res.send('you hit the logout in route');
};
const signup = async (req, res) => {
res.send('you hit the signup in route');
};
const cookieCheck = async (req, res) => {
res.send('you hit the authorized route, we will need to check your cookies');
};
module.exports = { login, signup, logout, cookieCheck, getUsers };

Oh look, another Error, yay!🤓 🎉 This is the last one I promise 🤥

Error Three
SyntaxError: dbUsers.jsonUnexpected end of JSON input

How do we fix this one? no clue?

Let’s google it! 🧐 …Wait, what do I google?

Answer: SyntaxError: Unexpected end of JSON input

…If you didn’t find the answer, here is why we get the error:

We do not have any data in our dbUsers.json file

How to Fix?

Open our config/dbUsers.json file and copy 🍝 our mock data.

Make sure the data is properly formatted as JSON by using double quotes " "

// In config/dbUsers.json  {
"username": "tyroo",
"hashedPassword": "$2b$10$lprsK4kY7fWm1jv/a97jLO17n5NIm1qLmEoN/lO6Ip2curY.jSnSm",
"plainTextPassword": "tyrooTheKangaroo"
},
{
"username": "hanna",
"hashedPassword": "$2b$10$CsQBUeevFRweWyIh7uYorutAmBFiy6d1/brgTWUP85U0zh3r3nxr6",
"plainTextPassword": "hannaBanna"
},
{
"username": "zane",
"hashedPassword": "$2b$10$q1akMvEKFrDF34F6Xd.kHe1.IN27b3zUYBHn8TsnY3oZ9Su.2hkoG",
"plainTextPassword": "zaneTheMane"
}
]

For demonstration purposes, we’ve hardcoded plainTextPasswords and hashedPasswords for testing our mock users. However, we will create new users and hash their passwords.

Congratulations!

You’ve made it this far. At this point, it would be a good idea to start testing our routes and ensure everything is working correctly. If you have not done so already, go ahead and download POSTMAN.

🚨Before you start testing routes!!

Every time you save your JavaScript files nodemon will restart the application. Therefore, our newly created users are deleted every time we save.

🛣 Here’s what our routes look like, the corresponding methods, and the response:

| METHOD | ROUTE             | RESPONSE                        |
| ------ | ----------------- | ------------------------------- |
| GET | /users/logout | you hit the logout route |
| GET | /users/all | displays our mock database |
| GET | /users/authorized | you hit the authorized route... |
| POST | /users/login | you hit the login route |
| POST | /users/signup | you hit the signup route |

📂 Your application structure should look like this:

You can ignore the images directory and the README.mdthose were included in the image while testing the tutorial.

Application Structure Checkpoint
Application Structure + User Routes

Now that all our routes are defined, let write the logic for hashing passwords and start testing using POSTMAN.

🧐📝 Testing our Routes in POSTMAN

Let’s open POSTMAN and make our first request.

TODO:

  1. set our HTTP METHOD to GET
  2. set our route name to http://localhost:PORT/users/all
  3. ensure the PORT is the correct number. Check your .env
  4. finally, click send
POSTMAN dashboard with instructions
POSTMAN Dashboard

Notice on our GET /users/all route, our passwords are stored as plain text. What we need to do is store the plainTextPasswordsas hashedPasswords using bcrypt.

Let's create a file called passwordService.js in our utility directory and insert the following code:

// In utilities/passwordService.jsconst bcrypt = require('bcrypt');
const { SALTROUNDS } = process.env;
module.exports = {
hashPassword: async myPlaintTextPassword => {
try {
let hash = await bcrypt.hash(myPlaintTextPassword, parseInt(SALTROUNDS));
return hash;
} catch (err) {
if (err) throw err;
}
},
checkPassword: async (myPlaintTextPassword, hash) => {
try {
let isMatched = await bcrypt.compare(myPlaintTextPassword, hash);
return isMatched;
} catch (err) {
if (err) throw err;
}
}
};
  1. What we are doing here is requiring the bcrypt package and using our environment variable SALTROUNDS from our .env file.
  2. Exporting two functions, hashPassword and checkPassword which will be used in our login and register routes. This would typically be done on your user model but because we are not using an official database we will use them directly on our routes.
  • hashPassword: is an async function that takes a plain text password as an argument, uses the bcrypt.hash method which takes a plain text as its first argument and the number of salt rounds as its second argument and returns the hashed password.
  • checkPassword: is an async function that takes a plain text password as its first argument and a hashed password as its second argumentand uses the bcrypt.compare method which takes a plain text as its first argument and the hash as its second argument and returns the hashed password.

Although our environment variable for SALTROUNDS is a number, we still need to parse our value as an Integer.

SaltRounds…Ummm what's that? 🤯

SALTROUNDS: “The Cost Factor”

Bcrypt Hash example
Bcrypt Hash Example

The cost factor controls the amount of time needed to calculate a single BCrypt hash. The higher the cost factor, the more hashing rounds is done. Increasing the cost factor by one doubles the necessary time. The higher the cost factor, the more difficult a hash is susceptible to brute-forcing.

  1. The salt is a random value and should differ for each calculation. The results should hardly ever be the same, even for equal passwords.
  2. The salt is usually included in the resulting hash-string in a readable form. So with storing the hash-string you also store the salt.
  3. A cost factor of 10 means that the calculation is done 2¹⁰ times which is about 1000 times. The more rounds of calculation you need to get the final hash, the more CPU/GPU time is necessary. This is no problem for calculating a single hash for a login, but it is a huge problem when you brute-force millions of password combinations.

We have our function, how do we use it?

For this exercise, we will modify each route individually, so be sure to always copy and paste the newconstant variables located at the top of each code snippet.

Insert the following code for the signup route:

// In controllers/user.jsconst uuid = require('uuid/v4');
const Users = require('../config/dbUsers.json');
const { hashPassword } = require('../utilities/passwordService');
const signup = async (req, res) => {
try {
let newUser = {
id: uuid(),
password: req.body.password,
hashedPassword: await hashPassword(req.body.password),
username: req.body.username
};
Users.push(newUser);
res.status(200).redirect('/users/all');
} catch (err) {
if (err) throw err;
}
};

What we are doing here is requiring a package uuid which creates a unique id for our newUser object. Then we use our hashPassword middleware function and pass our password from the post in ourreq.body. We then push our newUser to our dbUsers.jsonfile, send a status code to our client and redirect to our /users/allroute.

Test your /users/signup route using POSTMAN

POST METHOD POSTMAN
  • HTTP METHOD:POST,
  • ROUTE:http://localhost:yourPortNumber/users/signup
  • click on body
  • select x-www-form-urlencoded
  • finally, add a key-value pair of username, password

Once you submit your POST you should have a new user in the response

New User with id, username, password, and hashed password

Wallah! You should now be able to create a new user which should have an id, username, password, and a hashedPassword field.

Next, we will add functionality to our login route using our checkPassword middleware function from utilities/passwordService.js.

Add the following code to our login route:

// in controllers/user.jsconst uuid = require('uuid/v4');
const Users = require('../config/dbUsers.json');
const { hashPassword, checkPassword } = require('../utilities/passwordService');
const login = async (req, res) => {
try {
let user = Users.find(user => user.username === req.body.username);
let isMatch = await checkPassword(req.body.password, user.hashedPassword);
if (user) {
if (isMatch) {
res.status(200).redirect('/users/authorized');
} else {
res.send('sorry password did not match');
}
} else {
res.send('sorry username does not match');
}
} catch (err) {
if (err) throw err;
}
};

Test your login route using POSTMAN

- request type is POST
- route is http://localhost:yourPortNumber/users/login
- click on body
- select www-form-urlencoded
- add key-value pair of username & password
- try using incorrect credentials
- try using correct credentials

What we are doing in our login route is returning the first user in our “database” that matches the req.body.username. Then we are using our checkPassword function which returns a Boolean value. The check password function checks if the req.body.password matches the hashedPassword from our user.

Now we conditionally check if a user exists. If the user exists, we then check if the password matches. If all goes well, we send a 202 status code and redirect to our authorized route. Otherwise, we send errors to the client explaining what went wrong. In a real application, your message would look more something like: “sorry, the credentials you have entered are incorrect please try again”.

We now need to implementjson-webtokens and 🍪’s.

Let's create and open a file utilities/tokenService.js. In this file, we need the jsonwebtoken package and our SECRETenvironment variable. We will then create two functions, createToken and isValidToken.

// In utilities/tokenService.jsrequire('dotenv').config();
const jwt = require('jsonwebtoken');
const { SECRET } = process.env;
module.exports = {
createToken: async user => {
try {
let token = await jwt.sign(
{ user, exp: Math.floor(Date.now() / 1000) + (60 * 60) },
SECRET
);
return token;
} catch (err) {
if (err) throw err;
}
},
isValidToken: async token => {
try {
let decoded = await jwt.verify(token, SECRET);
return decoded;
} catch (err) {
if (err) throw err;
}
}
};

What we expect the code above to do is create a token by using jwt to sign a token. Passing in an object with our expected user, and an expiration time, and ourSECRET environment variable. Then we return the token. Additionally, we verify our tokenusing jwt.verify which takes an expected token string as its first argument and our SECRET environment variable.

Now in our controllers/user.js file lets refactor our code and each route to use our token service and cookies.

Updating our Route handlers, one by one

🚨👀🧐MAKE SURE TO TEST EVERY ROUTE!!

// In controllers/user.jsconst uuid = require('uuid/v4');
const Users = require('../config/dbUsers.json');
const { hashPassword, checkPassword } = require('../utilities/passwordService');
const { createToken, isValidToken } = require('../utilities/tokenService');
const cookieOptions = {
expires: new Date(Date.now() + 900000),
httpOnly: true,
// secure: true, on deployment for https
signed: true
};

In the above snippet:

  • require UUID which automatically generates unique id’s for our users.
  • require our Users mock database.
  • require our utilities for our password Service and token Service.
  • create an object called cookie options which will be explained below.
// In controllers/user.jsconst getUsers = async (req, res) => {
try {
res.json(Users);
} catch (err) {
if (err) throw err;
}
};

In the above snippet:

  • we create a getUsers handler that sends our Users array to the client.
// In controllers/user.jsconst login = async (req, res) => {
try {
let user = Users.find(user => user.username === req.body.username);
let isMatch = await checkPassword(req.body.password, user.hashedPassword);
if (user) {
if (isMatch) {
let token = await createToken(user);
console.log('token', token);
res.cookie('token', token, cookieOptions).redirect('/users/authorized');
} else {
res.send('sorry password did not match');
}
} else {
res.send('sorry username does not match');
}
} catch (err) {
if (err) throw err;
}
};

In the above snippet, we create a login handler which:

  1. Finds a user by its username
  2. Checks the submitted password to our users hashedPassword
  3. Then we check if the user exists if true…
  4. We then check if the password matches, if true…
  5. Create a token with the user object.
  6. Create a cookie called token, pass in our token, and our cookieOptions and redirect to authorized route.

Notice, we console.log("token", token);. Copy 🍝 this token from the terminal and visit JWT. Scroll down to the debugger and past your token in the encoded area.

Our token, when decoded contains a header, payload, and signature. Where the header contains the algorithm and type of token, the payload contains the data, and the signature verifies if our signature matches our SECRET environment variable.

JWT

Encoded

Decoded

Don’t confuse this with encryption. What we’ve done is stored our credentials in an object (json web token) which is an additional layer of security.

// In controllers/user.jsconst logout = async (req, res) => {
try {
res.clearCookie('token').redirect('/users/all');
} catch (err) {
if (err) throw err;
}
};

In the above snippet, we simply remove our cookie from the client and redirect to our /users/all route. Therefore, without the cookie, our user is signed out and not authorized.

// In controllers/user.jsconst signup = async (req, res) => {
try {
let newUser = {
id: uuid(),
password: req.body.password,
hashedPassword: await hashPassword(req.body.password),
username: req.body.username
};
Users.push(newUser);
let token = await createToken(newUser);
console.log(token);
res
.cookie('token', token, cookieOptions)
.status(200)
.redirect('/users/authorized');
} catch (err) {
if (err) throw err;
}
};

In the above, our signup handler:

  1. creates a new user object and pushes the new user to our Users mock database.
  2. creates a token and cookie with our newUser object just like in our login route.

I know I said last error… 🤥but hey we’re all 👨‍💻and we’re not perfect.

// In controllers/user.jsconst cookieCheck = async (req, res) => {
try {
res.send(
'you hit the authorized route, we will need to check your cookies'
);
} catch (err) {
if (err) throw err;
}
};

For now, our cookieCheck handler just sends a message to the client. What we want our cookieCheck handler to do is get the token from the signed cookie and if it exists to validate the token and pull out the user's credentials.

Next, find a user in our mock DB using the credentials from the token and send them to the client. Of course, handle all our errors. We will log specific errors for this exercise but in a real application, these messages should not be here for obvious reasons.

Add the following code and then we will test all of our routes and ensure we are the correct user.

// In controllers/users.jsconst cookieCheck = async (req, res) => {
const { token } = req.signedCookies;
if (token) {
try {
let {
user: { username, hashedPassword }
} = await isValidToken(token);
let user = Users.find(user => user.username === username);
res.send({ username: user.username, password: hashedPassword });
} catch (err) {
if (err) throw err;
}
} else {
res.send({ message: 'Sorry your token has expired.' });
}
};

Using POSTMAN, we will test the following routes:

| METHOD | ROUTE             | RESPONSE                        |
| ------ | ----------------- | ------------------------------- |
| GET | /users/logout | clears cookies and redirects |
| GET | /users/all | displays all users |
| GET | /users/authorized | validates token client |
| POST | /users/login | verifies username/password |
| POST | /users/signup | creates new user inside cookie |

Recap:

  1. Learned what is a Cookies
  2. Understand how to use a JSON Web Token
  3. Learned how to read, research and handle errors

If you enjoyed the article, throw it a 👏🏼 and let me know! Thanks for reading and hope to bring more awesome content to your screens!

--

--