NodeJS Express Login Authentication with JWT and MySQL

NodeJS Express Login Authentication with JWT and MySQL

Using both access token and refresh token

In today’s post, we will create a NodeJS Express login system for authenticating users to our application. The high-level points we will cover in this post are as follows:

  • Register users and store their information and credentials in a MySQL database. We will use TypeORM for connecting our application to MySQL database.
  • Create a Login API to authenticate users and issue access token and refresh token. Basically, we will issue JWT tokens with expiry limits.
  • Create an API for returning the details of currently authenticated user based on the token stored in the cookie.
  • Setup the API endpoint to refresh the access token using the refresh token issued earlier
  • Implement logout endpoint to wipe off the access token and refresh token from the cookie.

Disclaimer: Please note that the purpose of this post is purely educational with the view to develop an understanding of the login process in a NodeJS Express application for beginner-level. Examples have been made intentionally simple to focus on the concept. Therefore, do not treat this as production-ready code that can simply be used in a real world application without sufficient management of security issues based on the application needs.

1 - Package Installation

The application requires a bunch of packages. I will try to break down the various packages based on their overall functionality within the application.

Firstly, we initialize a NodeJS project using the below command.

$ npm init -y

Next, we install a few basic packages to get started with a Node Express application.

$ npm i express cors cookie-parser

Since we will be using Typescript for this application, we will also install a few dependencies to support type definitions. Since these are development dependencies, hence we use the -D flag.

$ npm i -D @types/express @types/cors @types/cookie-parser nodemon typescript

For database related functionality, we need to install the below package.

$ npm i typeorm reflect-metadata mysql2

Next, we have to install the typescript definitions for NodeJS.

$ npm i -D @types/node

For encrypting passwords before storing in MySQL, we will use the bcrypt package and its corresponding type definitions.

$ npm i bcryptjs
$ npm i -D @types/bcryptjs

Lastly, for handling JWTs in our application, we need to install the jsonwebtoken package.

$ npm i jsonwebtoken
$ npm i -D @types/jsonwebtoken

This is how our dependencies documentation appears in the package.json file.

"dependencies": {
    "bcryptjs": "^2.4.3",
    "cookie-parser": "^1.4.6",
    "cors": "^2.8.5",
    "express": "^4.18.1",
    "jsonwebtoken": "^8.5.1",
    "mysql2": "^2.3.3",
    "reflect-metadata": "^0.1.13",
    "typeorm": "^0.3.10"
},
"devDependencies": {
    "@types/bcryptjs": "^2.4.2",
    "@types/cookie-parser": "^1.4.3",
    "@types/cors": "^2.8.12",
    "@types/express": "^4.17.14",
    "@types/jsonwebtoken": "^8.5.9",
    "@types/node": "^18.7.23",
    "nodemon": "^2.0.20",
    "typescript": "^4.8.4"
}

2 - Typescript Configuration

Since we are using Typescript to write our application code, we need to create the Typescript configuration file.

This file will be created in the root project directory and we have to name it tsconfig.json.

See below the contents of the file.

{
    "compilerOptions": {
        "target": "es2016",
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true,
        "module": "commonjs",
        "esModuleInterop": true,
        "forceConsistentCasingInFileNames": true,
        "strict": true,
        "skipLibCheck": true
    }
}

Each property in this file has a specific use. For example, experimentalDecorators allows us to enable the use of decorators in our Typescript file. We will use them when we declare the entity definitions.

3 - NodeJS TypeORM MySQL Configuration

For the database connectivity, we need to create another configuration file for TypeORM. This file will also be in the root project directory and we have to name it ormconfig.json.

See below:

{
    "type": "mysql",
    "host": "localhost",
    "port": 3306,
    "username": "root",
    "password": "password",
    "database": "node_auth_demo",
    "entities": [
        "src/entity/*.ts"
    ],
    "logging": false,
    "synchronize": true
}

As you can see, we specify the database type as mysql. Also, we provide information about the database host details and also, the credentials for connection.

The entities property in the configuration JSON specifies the location of the entity definition files for our application.

4 - Creating the User Entity

