.PEM Certificate Files

A .pem file can be used to hold a security certificate. The secret JWT_PRIVATE_KEY, which is held in the server/config/.env file in the previous example, can be held in a .pem file instead.

In order to create a .pem file, we need to install OpenSSL. OpenSSL can be downloaded and installed on Windows from here.

Install the first EXE option, as shown below.

Assuming OpenSSL has been installed in the folder c:\openssl, we need to run the following command:

c:/openssl/bin/openssl genrsa -des3 -out c:\openssl\jwt_private_key.pem 2048

The above command will create a private key called jwt_private_key.pem in the folder c:\openssl

When creating the private key, you will be asked to provide a secret key that is at least four characters long. In a commercial environment, you would safely store a copy of the secret key away from your system.

Create your own .PEM private key certificate.

Once it has been created, move the jwt_private_key.pem file to the project's server/config folder.

We can store the filename of the jwt_private_key.pem file in the /server/config/.env file as the constant JWT_PRIVATE_KEY_FILENAME

The various server-side router files need to be changed, so that they use the jwt_private_key.pem file instead of JWT_PRIVATE_KEY. To achive this, we need to make the following changes in the of the server/routers/users.js router file:

Open the jwt project from the previous section in these notes. Change the code so that it uses a private key that is stored in a .pem file.

"Cars" Worked Example

The full project code for the "Cars" Worked Example that is described below can be downloaded from this link.

In this example, we use a .PEM private key certificate instead of JWT_PRIVATE_KEY.

Client-Side

The client-side is unchanged, as the JWT_PRIVATE_KEY_FILENAME is only visible on the server-side.

Server-Side

server\config\jwt_private_key.pem

The JWT certificate file, jwt_private_key.pem, which was generated using OpenSSL, has to be placed in the project's server\config folder.

server\config\.env

# This file holds global constants that are visible on the Server-side

# Database
DB_NAME = D01234567
DB_HOST = localhost
DB_USER = root
DB_PASS = yourDBpassword


# Access Levels
ACCESS_LEVEL_GUEST = 0
ACCESS_LEVEL_NORMAL_USER = 1
ACCESS_LEVEL_ADMIN = 2


# Keys
JWT_PRIVATE_KEY_FILENAME = ./config/jwt_private_key.pem
JWT_EXPIRY = "7d"


# Salt length of encryption of user passwords
# The salt length should be 16 or higher for commercially released code
# It has been set to 3 here, so that the password will be generated faster
PASSWORD_HASH_SALT_ROUNDS = 3


# Port
SERVER_PORT = 4000


# Local Host
LOCAL_HOST = http://localhost:3000

JWT_PRIVATE_KEY is replaced with JWT_PRIVATE_KEY_FILENAME.

JWT_PRIVATE_KEY_FILENAME holds the location of the jwt_private_key.pem certificate file.

server\routes\users

const router = require(`express`).Router()

const usersModel = require(`../models/users`)

const bcrypt = require('bcrypt')  // needed for password encryption

const jwt = require('jsonwebtoken')
const fs = require('fs')
const JWT_PRIVATE_KEY = fs.readFileSync(process.env.JWT_PRIVATE_KEY_FILENAME, 'utf8')


// IMPORTANT
// Obviously, in a production release, you should never have the code below, as it allows a user to delete a database collection
// The code below is for development testing purposes only 
router.post(`/users/reset_user_collection`, (req,res) => 
{
    usersModel.deleteMany({}, (error, data) => 
    {
        if(data)
        {
            const adminPassword = `123!"£qweQWE`
            bcrypt.hash(adminPassword, parseInt(process.env.PASSWORD_HASH_SALT_ROUNDS), (err, hash) =>  
            {
                usersModel.create({name:"Administrator",email:"admin@admin.com",password:hash,accessLevel:parseInt(process.env.ACCESS_LEVEL_ADMIN)}, (createError, createData) => 
                {
                    if(createData)
                    {
                        res.json(createData)
                    }
                    else
                    {
                        res.json({errorMessage:`Failed to create Admin user for testing purposes`})
                    }
                })
            })
        }
        else
        {
            res.json({errorMessage:`User is not logged in`})
        }
    })                
})


