JSON Web Tokens

JSON Web Tokens (JWT) allows us to transmit JSON data in an encrypted format between the client-side and server-side. JWT can be used instead of server-side sessions to authenticate client-side users whenever they try to access the 'cars' collection or any other server-side resources.

In order to use JWT, you must install jsonwebtoken at the command prompt, as shown below

npm install jsonwebtoken

Install the JWT package

Implementing JWT requires code on both the server-side and the client-side.

Server-Side

On the server-side, we need to be able to:

  1. Create JWT
  2. Verify JWT

 

1. Create JWT

JWT must be signed on the server-side using a secret key. We can store a secret key in process.env.JWT_PRIVATE_KEY.

We can give a time period after which a JWT will expire. We can store the expiry time period in process.env.JWT_EXPIRY

Use the jwt.sign() method to create a new JWT. The jwt.sign() parameters are:

 

2. Verify JWT

We can verify that a JWT is valid using the jwt.verify() method. The jwt.verify() parameters are:

 

JWT are used in place of sessions. Therefore, if we are using JWT, we do not need to hold any server-side session information.

 

Client-Side

When a user registers or logs in on the client-side, the server-side will return a JWT.

The client-side should store the JWT in localStorage, so that it will be available the next time the user opens the application browser window. As a JWT will allow a user to remain logged in the next time they open an application's browser tab, we need to store the JWT in localStorage. All other client-side data that had previously been stored in sessionStorage should now also be stored in localStorage.

Whenever a client-side component wants to query a server-side route, the client-side component will pass the JWT along with the other data that it wants to send to the server-side route. A JWT can be passed with any axios() method by including the headers JSON object below in the axios() method.

{headers:{"authorization":localStorage.token}}

For example, the code below will include the JWT that is stored in localStorage.token in the headers section of the cookie that is being passed by the client-side axios() method.

axios.post(`${SERVER_HOST}/cars`, carObject, {headers:{"authorization":localStorage.token}})

Open the server_session project from the previous section in these notes. Change the code so that it uses a JWT rather than a session variable to hold the user's logged in status.

"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 JWT to authenticate the user.

As we are using a JWT, we do not need to hold any server-side session information.

As a JWT allows a user to remain logged in over time, we replace all instances of sessionStorage with localStorage on the client-side.

As we are not using server-side sessions, we can remove all instances of the axios.defaults.withCredentials = true code below. This code had to be placed in front of every client-side axios() method in the server-side session example that was covered in the previous section of these notes.

axios.defaults.withCredentials = true

Client-Side

client/src/App.js

import React, {Component} from "react"
import {BrowserRouter, Switch, Route} from "react-router-dom"

import "bootstrap/dist/css/bootstrap.css"
import "./css/App.css"

import Register from "./components/Register"
import ResetDatabase from "./components/ResetDatabase"
import Login from "./components/Login"
import Logout from "./components/Logout"
import AddCar from "./components/AddCar"
import EditCar from "./components/EditCar"
import DeleteCar from "./components/DeleteCar"
import DisplayAllCars from "./components/DisplayAllCars"
import LoggedInRoute from "./components/LoggedInRoute"


import {ACCESS_LEVEL_GUEST} from "./config/global_constants"


if (typeof localStorage.accessLevel === "undefined")
{
    localStorage.name = "GUEST"
    localStorage.accessLevel = ACCESS_LEVEL_GUEST
    localStorage.token = null
}

    
export default class App extends Component 
{
    render() 
    {
        return (
            <BrowserRouter>
                <Switch>
                    <Route exact path="/Register" component={Register} />
                    <Route exact path="/ResetDatabase" component={ResetDatabase} />                    
                    <Route exact path="/" component={DisplayAllCars} />
                    <Route exact path="/Login" component={Login} />
                    <LoggedInRoute exact path="/Logout" component={Logout} />
                    <LoggedInRoute exact path="/AddCar" component={AddCar} />
                    <LoggedInRoute exact path="/EditCar/:id" component={EditCar} />
                    <LoggedInRoute exact path="/DeleteCar/:id" component={DeleteCar} />
                    <Route exact path="/DisplayAllCars" component={DisplayAllCars}/> 
                    <Route path="*" component={DisplayAllCars}/>                            
                </Switch>
            </BrowserRouter>
        )
    }
}