With the configuration part out of the way, we can now focus on actually building the application.

Let us start with the entity definition for storing our user data. If not already done, create a src folder in your root project directory. Within the src folder, we create another folder entity.

Inside the entity folder, we create the entity definition file named user.entity.ts.

import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";

@Entity()
export class User {
    @PrimaryGeneratedColumn()
    id!: number;

    @Column()
    name!: string;

    @Column({
        unique: true
    })
    email!: string;

    @Column()
    password!: string;
}

Notice the special decorators in the User class. The @Entity() decorator tells TypeORM to create a table named user in the specified MySQL database. We also use other decorators such as @PrimaryGeneratedColumn() for the primary key field and @Column() decorator for other fields. All of these decorators are imported from the typeorm package we installed earlier.

The exclamation mark (!) after every field name is to suppress the warning to initialize the fields.

5 - Creating the Controller Class

Time to write the core logic of our application. To reiterate, the core logic consists of the following features:

  • register users
  • login
  • fetch authenticated user
  • refresh the token
  • logout

Within the src folder, we create another folder named controller. In the controller folder, we create a file named auth.controller.ts.

import { Request, Response } from "express";
import { getRepository } from "typeorm";
import { User } from "../entity/user.entity";
import bcryptjs from 'bcryptjs';
import { sign, verify } from 'jsonwebtoken';

export const Register = async (req: Request, res: Response) => {
    const { name, email, password } = req.body;

    const user = await getRepository(User).save({
        name,
        email,
        password: await bcryptjs.hash(password, 12)
    })

    res.send(user);
}

export const Login = async (req: Request, res: Response) => {
    const { email, password } = req.body;

    const user = await getRepository(User).findOne({
        where: {
            email: email
        }
    });

    if (!user) {
        return res.status(400).send({
            message: 'Invalid Credentials'
        })
    }

    if (!await bcryptjs.compare(password, user.password)) {
        return res.status(400).send({
            message: 'Invalid Credentials'
        })
    }

    const accessToken = sign({
        id: user.id
    }, "access_secret", {expiresIn: 60 * 60});

    const refreshToken = sign({id: user.id
    }, "refresh_secret", {expiresIn: 24 * 60 * 60 })

    res.cookie('accessToken', accessToken, {
        httpOnly: true,
        maxAge: 24 * 60 * 60 * 1000 //equivalent to 1 day
    });

    res.cookie('refreshToken', refreshToken, {
        httpOnly: true,
        maxAge: 7 * 24 * 60 * 60 * 1000 //equivalent to 7 days
    })

    res.send({
        message: 'success'
    });
}

export const AuthenticatedUser = async (req: Request, res: Response) => {
    try {
        console.log(req.cookies);
        const accessToken = req.cookies['accessToken'];

        const payload: any = verify(accessToken, "access_secret");

        if(!payload) {
            return res.status(401).send({
                message: 'Unauthenticated'
            })
        }

        const user = await getRepository(User).findOne({
            where: {
                id: payload.id
            }
        });

        if (!user) {
            return res.status(401).send({
                message: 'Unauthenticated'
            })
        }

        const {password, ...data} = user;

        res.send(data);

    }catch(e) {
        console.log(e)
        return res.status(401).send({
            message: 'Unauthenticated'
        })
    }
}

export const Refresh = async (req: Request, res: Response) => {
    try {
        const refreshToken = req.cookies['refreshToken'];

        const payload: any = verify(refreshToken, "refresh_secret");

        if (!payload) {
            return res.status(401).send({
                message: 'unauthenticated'
            })
        }

        const accessToken = sign({
            id: payload.id,
        }, "access_secret", { expiresIn: 60 * 60 })

        res.cookie('accessToken', accessToken, {
            httpOnly: true,
            maxAge: 24 * 60 * 60 * 1000 //equivalent to 1 day
        });

        res.send({
            message: 'success'
        })

    }catch(e) {
        return res.status(401).send({
            message: 'unauthenticated'
        })
    }
}

export const Logout = async (req: Request, res: Response) => {
    res.cookie('accessToken', '', {maxAge: 0});
    res.cookie('refreshToken', '', {maxAge: 0});
}

