How to Authenticate With JWT.

How to Authenticate With JSON Web Token (JWT)

In this article you are going to learn:

• What JSON Web Token (JWT) is.

• How to create JSON Web Token (JWT).

• How to Authenticate and Authorize With JSON Web Token (JWT)

Introduction

JSON Web Token is a standard for creating and sharing encrypted data.

Structure of a JWT :

A JWT contains a Header, Payload and Signature

Header

The first part of a JWT is the Header, It contains the algorithm that will be used to encrypt and token type.

Here is an example of a header:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

And here is an example of the header in JSON format:

{
  "alg": "HS256",
  "typ": "JWT"
}

Payload

The second part of JWT is the Payload, It contains the claims which are statements about the user and additional data.

Here is an example of a payload:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

And here is an example of the payload in JSON format:

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

Signature

The third or last part of JWT is the signature, It is used to verify the owner of the message and if it wasn't changed along the way, To create the signature the Base64-encoded header, Payload and Secret are taken, then signed along with the algorithm specified in the header.

An example of signature:

SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

And here is how the signature is created:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
 your-256-bit-secret

) secret base64 encoded

Prerequisites

Since we'll be building a Backend Service With Node.js and Express, Knowing All these is an advantage, Though I'll be walking you through it if you don't know them.

• JavaScript

• Express.js

• Some skills in version control

I expect you have Node.js, Any Code Editor, API Testing platform like Postman, MongoDB or any other Database Installed.

Setting up server

Create a file and then name it anything you like I'll name mine fullauth and then initialize the folder with git init command.

Now type code . in your terminal to open the file, this will only work if you are using VsCode.

The next thing we want to do is initialize npm by executing npm init -y in our terminal and we should have our package.json in our folder.

Now we need to create all folders and files we need, we can easily do that with this command:

mkdir config  controller middleware model routes 

touch config/connect.js controller/authcontroller.js middleware/auth.js model/user.js routes/authroute.js .gitignore app.js index.js .env

And our folder should look like this :

fullauth.jpg

Installing dependencies

And now we'll be installing all dependencies we need with this command:

npm i mongoose dotenv express jsonwebtoken bcryptjs nodemon

Now we'll add "dev": "nodemon index.js" to scripts in package.json file and "type": "module", the former is for the server to restart automatically and latter is to enable ES6 modules, our package.json file should look like this:

{
  "name": "fullauth",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "nodemon index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "bcryptjs": "^2.4.3",
    "dotenv": "^16.0.3",
    "express": "^4.18.2",
    "jsonwebtoken": "^8.5.1",
    "mongoose": "^6.7.0",
    "nodemon": "^2.0.20"
  },
  "type": "module"
}

Getting Started

Creating a Node.js Server

Connecting to local MongoDB

We'll add this to connect.js file:

import mongoose from "mongoose";
import "dotenv/config";

const connect = {
    connectToLocalMongoDB : () =>{
        mongoose
          .connect(process.env.MONGO_URI_LOCAL, {
            useNewUrlParser: true,
            useUnifiedTopology: true,
          })
          .then(() => console.log("DB Connected!"))
          .catch((err) => {
            console.log("not able to connect to database"+ err);
          });
    }
}

export default connect;

Adding MONGO_URI_LOCAL to .env file

MONGO_URI_LOCAL=mongodb://127.0.0.1:27017/fullauth

Updating app.js file:

import express from 'express'
import connect from "./config/connect.js";
const app = express()

app.use(
    express.urlencoded({
      extended: false,
    })
  );

app.use(express.json());

connect.connectToLocalMongoDB()

export default app

Starting server with index.js file

We'll add this to our index.js file:

import "dotenv/config";
import app from "./app.js";

const PORT = process.env.PORT || 8080

app.get('/', (req, res) =>{
    res.send("Welcome to Full authentication app")
})

app.listen(PORT, () =>{
    console.log(`app is listening on port ${PORT}`)
})

Adding PORT to .env file

PORT=8080

Now you can run npm run dev to start the server.

Creating a User model in user.js file:

We'll be creating a user model in the user.js file, this is what we'll be calling to perform operations on the database:


import mongoose from "mongoose";

const User = new mongoose.Schema({
    first_name: {type: String, default: null},
    last_name: {type: String, default: null},
    email: {type: String, default: null},
    password: {type: String, default: null},
})

export default mongoose.model("user", User);

Implementing SignUp and SignIn Controllers

We want to implement the signUp controller, the /signup endpoint will be connected to this controller so a new user can register.

What we'll be doing:

  • check if user exists
  • check if any field is empty
  • create a new user and JWT

Importing dependencies in authcontroller.js file :

import "dotenv/config";
import user from "../model/user.js";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";

Creating SignUp controller in authcontroller.js file:

export const signUp = async (req, res) =>{
    const { firstname, lastname, email, password } = req.body

    const oldUser = await user.findOne({email: email})

    if(oldUser){
        return res.status(424).json({
            status: false,
            message: "user already exists"
        })
    }

    if(!(firstname && lastname && email && password)){
        return res.status(424).json({
            status: false,
            message: "all fields are required"
        })
    }

    if( oldUser == null ){
        const encrptedPassword = await bcrypt.hash(password, 12);

        const newUser =  await user.create({
            first_name: firstname,
            last_name: lastname,
            email: email,
            password: encrptedPassword,
        })

        const token = jwt.sign({id: newUser._id, }, process.env.JWT_SECRET_KEY,{expiresIn: "2d"});

        return res.status(200).json({
            status: true,
            message: "Registration successful",
            data: {
                token: token
            }
        })
    }
}