On the client-side, we replace all occurances of serverStorage with localStorage.

This will ensure the the data is available the next time that we run the application.

client/src/components/DisplayAllCars.js

import React, {Component} from "react"
import {Link} from "react-router-dom"

import axios from "axios"

import CarTable from "./CarTable"
import Logout from "./Logout"

import {ACCESS_LEVEL_GUEST, ACCESS_LEVEL_ADMIN, SERVER_HOST} from "../config/global_constants"


export default class DisplayAllCars extends Component 
{
    constructor(props) 
    {
        super(props)
        
        this.state = {
            cars:[]
        }
    }
    
    
    componentDidMount() 
    {
        axios.get(`${SERVER_HOST}/cars`)
        .then(res => 
        {
            if(res.data)
            {
                if (res.data.errorMessage)
                {
                    console.log(res.data.errorMessage)    
                }
                else
                {           
                    console.log("Records read")   
                    this.setState({cars: res.data}) 
                }   
            }
            else
            {
                console.log("Record not found")
            }
        })
    }

  
    render() 
    {   
        return (           
            <div className="form-container">
                {
                    localStorage.accessLevel > ACCESS_LEVEL_GUEST 
                    ? <div className="logout">
                        <Logout/>
                      </div>
                    :
                      <div>
                        <Link className="green-button" to={"/Login"}>Login</Link>
                        <Link className="blue-button" to={"/Register"}>Register</Link>  
                        <Link className="red-button" to={"/ResetDatabase"}>Reset Database</Link>  <br/><br/><br/>
                      </div>
                }
                
                <div className="table-container">
                    <CarTable cars={this.state.cars} />                         
                    {
                        localStorage.accessLevel >= ACCESS_LEVEL_ADMIN 
                        ?
                          <div className="add-new-car">
                            <Link className="blue-button" to={"/AddCar"}>Add New Car</Link>
                          </div>
                        :
                          null
                    }
                </div>
            </div> 
        )
    }
}

On the client-side, replace all occurances of serverStorage with localStorage.

This will ensure the the data is available the next time that we run the application.

 

Remove the line of code below from in front of the axios() method, as this line of code is only needed if we are using sever-side sessions.

        axios.defaults.withCredentials = true

client/src/components/AddCar.js

import React, {Component} from "react"
import {Redirect, Link} from "react-router-dom"
import Form from "react-bootstrap/Form"

import axios from "axios"

import Button from "../components/Button"

import {ACCESS_LEVEL_ADMIN, SERVER_HOST} from "../config/global_constants"


export default class AddCar extends Component
{
    constructor(props)
    {
        super(props)

        this.state = {
            model:"",
            colour:"",
            year:"",
            price:"",
            redirectToDisplayAllCars:localStorage.accessLevel < ACCESS_LEVEL_ADMIN
        }
    }


    componentDidMount() 
    {     
        this.inputToFocus.focus()        
    }
 
 
    handleChange = (e) => 
    {
        this.setState({[e.target.name]: e.target.value})
    }


    handleSubmit = (e) => 
    {
        e.preventDefault()

        const carObject = {
            model: this.state.model,
            colour: this.state.colour,
            year: this.state.year,
            price: this.state.price
        }

        axios.post(`${SERVER_HOST}/cars`, carObject, {headers:{"authorization":localStorage.token}})
        .then(res => 
        {   
            if(res.data)
            {
                if (res.data.errorMessage)
                {
                    console.log(res.data.errorMessage)    
                }
                else
                {   
                    console.log("Record added")
                    this.setState({redirectToDisplayAllCars:true})
                } 
            }
            else
            {
                console.log("Record not added")
            }
        })
    }