router.post(`/users/register/:name/:email/:password`, (req,res) => {
    // If a user with this email does not already exist, then create new user
    usersModel.findOne({email:req.params.email}, (uniqueError, uniqueData) => 
    {
        if(uniqueData)
        {
            res.json({errorMessage:`User already exists`})
        }
        else
        {
            bcrypt.hash(req.params.password, parseInt(process.env.PASSWORD_HASH_SALT_ROUNDS), (err, hash) =>  
            {
                usersModel.create({name:req.params.name,email:req.params.email,password:hash}, (error, data) => 
                {
                    if(data)
                    {
                        const token = jwt.sign({email: data.email, accessLevel:data.accessLevel}, JWT_PRIVATE_KEY, {algorithm: 'HS256', expiresIn:process.env.JWT_EXPIRY})     
           
                        res.json({name: data.name, accessLevel:data.accessLevel, token:token})
                    }
                    else
                    {
                        res.json({errorMessage:`User was not registered`})
                    }
                }) 
            })
        }
    })         
})


router.post(`/users/login/:email/:password`, (req,res) => 
{
    usersModel.findOne({email:req.params.email}, (error, data) => 
    {
        if(data)
        {
            bcrypt.compare(req.params.password, data.password, (err, result) =>
            {
                if(result)
                {                    
                    const token = jwt.sign({email: data.email, accessLevel:data.accessLevel}, JWT_PRIVATE_KEY, {algorithm: 'HS256', expiresIn:process.env.JWT_EXPIRY})     
           
                    res.json({name: data.name, accessLevel:data.accessLevel, token:token})
                }
                else
                {
                    res.json({errorMessage:`User is not logged in`})
                }
            })
        }
        else
        {
            console.log("not found in db")
            res.json({errorMessage:`User is not logged in`})
        } 
    })
})


router.post(`/users/logout`, (req,res) => {       
    res.json({})
})


module.exports = router

Read the JWT key from the certificate file './config/jwt_private_key.pem' and assign it to the variable JWT_PRIVATE_KEY using the code below.

const JWT_PRIVATE_KEY = fs.readFileSync(process.env.JWT_PRIVATE_KEY_FILENAME, 'utf8')

Whenever a user registers as a new user (which automatically logs the user in) or does a normal login, a new JWT will be created. The JWT will use the JWT_PRIVATE_KEY that was read from the .PEM file in the code above.

router.post(`/users/register/:name/:email/:password`, (req,res) => 
{
                        ...

                        const token = jwt.sign({email: data.email, accessLevel:data.accessLevel}, JWT_PRIVATE_KEY, {algorithm: 'HS256', expiresIn:process.env.JWT_EXPIRY})     
           
                        ...         
})






router.post(`/users/login/:email/:password`, (req,res) => 
{
                    ...
                    
                    const token = jwt.sign({email: data.email, accessLevel:data.accessLevel}, JWT_PRIVATE_KEY, {algorithm: 'HS256', expiresIn:process.env.JWT_EXPIRY})     
           
                    ...
})

Display the value of JWT_PRIVATE_KEY on the server-side console.

server/routes/cars.js

const router = require(`express`).Router()

const carsModel = require(`../models/cars`)

const jwt = require('jsonwebtoken')
const fs = require('fs')
const JWT_PRIVATE_KEY = fs.readFileSync(process.env.JWT_PRIVATE_KEY_FILE, 'utf8')


// read all records
router.get(`/cars`, (req, res) => 
{   
    //user does not have to be logged in to see car details
    carsModel.find((error, data) => 
    {
        res.json(data)
    })
})


// Read one record
router.get(`/cars/:id`, (req, res) => {
    jwt.verify(req.headers.authorization, JWT_PRIVATE_KEY, {algorithm: "HS256"}, (err, decodedToken) => 
    {
        if (err) 
        { 
            res.json({errorMessage:`User is not logged in`})
        }
        else
        {
            carsModel.findById(req.params.id, (error, data) => 
            {
                res.json(data)
            })
        }
    })
})


