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
andpassword
) and create a new user record. While saving the password, we usebcrypt
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 theemail
andpassword
from 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
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 theverify()
method from thejsonwebtoken
library. This method takes theaccessToken
from the cookie as input along with the secret key. Note that while returning the user details, we strip off thepassword
property from the object.The
Refresh
Function - When the user’s access token expires, therefreshToken
helps 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 theaccessToken
cookie.The
Logout
Function - As the name suggests, this function simply sets theaccessToken
andrefreshToken
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.
Thank you for this great and clear guide, it was just what needed! - I will follow your articles with great interest!