    render()
    {        
        return (
            <div className="form-container"> 
                {this.state.redirectToDisplayAllCars ? <Redirect to="/DisplayAllCars"/> : null}                                            
                    
                <Form>
                    <Form.Group controlId="model">
                        <Form.Label>Model</Form.Label>
                        <Form.Control ref = {(input) => { this.inputToFocus = input }} type="text" name="model" value={this.state.model} onChange={this.handleChange} />
                    </Form.Group>
    
                    <Form.Group controlId="colour">
                        <Form.Label>Colour</Form.Label>
                        <Form.Control type="text" name="colour" value={this.state.colour} onChange={this.handleChange} />
                    </Form.Group>
    
                    <Form.Group controlId="year">
                        <Form.Label>Year</Form.Label>
                        <Form.Control type="text" name="year" value={this.state.year} onChange={this.handleChange} />
                    </Form.Group>
    
                    <Form.Group controlId="price">
                        <Form.Label>Price</Form.Label>
                        <Form.Control type="text" name="price" value={this.state.price} onChange={this.handleChange} />
                    </Form.Group> 
            
                    <Button value="Add" className="green-button" onClick={this.handleSubmit}/>            
            
                    <Link className="red-button" to={"/DisplayAllCars"}>Cancel</Link>
                </Form>
            </div>
        )
    }
}

On the client-side, replace all occurances of serverStorage with localStorage.

This will ensure the the data is available the next time that we run the application.

 

 

Embed the JWT in the headers parameter of the axios() method.

        axios.post(`${SERVER_HOST}/cars`, carObject, {headers:{"authorization":localStorage.token}})

 

Remove the line of code below from in front of the axios() method, as this line of code is only needed if we are using sever-side sessions.

        axios.defaults.withCredentials = true

client/src/components/EditCar.js

import React, {Component} from "react"
import Form from "react-bootstrap/Form"
import {Redirect, Link} from "react-router-dom"
import axios from "axios"

import Button from "../components/Button"

import {ACCESS_LEVEL_NORMAL_USER, SERVER_HOST} from "../config/global_constants"

export default class EditCar extends Component 
{
    constructor(props) 
    {
        super(props)

        this.state = {
            model: ``,
            colour: ``,
            year: ``,
            price: ``,
            redirectToDisplayAllCars:localStorage.accessLevel < ACCESS_LEVEL_NORMAL_USER
        }
    }

    componentDidMount() 
    {      
        this.inputToFocus.focus()
  
        axios.get(`${SERVER_HOST}/cars/${this.props.match.params.id}`, {headers:{"authorization":localStorage.token}})
        .then(res => 
        {     
            if(res.data)
            {
                if (res.data.errorMessage)
                {
                    console.log(res.data.errorMessage)    
                }
                else
                { 
                    this.setState({
                        model: res.data.model,
                        colour: res.data.colour,
                        year: res.data.year,
                        price: res.data.price
                    })
                }
            }
            else
            {
                console.log(`Record not found`)
            }
        })
    }


    handleChange = (e) => 
    {
        this.setState({[e.target.name]: e.target.value})
    }


    handleSubmit = (e) => 
    {
        e.preventDefault()

        const carObject = {
            model: this.state.model,
            colour: this.state.colour,
            year: this.state.year,
            price: this.state.price
        }

        axios.put(`${SERVER_HOST}/cars/${this.props.match.params.id}`, carObject, {headers:{"authorization":localStorage.token}})
        .then(res => 
        {             
            if(res.data)
            {
                if (res.data.errorMessage)
                {
                    console.log(res.data.errorMessage)    
                }
                else
                {      
                    console.log(`Record updated`)
                    this.setState({redirectToDisplayAllCars:true})
                }
            }
            else
            {
                console.log(`Record not updated`)
            }
        })
    }