This is a pretty big file that implements all the features. Let us walk-through every feature implementation.

  • The Register Function - In this function, we accept the incoming user data (name, email and password) and create a new user record. While saving the password, we use bcrypt library to hash the password. It is not advisable to store plain passwords in the database.
  • The Login Function - As the name suggests, this function contains the logic for logging in a user. We receive the email and password from the incoming request body. Using the email, we fetch the user from the database and if the passwords match, we create an access token using the sign() method. The sign() method takes a payload (user id), a secret key and expiry duration of the token as input. We also generate the refresh token using the same method. Also, before sending a success response, we set the access token and refresh token to the cookies using the res.cookie() function.
  • The AuthenticatedUser Function - In this function, we use the access token of the caller to extract the payload (user id). Using this id, we fetch the user data from the database and return the same to the caller. To extract the payload, we have to use the verify() method from the jsonwebtoken library. This method takes the accessToken from the cookie as input along with the secret key. Note that while returning the user details, we strip off the password property from the object.
  • The Refresh Function - When the user’s access token expires, the refreshToken helps in providing a fresh access token. In this function, we basically extract the payload from the refresh token of the caller using the verify() method as before. However, this time, we generate another access token using the sign() method and set it to the accessToken cookie.
  • The Logout Function - As the name suggests, this function simply sets the accessToken and refreshToken cookies to empty strings. In other words, the user is logged out of the application.

6 - Creating the Express Router

With the core logic out of the way, we can now create a dedicated router class for routing requests to the controller functions.

To do so, create a file named routes.ts within the src folder.

import { Router } from "express";
import { AuthenticatedUser, Login, Logout, Refresh, Register } from "./controller/auth.controller";

export const routes = (router: Router) => {
    router.post('/api/register', Register)
    router.post('/api/login', Login)
    router.get('/api/user', AuthenticatedUser)
    router.post('/api/refresh', Refresh)
    router.get('/api/refresh', Logout)
}

Basically, we are mapping the API paths to the corresponding controller functions. For example, the path /api/register is mapped to the Register function.

Note that routes for registering user, logging in the user and refreshing the token use the HTTP method POST. The other routes use GET.

7 - The Main File (index.ts)

Finally, we can create the main file or the index.ts file of our application. This file needs to be created in the `src folder.

import cookieParser from 'cookie-parser';
import express from 'express';
import cors from 'cors';
import { createConnection } from 'typeorm';
import { routes } from './routes';

createConnection().then(() => {
    const app = express();

    app.use(express.json());
    app.use(cookieParser());
    app.use(cors({
        origin: ['http://localhost:3000', 'http://localhost:8080', 'http://localhost:4200'],
        credentials: true
    }));

    routes(app);

    app.listen(8000, () => {
        console.log('Listening to port 8000');
    })
})

After the various imports, we call the createConnection() function from typeorm. Based on the configuration in the ormconfig.json file, this function tries to establish a connection with the MySQL server.

If the connection is successful, we create an Express instance and add the various middleware functions such as cookieParser, json and cors. Also, attach the routes to the application instance and finally, start the application on port 8000.

To start the application, we will add the below start script in the package.json file.

"scripts": {
    "start": "nodemon src/index.ts"
},

We can now start the application using the command npm run start and access the various APIs on http://localhost:8000.

Testing the application is a matter of following the sequence of the user authentication process.

  • Register a new user using /api/register endpoint.
  • Once the user is registered, try logging in with the user by calling the /api/login endpoint.
  • If login is successful, call the /api/user endpoint to fetch the details of the authenticated user.
  • Next, try refreshing the access token using /api/refresh endpoint.
  • Lastly, logout from the application using /api/logout endpoint.

The code for this post is available on Github in case you want to play around with it further.

How did you find this post? Was it helpful in explaining the concept? Have you been using JWTs in your applications? If yes, how has been the experience?

Please do share your views and experiences in the comments section below as it helps everyone learn from each other. Also, if the post was useful, do share it with your friends and colleagues.

Did you find this article valuable?

Support Saurabh Dashora by becoming a sponsor. Any amount is appreciated!