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 -yNext, 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 typescriptFor database related functionality, we need to install the below package.
$ npm i typeorm reflect-metadata mysql2Next, we have to install the typescript definitions for NodeJS.
$ npm i -D @types/nodeFor encrypting passwords before storing in MySQL, we will use the bcrypt package and its corresponding type definitions.
$ npm i bcryptjs
$ npm i -D @types/bcryptjsLastly, for handling JWTs in our application, we need to install the jsonwebtoken package.
$ npm i jsonwebtoken
$ npm i -D @types/jsonwebtokenThis 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
RegisterFunction - In this function, we accept the incoming user data (name,emailandpassword) and create a new user record. While saving the password, we usebcryptlibrary to hash the password. It is not advisable to store plain passwords in the database.The
LoginFunction - As the name suggests, this function contains the logic for logging in a user. We receive theemailandpasswordfrom the incoming request body. Using theemail, we fetch the user from the database and if the passwords match, we create an access token using thesign()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 theres.cookie()function.The
AuthenticatedUserFunction - 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 theverify()method from thejsonwebtokenlibrary. This method takes theaccessTokenfrom the cookie as input along with the secret key. Note that while returning the user details, we strip off thepasswordproperty from the object.The
RefreshFunction - When the user’s access token expires, therefreshTokenhelps in providing a fresh access token. In this function, we basically extract the payload from the refresh token of the caller using theverify()method as before. However, this time, we generate another access token using thesign()method and set it to theaccessTokencookie.The
LogoutFunction - As the name suggests, this function simply sets theaccessTokenandrefreshTokencookies 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/registerendpoint.Once the user is registered, try logging in with the user by calling the
/api/loginendpoint.If login is successful, call the
/api/userendpoint to fetch the details of the authenticated user.Next, try refreshing the access token using
/api/refreshendpoint.Lastly, logout from the application using
/api/logoutendpoint.
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.


Thank you for this great and clear guide, it was just what needed! - I will follow your articles with great interest!