    render() 
    {
        return (
            <div className="form-container">
    
                {this.state.redirectToDisplayAllCars ? <Redirect to="/DisplayAllCars"/> : null}  
                        
                <Form>
                    <Form.Group controlId="model">
                        <Form.Label>Model</Form.Label>
                        <Form.Control ref = {(input) => { this.inputToFocus = input }} type="text" name="model" value={this.state.model} onChange={this.handleChange} />
                    </Form.Group>

                    <Form.Group controlId="colour">
                        <Form.Label>Colour</Form.Label>
                        <Form.Control type="text" name="colour" value={this.state.colour} onChange={this.handleChange} />
                    </Form.Group>

                    <Form.Group controlId="year">
                        <Form.Label>Year</Form.Label>
                        <Form.Control type="text" name="year" value={this.state.year} onChange={this.handleChange} />
                    </Form.Group>
        
                    <Form.Group controlId="price">
                        <Form.Label>Price</Form.Label>
                        <Form.Control type="text" name="price" value={this.state.price} onChange={this.handleChange} />
                    </Form.Group>
  
                    <Button value="Update" className="green-button" onClick={this.handleSubmit}/>  
    
                    <Link className="red-button" to={"/DisplayAllCars"}>Cancel</Link>
                </Form>
            </div>
        )
    }
}

On the client-side, replace all occurances of serverStorage with localStorage.

This will ensure the the data is available the next time that we run the application.

 

 

Embed the JWT in the headers parameter of the two axios() methods.

        axios.get(`${SERVER_HOST}/cars/${this.props.match.params.id}`, {headers:{"authorization":localStorage.token}})


        axios.put(`${SERVER_HOST}/cars/${this.props.match.params.id}`, carObject, {headers:{"authorization":localStorage.token}})

 

Remove the line of code below from in front of the axios() method, as this line of code is only needed if we are using sever-side sessions.

        axios.defaults.withCredentials = true

client/src/components/DeleteCar.js

import React, {Component} from "react"
import {Redirect} from "react-router-dom"
import axios from "axios"

import {SERVER_HOST} from "../config/global_constants"


export default class DeleteCar extends Component 
{
    constructor(props) 
    {
        super(props)
        
        this.state = {
            redirectToDisplayAllCars:false
        }
    }
    
    
    componentDidMount() 
    {   
        axios.delete(`${SERVER_HOST}/cars/${this.props.match.params.id}`, {headers:{"authorization":localStorage.token}})
        .then(res => 
        {
            if(res.data)
            {
                if (res.data.errorMessage)
                {
                    console.log(res.data.errorMessage)    
                }
                else // success
                { 
                    console.log("Record deleted")
                }
                this.setState({redirectToDisplayAllCars:true})
            }
            else 
            {
                console.log("Record not deleted")
            }
        })
    }
  
  
    render() 
    {
        return (
            <div>   
                {this.state.redirectToDisplayAllCars ? <Redirect to="/DisplayAllCars"/> : null}                      
            </div>
        )
    }
}

On the client-side, replace all occurances of serverStorage with localStorage.

This will ensure the the data is available the next time that we run the application.

 

 

Embed the JWT in the headers parameter of the axios() method.

        axios.delete(`${SERVER_HOST}/cars/${this.props.match.params.id}`, {headers:{"authorization":localStorage.token}})

 

Remove the line of code below from in front of the axios() method, as this line of code is only needed if we are using sever-side sessions.

        axios.defaults.withCredentials = true

client/src/components/ResetDatabase.js

import React, {Component} from "react"
import {Redirect, Link} from "react-router-dom"
import axios from "axios"

import Button from "../components/Button"
import {SERVER_HOST} from "../config/global_constants"


export default class ResetDatabase extends Component
{
    constructor(props)
    {
        super(props)
        
        this.state = {   
            isReset:false
        } 
    }
    
    
    handleChange = (e) => 
    {
        this.setState({[e.target.name]: e.target.value})
    }
    

