This tutorial is an introduction point to understand how simple it is to build a JWT authentication system with Node.js and Express.js without using third-party libraries like Passport.js and services like Google Firebase or Auth0. The backend is built using Express.js for the server, TypeORM for Data access layer, and the routing-controllers library to create Routes and Controllers using Decorators
Welcome back for another tutorial where I will explain how to secure your REST API without using the Passport.js library. This tutorial is an introduction point to understand how the authentication and authorization flow is performed behind the scenes by third-party libraries like Passport.js and services like Google Firebase or Auth0, as there are some developers that don’t really understand the process of authentication.
Recently I was working on a personal project where I had to implement the JWT authentication and authorization functionality. I come to a decision to realize it without the help of Passport.js or any other third-party library, so I can discover by myself how the JSON Web Token Authentication can be achieved.
The backend is built using Express.js for the server, TypeORM for Data access layer, and the routing-controllers library to create Routes and Controllers using Decorators in the same approach of Spring Boot.
In this tutorial we will not see how to setup Typescript with an Express.js project, so it is supposed that you already know how to do it. Or you can use the TypeORM CLI tool to generate a Typescript application, after installing TypeORM globally. They have a very detailed Documentation you can refer to.
In this post we will focus essentially on the Authentication.
Requirements 📋
After setting up the Node.js project with Express.js, Typescript, TypeORM, and routing-controllers, install the following dependencies using this command.
npm install -s helmet jsonwebtoken bcryptjs
#or
yarn add helmet jsonwebtoken bcryptjs
helmet: Adds different HTTP headers to secure our application.
jsonwebtoken: Provides the jwt operations like generation and verification of token.
bcryptjs: Used to hash user passwords.
Since we are working with Typescript, let’s install the type definitions for our dependencies and save them as dev dependencies.
npm install -D @types/helmet @types/jsonwebtoken @types/bcryptjs
#or
yarn add @types/helmet @types/jsonwebtoken @types/bcryptjs -D
Installing the type check dependencies will give us the opportunity to use the autocomplete and typecheck features of Typescript.
Project Structure 🧬
Create an Authentication Controller 👮
Inside the controllers directory create a typescript file AuthController.ts
which will hold the Signin, and Signup process for users, as well as some other secured methods to test if the JWT authentication is working.
@JsonController('/auth')
export class AuthController {}
Create a Sign-Up Method 🔓
@JsonController('/auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post("/signup")
async signUp(@EntityFromBody() user: User) {
const password = user.password;
const passwordHashed = this.hashPassword(password);
user.password = passwordHashed;
let savedUser: User = await this.userService.create(user);
const token = this.generateJwt(savedUser);
return {
user: {
userId: savedUser.id, username:savedUser.username, role:savedUser.role
},
token
};
}
hashPassword(password: string) {
password = bcrypt.hashSync(password, 12);
return password;
}
}
As you can see here the signup method hash the user password before storing user details in the database, then it generates the JSON Web Token and returns it as a result.
Create a Sign-In Method 🔐
@JsonController('/auth')
export class AuthController {
@Post("/signin")
async signIn(@BodyParam("username") username: string, @BodyParam("password") password: string) {
//Get user from database
let user: User;
try {
user = await this.userService.getWithPassword(username);
} catch (error) {
throw new UnauthorizedError("Username incorrect")
}
//Check if encrypted password match
if (!this.isPasswordCorrect(password, user.password)) {
throw new UnauthorizedError("Password Incorrect")
}
//Sing JWT, valid for 1 hour
const token = this.generateJwt(user);
return {
user: {
userId: user.id, username:user.username, role:user.role
},
token
};
}
isPasswordCorrect(password: string, savedPassword: string): boolean{
if(bcrypt.compareSync(password, savedPassword)) return true;
else return false;
}
}
When the user wants to login, this is what happens:
- The user sends its credentials (username and password)
- The server tries to find the user in the database using the provided username
- If the user exists, the server compares the sent password and the one stored
- If the comparison is valid, the server sends back a JSON Web Token (JWT) to the user
Then, the JWT key had to be sent with every user’s request to access the protected endpoints.
Generate a JWT in Node.js 🧰
Let’s create the generateJwt
method which will generate the JSON Web Token using the jsonwebtoken library as follow.
import jwt from "jsonwebtoken"
generateJwt(user: User) {
return jwt.sign({data:{userId:user.id, username:user.username, role:user.role}},
config.jwtSecret, { expiresIn: '6h' });
}
A golden rule is to keep your JWT Secret in a safe place; otherwise your whole authentication system will be compromised.
Create an Authentication Middleware Checker 🛡️
Inside the middlewares directory create the CheckJWT.ts
file which will hold the logic that checks if the user is authenticated or not.
import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
import config from "../config/config";
const extractTokenFromHeader = (req: Request) => {
if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') {
return req.headers.authorization.split(' ')[1];
}
}
export const checkJwt = (req: Request, res: Response, next: NextFunction) => {
//extract the jwt token from the Authorization header
const token = extractTokenFromHeader(req);
let jwtPayload;
//Try to validate the token and get data
try {
jwtPayload = jwt.verify(token, config.jwtSecret);
res.locals.jwtPayload = jwtPayload;
} catch (error) {
//If token is not valid, respond with 401 (unauthorized)
res.status(401).send({response: "You should be logged in to access this url"});
return;
}
//We refresh the token on every request by setting another 1h
const {data:{ userId, username, role }} = jwtPayload;
const newToken = jwt.sign({data:{ userId, username, role }}, config.jwtSecret, {
expiresIn: "1h"
});
res.setHeader("Authorization", 'Bearer ' + newToken);
next();
}
This Middleware has to be placed or called before reaching any protected endpoint. The Routing-controllers library provides the @UseBefore()
decorator especially for this purpose. You can place it on the controller level or method level, where you want the middleware to be executed before the requested action.
Create an Authorization Middleware Checker ⛔
The Routing-controllers package also offers the @Authorized()
decorator which help implement an authorization system based on the user’s roles.
So, inside the middlewares directory create the CheckRole.ts
file which will contain the logic that checks if the user has the required role to access an endpoint or not.
import { getCustomRepository } from "typeorm";
import { User } from "../db/entity/User";
import { Action } from "routing-controllers";
import { UserRepository } from "../db/repository/UserRepository";
export const checkRole = async (action: Action, roles: Array) => {
//Get the Express Response object from Routing-controllers Action
let res = action.response;
//Get the user ID stored on the response object by the checkJwt middleware
const id = res.locals.jwtPayload.userId;
//Find user role in the database
const userRepository = getCustomRepository(UserRepository);
let user: User;
try {
user = await userRepository.findOneOrFail(id);
} catch (id) {
return false;
}
if (user && !roles.length)
return true;
//Check if array of authorized roles includes the user's role
if (roles.indexOf(user.role) > -1) return true;
else return false;;
};
For the @Authorized
decorator to work you need to configure the authorizationChecker
option. You can have more details about the Routing-controllers library by referring to their Repository.
createConnection().then(async connection => {
useExpressServer(app, { // register created express server in routing-controllers
controllers: [AuthController],
authorizationChecker: checkRole// and configure it the way you need (controllers, validation, etc.)
});
app.listen(config.port, () => console.log(`Listening on port ${config.port}`));
}).catch(error => console.log("Error: ", error));
Now, you can put the decorator on the controller actions and specify the roles needed to access these endpoints like so @Authorized(["admin","user"])
.
Secure Endpoints and Verify JWT Authentication and Authorization 🧪
Head back to the AuthController
and add a protected endpoint to test if everything is working as expected.
@Get("/protected")
@UseBefore(checkJwt)
async protectedEP() {
let username = res.locals.jwtPayload.data.username;
return `You successfully reached This protected endpoint Mr: ${username}`;
}
If the JWT is valid you will get a response from the server with a message returned by the protectedEP()
method, otherwise you will be Unauthorized to access this endpoint.
@Get("/admin")
@UseBefore(checkJwt)
@Authorized(["admin"])
async adminEP() {
let username = res.locals.jwtPayload.data.username;
return `You successfully reached This protected endpoint Mr: ${username} because you're an admin`;
}
If the authenticated user has the required role, he will receive a message returned from the adminEP()
method, otherwise he will get a Forbidden error message.
Takeaway 📦
While it is always recommended to opt for existing third-party libraries and services for authentication, so we don’t reinvent the wheel and save a huge development time, it is also important to understand the mystery behind JWT authentication and how it works.
In this article we demonstrated how simple it is to build a JWT authentication system with Node.js and Express.js without using third-party libraries like Passport.js or services like Google Firebase.
I hope you have learned something new today, until then stay tuned.