UDERSTANDING JWT -JSONWEBTOKEN
Evans Ansong
Posted on December 23, 2021
In token-based authentication, we store the user’s state on the client. JSON Web Token (JWT) is an open standard (RFC 7519) that defines a way of securely transmitting information between a client and a server as a JSON object. In this blog, I will use tokens and JWT terms to demonstrate how the transmission between the client and the server actually works.
ANATOMY OF JWT
The anatomy of a JWT contains three forms that is the Header , Payload and the Signature . The headers tells the signing algorithm of the token, payload is the data that the JWT contains which can be the users profile and other information and the signature part verifies the authenticity of the parts. The pieces of data are referred to us claims.
Example of JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
The above example uses the Base64 URL-encoded JSON object. It contains information describing the type of token and the signing algorithm used, such as HMAC, SHA256, or RSA.
Example
{
"alg": “RSA”,
"typ": "JWT"
}
The JWT Payload contains claims that are pieces of information asserted about a subject. The claims will contain the statements about the user and any other additional information. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature. Claims will either be registered, public or private.
Example
{
“Username”: “Jackman”,
“Email”: "Jackman@example.com”,
"admin": true
}
Creating the JWT signature involves taking the encoded header, payload, the signing using the signing algorithm specified and a secret key.
Example
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
The above will create a JWT token using the HMAC SHA256 algorithm. This is the signature to verify that the token has not been modified when the secret key is applied on the back-end. Meaning anyone who have the token can see the data of the token unless encrypted but they cannot modify the data in the token unless they have the secrete key which should not be possible since the server is the only thing that have access to the secrete key.
HOW JWT IS USED
- The user logs in ( they send their credentials to the server and the server will check if it's correct and logs in)
- Upon the request, the server verifies the credentials and generates a JWT token containing the user information and sends it back to the client (front-end)
- The front-end then stores the JWT (usually in cookies, sessions or local storage) and includes this JWT whenever it needs privilege access to.
- The server then uses the JWT to verify the validity of the token that the client sent is legit and has not been modified.
But before the front-end send a JWT to the server ,it will look like this in the request header {authentication:'Bearer ’}
SIGNING VRS ENCYPTING TOKENS
Signing proves that the data in the JWT has not been modified, where as encrypting is like preventing the data in the JWT from being seen by third parties. JWT are not encrypted by default.
ADVANTAGES OF JWT (Token-based authentication)
Token based authentication is stateless (all the information is inside the token itself)
They also uses JSON(JavaScript object notation) which is compact and secure compared to others like xml.
The data is stored in the JWT, meaning it can contain any type of data giving the flexibility on what information is to be included in the token.
And Tokens remain valid until they expire or until the data has been modified.
Logout is also simple by destroying the token in the browser without server interaction.
NOTE:The user will have to re-authenticate the tokens when they expire so they have to login every two days or something depending on the expiration date set on the token.
LIMITATIONS
Storing a lot of data in the token makes it huge, which slows the requests.
Token in the client-side might be hijacked by an attacker making it vulnerable to Cross-site scripting (XSS) attacks which often occurs when an outside entity can execute code within your domain by embedding malicious JavaScript on the page to read and compromise the contents of the browser storage.
DEMO
To carry on with this Demo, you must have NodeJs, MongoDB installed on your system, you can install the latest MongoDB version here, latest NodeJs version here,you must also know how to use basic terminal commands and have postman account, you can sign up here.
We will build a simple RESTful API to use JWT to understand how all this works. To begin you need to get the starter template on my GitHub here. Follow the instructions in the README.md to get the server up and running and come back to this demo.
There are a few packages we will need to install :
- Bcrypt to encrypt the user password.
- dotenv to configure the environment variables and,
- Jsonwebtokens
So open the project and install the packages we will need , in the project directory (outside the src directory) run this command :
npm install bcrypt dotenv jsonwebtoken
After these are installed , we will have to configure the signup route to be able to sign up users and get the user information:
touch signUpRoute.js
Good! now let’s import the packages and add the following code to the signUpRoute.js file :
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { getDbConnection } from '../db';
export const signUpRoute = {
path: '/api/signup',
method: 'post',
handler: async (req, res) => {
const { email, password } = req.body;
// this is the name of the database
//which will be created by default
const db = getDbConnection('jwt-demo-db');
// checking to see if the user already exists in the db
const user = await db.collection('users').findOne({ email });
if (user) {
res.sendStatus(409);
};
//if no user exist in the database with such credentials,
//we hash the password and save the user to the db
const passwordHash = await bcrypt.hash(password, 10);
const personalInfo= {
hairColor: 'brown',
favoriteFood: 'rice ',
bio: 'I am the father of all',
};
const result = await db.collection('users').insertOne({
email,
passwordHash,
info: personalInfo,
isAdmin: false,
});
const { insertedId } = result;
// code here
}
};
With this in place, we can now generate json web token for the information that will be send back to the client so they can store and use it. We need to configure the JWT environment variable, so in the top level folder outside the src folder , run this command :
touch .env
This will create a new file for us so we can insert out secret key:
JWT_SECRET=thywillbedone
NOTE : you can change the secrete key to your own
Now we can generate json web token by adding this code just below the inserted id on the signUpRoute.js :
jwt.sign({
id: insertedId,
email,
info: personalInfo,
isAdmin: false,
},
process.env.JWT_SECRET,
{
expiresIn: '2d',
},
(err, token) => {
if (err) {
return res.status(500).send(err);
}
res.status(200).json({ token });
});
Making the complete signUpRoute.js file look like this :
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { getDbConnection } from "../db";
export const signUpRoute = {
path: "/api/signup",
method: "post",
handler: async (req, res) => {
const { email, password } = req.body;
// this is the name of the database which
// will be created by default
const db = getDbConnection("jwt-demo-db");
// checking to see if the user already exists
const user = await db.collection("users").findOne({ email });
if (user) {
res.sendStatus(409);
}
//if no user exist in the database with such credentials,
//we hash the password and save the user to the db
const passwordHash = await bcrypt.hash(password, 10);
const personalInfo = {
hairColor: "brown",
favoriteFood: "rice ",
bio: "I am the father of all",
};
const result = await db.collection("users").insertOne({
email,
passwordHash,
info: personalInfo,
isVerified: false,
});
const { insertedId } = result;
jwt.sign(
{
id: insertedId,
email,
info: personalInfo,
isAdmin: false,
},
process.env.JWT_SECRET,
{
expiresIn: "2d",
},
(err, token) => {
if (err) {
return res.status(500).send(err);
}
res.status(200).json({ token });
}
);
},
};
Great!!!, but we need to add the route to the index file to be able to make it work :
src -> routes -> index.js and add :
import { signUpRoute } from './signUpRoute';
Now add signUpRoute to the routes array :
import { testRoute } from './testRoute';
import { signUpRoute } from './signUpRoute';
export const routes = [
testRoute,
signUpRoute,
];
Don't worry too much about the structures of the routes unless you are curious 👀 It is also another efficient way of structuring our app
Now we can start our server and test our route with postman by sending a POST request to :
http://localhost:8080/api/signup
Firstly we need to make the server pay attention to the environment variable so let's add ( -r dotenv/config) by editting the package.json file scripts section to this:
"scripts": {
"start": "nodemon --exec babel-node -r dotenv/config ./src/server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
So the package.json file should also look like this :
{
"name": "back-end",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "nodemon --exec babel-node -r dotenv/config ./src/server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@babel/core": "7.13.10",
"@babel/node": "7.13.12",
"@babel/preset-env": "7.13.12",
"bcrypt": "^5.0.1",
"dotenv": "^10.0.0",
"express": "4.17.1",
"jsonwebtoken": "^8.5.1",
"mongodb": "3.6.5",
"uuid": "^8.3.2"
},
"devDependencies": {
"nodemon": "2.0.7"
}
}
Now start the server and set the accept parameters in postman to be: raw - json
The data we are expecting will look like this, so add this credentials to the form data:
{
"email":"someone@gmail.com",
"password":"noonecanstopit323"
}
If configuration was successful and you click send, you should get a response like this :
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYwZGNkMzI4MzQwMzQ3NzcxZWVhNmUxMyIsImVtYWlsIjoiZG90QGdtYWlsLmNvbSIsImluZm8iOnsiaGFpckNvbG9yIjoiIiwiZmF2b3JpdGVGb29kIjoiIiwiYmlvIjoiIn0sImlzVmVyaWZpZWQiOmZhbHNlLCJpYXQiOjE2MjUwODQ3MTMsImV4cCI6MTYyNTI1NzUxM30.Hadj4ReKFOj46-H7ua7a6yPAhvE8eenLQ9KNwgzfOcE"
}
What we get back is the token which contains all the data for the new user.
To verify this we can use the jwt.io website to parse the JWT token information. You can use jwt.io to experiment with JSON Web Tokens by decoding and encoding them.
So copy the token and paste it in the box and you should see the token decoded!! containing the exact information we signed with jwt.
This is how the information will be stored on the client side when the user signs in so they can access it and use it however.
We can even verify from the mongo shell that the newly created user was saved to the database.
SUMMARY
Authentication tokens and 2FA play a key role in establishing zero-trust network access control. For example a server could generate a token that has the claim "logged in as administrator" and provide that to a client. The client could then use that token to prove that it is logged in as admin.
Thanks a lot for coming through! I wish you a merry Christmas and a happy new year in advance! Happy coding.
Posted on December 23, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.