    resetUsersModel = () =>
    {
        axios.post(`${SERVER_HOST}/users/reset_user_collection`)
        .then(res => 
        {     
            if(res.data)
            {
                if (res.data.errorMessage)
                {
                    console.log(res.data.errorMessage)    
                }
                else // user successfully reset the User collection
                { 
                    console.log("User collection reset")
                    
                    localStorage.clear()
                }        
            }
            else
            {
                console.log("Failed to reset User collection")
            }
            
            this.setState({isReset:true})
        })   
    }



    render() 
    { 
        return (
            <form className="form-container" noValidate = {true} id = "loginOrRegistrationForm">

               {this.state.isReset ? <Redirect to="/DisplayAllCars"/> : null} 

                <p>"Reset User Database" is only for testing purposes.<br/>All code on the client-side and server-side relating to resetting the database should be removed from any development release</p>
                <Button value="Reset User Database" className="red-button" onClick={this.resetUsersModel}/> <br/><br/>
                <p>Reset the database and set up an administrator with:<br/> * email <strong>admin@admin.com</strong><br/> * password <strong>123!"£qweQWE</strong></p>        
            
                <Link className="red-button" to={"/DisplayAllCars"}>Cancel</Link>
            </form>
        )
    }
}

On the client-side, replace all occurances of serverStorage with localStorage.

This will ensure the the data is available the next time that we run the application.

 

Remove the line of code below from in front of the axios() method, as this line of code is only needed if we are using sever-side sessions.

        axios.defaults.withCredentials = true

client/src/components/Register.js

import React, {Component} from "react"
import {Redirect, Link} from "react-router-dom"
import axios from "axios"

import Button from "../components/Button"

import {SERVER_HOST} from "../config/global_constants"


export default class Register extends Component
{
    constructor(props)
    {
        super(props)
        
        this.state = {
            name:"",
            email:"",
            password:"",
            confirmPassword:"",    
            isRegistered:false
        } 
    }
    
    
    handleChange = (e) => 
    {
        this.setState({[e.target.name]: e.target.value})
    }
    
    
    handleSubmit = (e) => 
    {
        e.preventDefault()

        axios.post(`${SERVER_HOST}/users/register/${this.state.name}/${this.state.email}/${this.state.password}`)
        .then(res => 
        {     
            if(res.data)
            {
                if (res.data.errorMessage)
                {
                    console.log(res.data.errorMessage)    
                }
                else // user successfully registered
                { 
                    console.log("User registered and logged in")
                    
                    localStorage.name = res.data.name
                    localStorage.accessLevel = res.data.accessLevel                    
                    localStorage.token = res.data.token
                    
                    this.setState({isRegistered:true})
                }        
            }
            else
            {
                console.log("Registration failed")
            }
        })   
    }


    render() 
    {     
        return (
            <form className="form-container" noValidate = {true} id = "loginOrRegistrationForm">
           
                {this.state.isRegistered ? <Redirect to="/DisplayAllCars"/> : null} 
            
                <h2>New User Registration</h2>
           
                <input  
                    name = "name"              
                    type = "text"
                    placeholder = "Name"
                    autoComplete="name"
                    value = {this.state.name}
                    onChange = {this.handleChange}
                    ref = {(input) => { this.inputToFocus = input }} 
                /><br/>           

	        <input  
                    name = "email"              
                    type = "email"
                    placeholder = "Email"
                    autoComplete="email"
                    value = {this.state.email}
                    onChange = {this.handleChange}
                /><br/>              

	        <input  
                    name = "password"           
                    type = "password"
                    placeholder = "Password"
                    autoComplete="password"
                    title = "Password must be at least ten-digits long and contains at least one lowercase letter, one uppercase letter, one digit and one of the following characters (£!#€$%^&*)"
                    value = {this.state.password}
                    onChange = {this.handleChange}
                /><br/>           

                <input          
                    name = "confirmPassword"    
                    type = "password"
                    placeholder = "Confirm password"
                    autoComplete="confirmPassword"
                    value = {this.state.confirmPassword}
                    onChange = {this.handleChange}
                /><br/><br/>
                
                <Button value="Register New User" className="green-button" onClick={this.handleSubmit} />
                <Link className="red-button" to={"/DisplayAllCars"}>Cancel</Link>   
            </form>
        )
    }
}