// Add new record
router.post(`/cars`, (req, res) => 
{
    jwt.verify(req.headers.authorization, JWT_PRIVATE_KEY, {algorithm: "HS256"}, (err, decodedToken) => 
    {
        if (err) 
        { 
            res.json({errorMessage:`User is not logged in`})
        }
        else
        {
            if(decodedToken.accessLevel >= process.env.ACCESS_LEVEL_ADMIN)
            {                
                // Use the new car details to create a new car document
                carsModel.create(req.body, (error, data) => 
                {
                    res.json(data)
                })
            }
            else
            {
                res.json({errorMessage:`User is not an administrator, so they cannot add new records`})
            }
        }
    })
})


// Update one record
router.put(`/cars/:id`, (req, res) => 
{
    jwt.verify(req.headers.authorization, JWT_PRIVATE_KEY, {algorithm: "HS256"}, (err, decodedToken) => 
    {
        if (err) 
        { 
            res.json({errorMessage:`User is not logged in`})
        }
        else
        {
            carsModel.findByIdAndUpdate(req.params.id, {$set: req.body}, (error, data) => 
            {
                res.json(data)
            })        
        }
    })
})


// Delete one record
router.delete(`/cars/:id`, (req, res) => 
{    
    jwt.verify(req.headers.authorization, JWT_PRIVATE_KEY, {algorithm: "HS256"}, (err, decodedToken) => 
    {
        if (err) 
        { 
            res.json({errorMessage:`User is not logged in`})
        }
        else
        {
            if(decodedToken.accessLevel >= process.env.ACCESS_LEVEL_ADMIN)
            {
                carsModel.findByIdAndRemove(req.params.id, (error, data) => 
                {
                    res.json(data)
                })
            }
            else
            {
                res.json({errorMessage:`User is not an administrator, so they cannot delete records`})
            }        
        }
    })
})

module.exports = router

In the same way as we did it for the server/routes/users.js file, we can read the key from the certificate file './config/jwt_private_key.pem' and assign it to the variable JWT_PRIVATE_KEY using the code below.

const JWT_PRIVATE_KEY = fs.readFileSync(process.env.JWT_PRIVATE_KEY_FILE, 'utf8')

Throughout the code, the JWT is always verified against the JWT_PRIVATE_KEY that was read from the certificate file './config/jwt_private_key.pem'.
    jwt.verify(req.headers.authorization, JWT_PRIVATE_KEY, {algorithm: "HS256"}, (err, decodedToken) => 
    {

      ...    
    })

In the above example, a new JWT is only ever created when the user registers or logs in. The user's automatic login duration only lasts from the time of the login or registration. If we update the JWT each time a user user re-opens the app in a new web browser tab, this will means that the JWT's duration will be reset. The DisplayAllCars component's componentDidMount() method is fired when a user re-opens the app in a new web browser tab. Write code to update the JWT every time the user re-opens the app in a new web browser tab.

We can also update the JWT each time the user interacts with the server-side. Write code to update the JWT after every time the user verifies the JWT in the server/routes/cars.js file.

As an extra security step, people who are registering a new account are often sent an activation link that they will need to click on to verify that they are the owner of the email account that is being used to register the new account. The new user's email can be encoded in a JWT, which can be sent as part of the activation link. When the user clicks on the activation link, they should be sent to a screen that asks them to input their email again. This email can be compared against the one in the JWT. If the two emails match, then the user is valid and can be registered. Write code to do this.

 
<div align="center"><a href="../versionC/index.html" title="DKIT Lecture notes homepage for Derek O&#39; Reilly, Dundalk Institute of Technology (DKIT), Dundalk, County Louth, Ireland. Copyright Derek O&#39; Reilly, DKIT." target="_parent" style='font-size:0;color:white;background-color:white'>&nbsp;</a></div>