What we did :

  1. We imported all dependencies we'll be using

  2. We Checked if user exists

  3. We Created a new user if they do not exist.

  4. We used Jwt.sign to create a new token, the .sign method takes parameters which includes Payload, this contains the claims which are statements about the user and additional data, the second parameter is the secret key we'll use to create a unique hash and then the last parameter I added is the sign option setting the expiry time to 2 days.

Creating SignIn controller in authcontroller.js file:

export const signIn = async (req, res) =>{
    try {
        const { email, password } = req.body;

        const oldUser = await user.findOne({email: email})

        if(!(email && password)){
            return res.status(424).json({
                status: false,
                message: "all fields are required"
            })
        }

        if( oldUser == null ){
            return res.status(424).json({
                status: false,
                message: "User does not exist"
            })
        }

        if( oldUser && (await bcrypt.compare(password, oldUser.password))){
            const token = jwt.sign({id: oldUser._id, email:email }, process.env.JWT_SECRET_KEY,{expiresIn: "2d"});
            return res.status(200).json({
                status: true,
                message: "Login Successful",
                data:{
                    token: token
                }
            })
        }else{
            return res.status(424).json({
                status: false,
                message: "Incorrect email or password"
            })  
        } 
    } catch (error) {
        return res.status(500).json({
            status: false,
            message: "An error occurred"
        })  
    }
}

What we did :

  1. Check if User exist using email address provided

  2. If user exists and password matches the one in the Database, create a JWT with Id and email as payload , the secret in our .env file as signature and setting the expiry time to 2 days.

  3. Return response that includes the JWT.

Creating a middleware that checks and decode JWT in auth.js file :

import jwt from "jsonwebtoken";

export const verifyToken = async (req, res, next) =>{
    try {
        const token = req.body.token || req.query.token || req.headers['access-token']

        if(!token){
            return res.status(403).json({
                status: false,
                message: `A token is required for authentication`
            })
        }

        const decoded = jwt.verify(token, process.env.JWT_SECRET_KEY)

        req.user = decoded

    } catch (error) {
        return res.status(401).json({
            status: false,
            message: `Invalid Token`
        })
    }
    return next();
}

What we did :

  1. Check for JWT in request Body, Query or Header with ' access-token ' and key.

  2. If JWT is provided decode it with jwt.verify method, this method takes two parameters i.e. the JWT and the secret key that was used to sign it.

  3. Assign the decoded object to req.user so after authentication routes can assess it.

  4. Return " Invalid code " if code is expired or tampered with.

Creating a controller that greets an authenticated user in authcontroller.js file :

export const greetUser = async (req, res) =>{
    try {
        const id = req.user.id;

        const oldUser = await user.findById(id);

        if( oldUser ){
            return res.status(200).json({
                status: true,
                message: `Hello ${oldUser.first_name}`
            })
        }else{
            return res.status(498).json({
                status: false,
                message: `Invalid Id`
            })
        }

    } catch (error) {
        return res.status(500).json({
            status: false,
            message: "An error occurred"
        })  
    }
}

What we did :

  1. Get the id from req.user when user is authenticated

  2. Find user by id from Database

  3. Return response greeting user with name in Database.

Creating and using Routes

The Next thing we want to do is create endpoints, connect them to controllers in authroute.js file and import them in app.js file so it can be accessible.

Creating and Connecting routes in authroute.js file :

import { Router } from "express";
import { signUp, signIn, greetUser} from "../controller/authcontroller.js";
import { verifyToken } from "../middleware/auth.js";
const router = Router();

router.route("/signup").post(signUp);
router.route("/signIn").post(signIn);
router.route("/greet-user").get(verifyToken, greetUser)

export default router;

Import and use routes in app.js file :

import authroute from "./routes/authroute.js"

app.use(authroute)

Your app.js file should look like this :

import express from 'express'
import connect from "./config/connect.js";
import authroute from "./routes/authroute.js"
const app = express()

app.use(
    express.urlencoded({
      extended: false,
    })
  );

app.use(express.json());

app.use(authroute)

connect.connectToLocalMongoDB()

export default app

Testing Endpoints in Postman

Testing Signup Enpoint

Request :

signup req.jpg

Response :

signup res.jpg

Testing Signin Enpoint

Request :

signin req.jpg

Response :

signin res.jpg

Testing greet-user enpoint

Inserting JWT in Header :

Insert header.jpg

Request :

greet req.jpg

Response :

greet res.jpg

Conclusion

So we've come this far and I guess you should have learnt how to

  • Create a JWT
  • Use JWT in signup and signin enpoints
  • Extract JWT from header and Decode it
  • How to use object from JWT when user passes authentication

“Perfection is achieved not when there is nothing more to add, but rather when there is nothing more to take away.” – Antoine de Saint-Exupery

You can learn more about JWT Here

Thanks for Reading.

You can get the complete code Here

Follow me for more backend development tips.