On the client-side, replace all occurances of serverStorage with localStorage.

This will ensure the the data is available the next time that we run the application.

 

Remove the line of code below from in front of the axios() method, as this line of code is only needed if we are using sever-side sessions.

        axios.defaults.withCredentials = true

client/src/components/Login.js

import React, {Component} from "react"
import {Redirect, Link} from "react-router-dom"
import axios from "axios"

import Button from "../components/Button"
import {SERVER_HOST} from "../config/global_constants"


export default class Login extends Component
{
    constructor(props)
    {
        super(props)
        
        this.state = {
            email:"",
            password:"",
            isLoggedIn:false
        }
    }
        
    
    handleChange = (e) => 
    {
        this.setState({[e.target.name]: e.target.value})
    }
    
    
    handleSubmit = (e) => 
    {
        axios.post(`${SERVER_HOST}/users/login/${this.state.email}/${this.state.password}`)
        .then(res => 
        {     
            if(res.data)
            {
                if (res.data.errorMessage)
                {
                    console.log(res.data.errorMessage)    
                }
                else // user successfully logged in
                { 
                    console.log("User logged in")
                    
                    localStorage.name = res.data.name
                    localStorage.accessLevel = res.data.accessLevel  
                    localStorage.token = res.data.token
                    
                    this.setState({isLoggedIn:true})
                }        
            }
            else
            {
                console.log("Login failed")
            }
        })                
    }


    render()
    {            
        return (
            <form className="form-container" noValidate = {true} id = "loginOrRegistrationForm">
                <h2>Login</h2>
                
                {this.state.isLoggedIn ? <Redirect to="/DisplayAllCars"/> : null} 
                
                <input 
                    type = "email" 
                    name = "email" 
                    placeholder = "Email"
                    autoComplete="email"
                    value={this.state.email} 
                    onChange={this.handleChange}
                /><br/>
                    
                <input 
                    type = "password" 
                    name = "password" 
                    placeholder = "Password"
                    autoComplete="password"
                    value={this.state.password} 
                    onChange={this.handleChange}
                /><br/><br/>
                
                <Button value="Login" className="green-button" onClick={this.handleSubmit}/> 
                <Link className="red-button" to={"/DisplayAllCars"}>Cancel</Link>                                      
            </form>
        )
    }
}

On the client-side, replace all occurances of serverStorage with localStorage.

This will ensure the the data is available the next time that we run the application.

 

Remove the line of code below from in front of the axios() method, as this line of code is only needed if we are using sever-side sessions.

        axios.defaults.withCredentials = true

client/src/components/Logout.js

import React, {Component} from "react"
import {Redirect} from "react-router-dom"
import axios from "axios"

import Button from "../components/Button"
import {SERVER_HOST} from "../config/global_constants"


export default class Logout extends Component
{
    constructor(props)
    {
        super(props)
        
        this.state = {
            isLoggedIn:true
        }
    }
    
    
    handleSubmit = (e) => 
    {
        e.preventDefault()
        
        axios.post(`${SERVER_HOST}/users/logout`)
        .then(res => 
        {     
            if(res.data)
            {
                if (res.data.errorMessage)
                {
                    console.log(res.data.errorMessage)    
                }
                else
                { 
                    console.log("User logged out")
                    localStorage.clear() 
                    
                    this.setState({isLoggedIn:false}) 
                }
            }
            else
            {
                console.log("Logout failed")
            }
        }) 
    }


    render()
    {
        return (
            <div>   
        
                {!this.state.isLoggedIn ? <Redirect to="/DisplayAllCars"/> : null} 
                  
                <Button value="Log out" className="red-button" onClick={this.handleSubmit}/> 
            </div>
        )
    }
}

On the client-side, replace all occurances of serverStorage with localStorage.

This will ensure the the data is available the next time that we run the application.

 

Remove the line of code below from in front of the axios() method, as this line of code is only needed if we are using sever-side sessions.

        axios.defaults.withCredentials = true

Server-Side

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 = your_random_JSON_web_token
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 require a secret, server-side key. We replace the SESSION_PRIVATE_KEY, which was used with server-side sessions, with a JWT_PRIVATE_KEY.

JWT_PRIVATE_KEY = your_random_JSON_web_token

JWT have an expiry value. The JWT below will expire in seven days from the time that it is issued.

JWT_EXPIRY = '7d'

server/server.js

// Server-side global variables
require(`dotenv`).config({path:`./config/.env`})


// Database
require(`./config/db`)


// Express
const express = require(`express`)
const app = express()


app.use(require(`body-parser`).json())
app.use(require(`cors`)({credentials: true, origin: process.env.LOCAL_HOST}))


// Routers
app.use(require(`./routes/cars`))
app.use(require(`./routes/users`))


// Port
app.listen(process.env.SERVER_PORT, () => 
{
    console.log(`Connected to port ` + process.env.SERVER_PORT)
})


// Error 404
app.use((req, res, next) => {next(createError(404))})

// Other errors
app.use(function (err, req, res, next)
{
    console.error(err.message)
    if (!err.statusCode) 
    {
        err.statusCode = 500
    }
    res.status(err.statusCode).send(err.message)
})

As we are not using sessions, we can remove the server-side session related code below that was included in the previous section of the notes.

app.use(require(`express-session`)({
secret: process.env.SESSION_PRIVATE_KEY,
resave: false,
cookie: {secure: false, maxAge: 60000},
saveUninitialized: true
}))

server/routes/users.js

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

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

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

const jwt = require('jsonwebtoken')


// 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}, process.env.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}, process.env.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

In order to sign new JWT or verify existing JWT, we need to include the jsonwebtoken library.

const jwt = require('jsonwebtoken')

We need to be able sign JWT with the JWT_PRIVATE_KEY secret key that is held in the .env file. We use the function jwt.sign() to do this.

The JWT signing function - jwt.sign() - consists of three parameters:

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

The JWT is returned to the client-side axios() method along with any other data that is being returned.

res.json({name: data.name, accessLevel:data.accessLevel, token:token})

server/routes/cars.js

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

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

const jwt = require('jsonwebtoken')

// 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, process.env.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, process.env.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, process.env.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, process.env.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

The jwt.verify() function checks if the JWT is valid.

The JWT is availble to the server-side route code as req.headers.authorization.

    jwt.verify(req.headers.authorization, process.env.JWT_PRIVATE_KEY, {algorithm: "HS256"}, (err, decodedToken) => 
    {

    })

If jwt.verify() fails to validate the JWT, then err will contain an error message.

If jwt.verify() successfully validates the JWT, then decodedToken will contain the data that was encrypted when the JWT was created.

We can use decodedToken.accessLevel to check the user's accessLevel. For example, in the code below, only an administrator will be able to execute the code inside the if statement.

    jwt.verify(req.headers.authorization, process.env.JWT_PRIVATE_KEY, {algorithm: "HS256"}, (err, decodedToken) => 
    {

            ...

            if(decodedToken.accessLevel >= process.env.ACCESS_LEVEL_ADMIN)
            {
                // only an administator will be able to execute the code inside this if statement
                ...
            }

            ...

    })

Adjust the code in the jwt.sign() methods in the file server/routes/users.js, so that the JWT expires after one minute. Test this and observe the error message that is returned in the client-side console (F12 in browser) when you refresh the webpage after more than one minute since doing a registration or login.

Remove the expiry property form jwt.sign(). This will result in a JWT that never expires.

In the cars example, the JWT contains the user's email and accessLevel. What do you think is the purpose of storing the user's email in the JWT?

 
<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>