User authentication and authorization in Node.js, Express.js app, using Typescript, Prisma, Zod and JWT
Published: 2024-06-09 00:04:05

Hey everyone! In this article, we're going to learn how to authenticate and authorize users. We're going to use the following tools for the task (Disclaimer: you don't have to use these tools in your code; it's just that I find them convenient to use. Feel free to use whatever tools you like as long as you're comfortable with them):

-Node.js
-Express.js
-Typescript
-Prisma ORM
-Neon (a serverless postgresql database)
-Zod (for validations)
-Bcrypt (for password hashing)
-JWT (for token generation and verification)

We're going to get started by opening the new project folder:

```
mkdir your_project_name
cd your_project_name
npm init -y
```

Now, let's install all the necessary dependencies, so that we don't have to jump back and forth from our code to the terminal:

```
npm i express dotenv cookie-parser bcrypt jsonwebtoken zod @prisma/client
```
  

Next, we'll install required types for above mentioned dependencies (keep in mind those are always installed as dev dependencies):

```
npm i -D typescript ts-node nodemon @types/node @types/express @types/cookie-parser @types/bcrypt @types/jsonwebtoken prisma
```

Finally, let's install the touch cli to create files without leaving the terminal because we're so lazy to create new files on our own:

```
npm i -g touch-cli
```

Before closing the terminal type the following commands to create the source folder in the root directory and app.ts file inside of it:

```
mkdir src
cd src
touch app.ts
cd ..
```

And we're done, yay! We can finally start coding, right?

Welp, no! We're not quite done yet because now we need to set up commands for running our application in `package.json`, create a `.gitignore` file, and list the files and folders that we don't want to upload to GitHub. If we want to bring it any closer to how it's done in real projects, we need to set up ESLint rules and Prettier config to ensure consistent code style, etc. Pretty overwhelming, right? So much for project setup, huh? Well, not to worry, my friend. I got you covered, as I'm going to focus solely on proper project setup in my upcoming article soon. Just you wait! For now, we'll skip that part and jump straight to the code. And we'll continue by setting up the server in our `src/app.ts` file: 


```typescript
import express from "express";
import type { Request, Response, NextFunction } from "express";
import cookieParser from "cookie-parser";


const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());

//Handling not existing routes

app.use((_req: Request, res: Response, _next: NextFunction) => {
  res.status(404).send("Route not found")
});

//Initialize the server
app.listen(3000, () => {
  console.log(`[server]: server is running at 
  http://localhost:3000/api`);
});

```
Done! Now, let's move on to the prisma setup:
In the terminal, type the following commands:

```
npx prisma
npx prisma init
```
Explanation for teapots:
The first command invokes the prisma cli and the second generates a new folder in the root directory of you project called prisma with `prisma.schema` file inside. It also generates .env file with the fake connection string for the database:

```typescript
DATABASE_URL="postgresql://janedoe:mypassword@localhost:5432/mydb?schema=sample"
```

Let's open the `prisma.schema` file generated by prisma cli, but before that, if you're using VSCode make sure to go to your editor extensions tab and download the following extension:

![Prisma extension in VSCode](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rdq3v7ddwjib58qflzek.png)

This essentially allows you to have colorful code in your `prisma.schema` file. Yeah, you just downloaded the extension just to unlock colorful text in your .schema file `¯\_(ツ)_/¯`, let's gooo. And now, it should look like this: 

![prisma.schema file](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jsdorb1pj5e2l1t08hpv.png)

Now, I'm not going to explain every detail here because, for starters, this article would become much longer if I did. Second, I think everything in this file is pretty much self-explanatory. If anything is unclear, just read the documentation; that's what it's there for, right? So, to keep things short, this is essentially your Prisma configuration file, where your database and provider are set up. In our case, the provider is "postgresql." If you want to use other relational or non-relational databases such as SQLite or MongoDB, you'll need to replace "postgresql" and specify the type of database you're using.

Going back to where we left off, this `prisma.schema` file can also be used to define our entities and their relationships. We will use this file for that purpose from now on. So, let's define our User entity and Roles (which we will use later for user authorization):

![updated prisma.schema file with User model and Role enum](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zxz17pujhb9ts2gb2qq5.png)

Done! Now, let's open the terminal and run our first migration:

```
npx prisma migrate dev --name init
```

Install the prisma client:

```
npm install @prisma/client
```

And we're ready to connect our database. Let's GOOOO`ᕙ(  •̀ ᗜ •́  )ᕗ`! Open your src folder and create new folder called config and in there create db.ts file:

```
cd src
mkdir config
cd config
touch db.ts
cd ../..
```

And it's here that we will define the function which will connect us to the database:

```typescript
import { PrismaClient } from "@prisma/client"

export const db = new PrismaClient()

export async function connectToDB() {
  try {
     await db.$connect();
     console.log("[database]: connected!");
   } catch (err) {
     console.log("[database]: connection error: ", err);
     await db.$disconnect();
   }
}
```

We are going to call this function whenever we run our application server. So, open the `src/app.ts` file and make the following changes:

```typescript
//the rest of the code...
const initializeApp = async () => {
try {
   app.listen(3000, () => {
    console.log(`[server]: server is running at http://localhost:3000/api`);
   });
   await connectToDB();
 } catch (err) {
   console.error(err);
   process.exit(1);
 } 
}

initializeApp()
```

Next, let's test the connection. BUT before that, we MUST replace the fake database connection string provided by Prisma in our `.env` file. For this, you'll have to go to the official [Neon](https://www.google.com/url?sa=t&source=web&rct=j&opi=89978449&url=https://neon.tech/&ved=2ahUKEwjo-Mbcrs2GAxWUDhAIHTGnBF0QFnoECCgQAQ&usg=AOvVaw2V-qc7rmEG-pLkc_6mJupK) website. There, all you have to do is sign up, create a new project with a free plan, and get the newly generated connection string for your serverless database. Finally, replace the fake connection string with the one generated for you by Neon:

```typescript
DATABASE_URL="postgresql://your:credentials@localhost:5432/db_name?schema=sample"
```
And, we're good to go! Open the terminal, run `npm run dev`, and see if you're getting the following messages:

```
[database]: connected!
[server]: server is running at http://localhost:3000/api
```

If you're not receiving these messages, then you definitely missed something. Retrace your steps to see if you skipped any steps or made any mistakes. Once everything is working correctly, we can proceed.

Before we return to coding, let me briefly introduce a couple of programming concepts that we will implement in this project later down the line.

Firstly, in our project, we're going to separate our logic into the following layers: **repositories**, **services**, and **controllers**. This pattern is commonly referred to as the **Service Layer Pattern** or **Service-Oriented Architecture (SOA)**. This approach is widely used in designing enterprise applications, particularly for web applications and APIs. Here's what each layer is responsible for:

**Repository** (bottom layer): The repository serves as a data access layer, essentially providing a class through which you can access the database and perform necessary queries.

**Service** (middle layer): The service layer handles the business logic. It receives data from the request, processes it, and if you need to save something to the database or retrieve it, you do so by using your repository. This is achieved via **dependency injection**.

**Controller** (top layer): The controller handles requests and responses. It receives the request and passes the data to the service layer. Again, how do we access the service layer? Via dependency injection! The service processes the provided data, and if it returns something, you extract it and send an appropriate response to the client.

As for **dependency injection**, this is esentiallly a technique that aims to facilitate separation of concerns, which leads to loosely coupled programs. Why do we need it?
1. Improved Testability
2. Flexibility and Maintainability
3. Enhanced Readability.
4. Code Reusability

If you need a deeper explanation of these concepts, I'll write a separate article for that, just let me know in the comments.

Now that we have familiarized ourselves with these concepts, it'll be much easier for us to grasp what we're going to be doing in the next steps.

**Step 1: Build the Layers**
Let's build up our layers from the bottom to the top. We'll start by creating the repository layer. Open the terminal and type the following:

```
cd src
mkdir repositories
cd repositories
touch UserRepository.ts
cd ../..
```

Close the terminal, open the `UserRepository.ts` file that you have just created, and write the following:

```typescript
import { db } from "../config/db";
import type { Prisma } from "@prisma/client";

export default class UserRepository {
	private readonly db;
	constructor() {
		this.db = db;
	}
	async getAll() {
		return await this.db.user.findMany();
	}
	async getById(id: string) {
		return await this.db.user.findUnique({ where: { id } });
	}
	async getByKey(key: keyof Prisma.UserWhereInput, value: Prisma.UserWhereInput[keyof Prisma.UserWhereInput]) {
		return await this.db.user.findFirst({ where: { [key]: value } });
	}

	async create(data: Prisma.UserCreateInput) {
		return await this.db.user.create({ data });
	}

	async update(id: string, data: Prisma.UserUpdateInput) {
		return await this.db.user.update({ where: { id }, data });
	}
	async delete(id: string) {
		return await this.db.user.delete({ where: { id } });
	}
}

```
As you can see, there is no business logic involved here, just plain and simple database queries. Let's move on to the next layer: the service layer. Open the terminal and paste the following commands:

```
cd src
mkdir services
cd services
touch UserService.ts
cd ../..
```
Done! Open the `UserService.ts` file and paste this:

```typescript
import type { Prisma } from "@prisma/client";
import bcrypt from "bcrypt";
import UserRepository from "../repositories/UserRepository";

const userRepository = new UserRepository();

export default class UserService {
  private userRepository: UserRepository;
  constructor() {
    //this is how dependency injection is done
    //we're injecting UserRepository into UserService
    this.userRepository = userRepository;
  }
  async getAll() {
    return await this.userRepository.getAll();
  }
  async getById(id: string) {
    const user = await this.userRepository.getById(id);
    if (!user) throw new Error("user not found");
    return user;
  }
  async getByKey(key: keyof Prisma.UserWhereInput, value: Prisma.UserWhereInput[typeof key]) {
    return await this.userRepository.getByKey(key, value);
  }
  async create(data: any) {
    const hashedPassword = await bcrypt.hash(data.password, 10);
    return await this.userRepository.create({ ...data, password: hashedPassword });
  }
  async update(id: string, data: any) {
    await this.getById(id);
    if (data.password)
      data.password = await bcrypt.hash(data.password, 10);
    return await this.userRepository.update(id, data);
  }
  async delete(id: string) {
    await this.getById(id);
    await this.userRepository.delete(id);
  }
}

```

In this code snippet we're going to temporarily use built-in Error constructor, but we will replace it with our own custom error later on. On to the controller layer!

```
cd src
mkdir controllers
cd controllers
touch UserController.ts
cd ../..
```

Open the `UserController.ts` file:

```typescript
import type { NextFunction, Request, Response } from "express";
import UserService from "../services/UserService";

const userService = new UserService();
export default class UserController {
  private readonly userService: UserService;
  constructor() {
    this.userService = userService;
  }
  async getAll(_req: Request, res: Response, next: NextFunction) {
    try {
      const users = await this.userService.getAll();
      res.status(200).json({ users });
    } catch (err) {
      next(err);
    }
  }
  async getById(req: Request, res: Response, next: NextFunction) {
    try {
      const user = await this.userService.getById(req.params.id);
      res.status(200).json({ user });
    } catch (err) {
      next(err);
    }
  }
  async create(req: Request, res: Response, next: NextFunction) {
    try {
      const user = await this.userService.create(req.body);
      res.status(201).json({ user });
    } catch (err) {
      next(err);
    }
  }
  async update(req: Request, res: Response, next: NextFunction) {
    try {
      const user = await this.userService.update(req.params.id, req.body);
      res.status(200).json({ user });
    } catch (err) {
      next(err);
    }
  }
  async delete(req: Request, res: Response, next: NextFunction) {
    try {
      await this.userService.delete(req.params.id);
      res.sendStatus(204);
    } catch (err) {
      next(err);
    }
  }
}
```

Notice that we're not implementing getByKey method from the service since we created that method only to use it inside of our application logic.

**Step 2: Error Handling**
We'll start by creating our own HttpExceptions:

```
cd src
mkdir utils
cd utils
touch HttpExceptions.ts
cd ../..
```

Open the `HttpExceptions.ts` file and paste this:

```typescript
export abstract class CustomError extends Error {
  abstract readonly statusCode: number;
  abstract readonly errors: string[];
  abstract readonly isLogging: boolean;
  constructor(message: string) {
    super(message);
    Object.setPrototypeOf(this, CustomError.prototype);
  }
}

export class HttpException extends CustomError {
  readonly _statusCode: number;
  readonly _isLogging: boolean;
  constructor(
    statusCode = 500,
    message = "Something went wrong",
    isLogging = false
  ) {
    super(message);
    this._statusCode = statusCode;
    this._isLogging = isLogging;
    Object.setPrototypeOf(this, HttpException.prototype);
  }
  get errors() {
    return [this.message];
  }
  get statusCode() {
    return this._statusCode;
  }
  get isLogging() {
    return this._isLogging;
  }
}

export class HttpValidationExceptions extends CustomError {
  readonly _statusCode = 400;
  readonly _isLogging: boolean;
  readonly _errors: string[];
  constructor(errors = ["Bad Request"], isLogging = false) {
    super("Bad Request");
    this._errors = errors;
    this._isLogging = isLogging;
    Object.setPrototypeOf(this, HttpValidationExceptions.prototype);
  }
  get errors() {
    return this._errors;
  }
  get statusCode() {
    return this._statusCode;
  }
  get isLogging() {
    return this._isLogging;
  }
}

```

So, first, we've built a CustomError absctract class which we then used to create our own HttpExceptions errors: one for common errors and other is for validation errors.

Next, let's create an ErrorHandler middleware that will catch these errors and send them in the proper format:

```
cd src
mkdir middlewares
cd middlewares
touch ErrorHandler.ts
cd ../..
```

Open the `ErrorHandler.ts`: 

```typescript
import type { Request, Response, NextFunction } from "express";
import { CustomError } from "../utils/HttpExceptions";
import { Prisma } from "@prisma/client";

const ErrorFactory = (err: Error, res: Response) => {
  if (err instanceof CustomError) {
    const { statusCode, stack, isLogging, errors } = err;
    if (isLogging) {
      const logMessage = JSON.stringify({ statusCode, errors, stack }, null, 2);
      console.log(logMessage);
    }
    return res.status(statusCode).send({ errors });
  }
  if (err instanceof Prisma.PrismaClientKnownRequestError) {
    console.log(JSON.stringify(err, null, 2));
    return res.status(400).send({ errors: [{ message: "Bad Request" }] });
  }
  return null;
};

const ErrorHandler = (err: Error, _req: Request, res: Response, _next: NextFunction) => {
  const handledError = ErrorFactory(err, res);
  if (!handledError) {
    console.log(JSON.stringify(`Unhandled error: ${err}`, null, 2));
    return res
      .status(500)
      .send({ errors: [{ message: "Internal server error" }] });
  }
};

export default ErrorHandler;

```

So, our ErrorHandler middleware uses ErrorFactory to handle various error types, including Prisma errors. If error isn't handled by ErrorFactory, then we send `Internal server error` with status code of 500.

Let's use this middleware globally. Go to `src/app.ts`

```typescript
import ErrorHandler from "./middlewares/ErrorHandler"
import { HttpException } from "./utils/HttpExceptions"

//the rest of the code...
//Handling not existing routes
app.use((_req: Request, _res: Response, next: NextFunction) => {
  next(new HttpException(404, "Route not found"));
});
//Error handling
app.use(ErrorHandler);

const initializeApp = async () => {
//the rest of the code...
```

Now we can use our own custtom error class, so let's implement it inside of the `UserService.ts`: 

```typescript
//the rest of the code...
async getById(id: string) {
    const user = await this.userRepository.getById(id);
    if (!user) throw new HttpException(404, "User not found");
    return user;
  }
//the rest of the code...
```

And with that, we can create our user routes. Open the terminal and paste the following:

```
cd src
mkdir routes
cd routes
touch AppRoutes.ts
touch UserRoutes.ts
cd ../..
```

In the `UserRoutes.ts` file:

```typescript
import { Router } from "express";
import UserController from "../controllers/UserController";

const userController = new UserController();
const router = Router()

router
  .get("/", userController.getAll.bind(userController))
  .get("/:id", userController.getById.bind(userController))
  .post("/", userController.create.bind(userController))
  .patch("/:id", userController.update.bind(userController))
  .delete(
    "/:id", userController.delete.bind(userController)
  );

export { router as UserRoutes };
```

Next in the `AppRoutes.ts` file:

```typescript
import { Router } from "express";
import { UserRoutes } from "./UserRoutes";

const router = Router();
router.use("/users", UserRoutes);

export { router as AppRoutes }
```

Let's import and use AppRoutes inside of our `src/app.ts` file:

```typescript
import { AppRoutes } from "./routes/AppRoutes"
//Routes
app.use("/api", AppRoutes);
//Handling not existing routes
app.use((_req: Request, _res: Response, next: NextFunction) => {
  next(new HttpException(404, "Route not found"));
});
//Error handling
app.use(ErrorHandler);

```

**Step 3: Validation**

Now that we have user routes and error handling middleware, we can start implementing validations. Let's define our validation schemas using Zod. Open the terminal:

```
cd src
mkdir validations
cd validations
touch UserValidations.ts
cd ../..
```

Inside of `UserValidations.ts`:

```typescript
import { Role } from "@prisma/client";
import { z } from "zod";

const phoneRegex = new RegExp(/^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/);

export const createUserSchema = z.object({
  fname: z.string().min(1, { message: "Must contain at least 1 character" }),
  lname: z.string().min(1, { message: "Must contain at least 1 character" }),
  phone: z.string().regex(phoneRegex, "Must be a valid phone number"),
  email: z.string().email({ message: "Must be a valid email address" }),
  password: z.string().min(6, { message: "Must be at least 6 characters long" }),
  roles: z.array(z.enum([Role.ADMIN, Role.MANAGER, Role.USER])).optional(),
  refreshToken: z.string().optional(),
});

export type CreateUserInput = z.infer<typeof createUserSchema>;

export const updateUserSchema = createUserSchema.partial();

export type UpdateUserInput = z.infer<typeof updateUserSchema>;

export const registerUserSchema = createUserSchema.omit({
  roles: true,
  refreshToken: true,
});

export type RegisterUserInput = z.infer<typeof registerUserSchema>;

export const loginUserSchema = registerUserSchema.omit({ fname: true, lname: true }).extend({
  phone: z.string().regex(phoneRegex, "Must be a valid phone number").optional(),
  email: z.string().email({ message: "Must be a valid email address" }).optional(),
});

export type LoginUserInput = z.infer<typeof loginUserSchema>;
```

Implement the types at `UserService.ts`:

```typescript
import type { Prisma } from "@prisma/client";
import bcrypt from "bcrypt";
import UserRepository from "../repositories/UserRepository";
import { HttpException } from "../utils/HttpExceptions";
import type { CreateUserInput, UpdateUserInput } from "../validations/UserValidations";

const userRepository = new UserRepository();

export default class UserService {
  private userRepository: UserRepository;
  constructor() {
    this.userRepository = userRepository;
  }
  async getAll() {
    return await this.userRepository.getAll();
  }
  async getById(id: string) {
    const user = await this.userRepository.getById(id);
    if (!user) throw new HttpException(404, "User not found");
    return user;
  }
  async getByKey(key: keyof Prisma.UserWhereInput, value: Prisma.UserWhereInput[typeof key]) {
    return await this.userRepository.getByKey(key, value);
  }
  async create(data: CreateUserInput) {
    const hashedPassword = await bcrypt.hash(data.password, 10);
    return await this.userRepository.create({ ...data, password: hashedPassword });
  }
  async update(id: string, data: UpdateUserInput) {
    await this.getById(id);
    if (data.password)
      data.password = await bcrypt.hash(data.password, 10);
    return await this.userRepository.update(id, data);
  }
  async delete(id: string) {
    await this.getById(id);
    await this.userRepository.delete(id);
  }
}

```

We didn't define zod schemas just to infer types from them. Let's use them to validate requests. Open the terminal:

```
cd src/middlewares
touch ValidateRequest.ts
cd ../..
```

Inside of `ValidateRequest.ts`:

```typescript
import type { Request, Response, NextFunction } from "express";
import { type z, ZodError } from "zod";
import { HttpValidationExceptions } from "../utils/HttpExceptions";

const ValidateRequest = (validationSchema: z.Schema) => {
  return (req: Request, _res: Response, next: NextFunction) => {
    try {
      validationSchema.parse(req.body);
      next();
    } catch (err) {
      if (err instanceof ZodError) {
        const errorMessages = err.errors.map(
        (error) => `${error.path.join(".")} is 
        ${error.message.toLowerCase()}`);
        next(new HttpValidationExceptions(errorMessages));
      }
    }
  };
};
export default ValidateRequest;

```

Usage example at `UserRoutes.ts`:

```typescript
//the rest of the code...
router
  .get("/", userController.getAll.bind(userController))
  .get("/:id", userController.getById.bind(userController))
  .post("/", ValidateRequest(createUserSchema), userController.create.bind(userController))
  .patch("/:id", ValidateRequest(updateUserSchema), userController.update.bind(userController))
  .delete(
    "/:id",
    authMiddleware.verifyPermissions("delete"),
    userController.delete.bind(userController)
  );

export { router as UserRoutes };
```

**Step 4: Repeat**
Now that we have setup our UserController, UserRoutes, UserServices, validations, and error handling we will repeat the same process for our Auth module with its routes, services, controllers and middlewares. It's going to be much easier this time, trust me. Let's start by creating service for JWT.

```
cd src/services
touch JwtService.ts
cd ../..
```

At `JwtService.ts`:

```typescript
import jwt from "jsonwebtoken";
import { HttpException } from "../utils/HttpExceptions";
import dotenv from "dotenv"

dotenv.config();

export type AuthTokens = {
  accessToken: string;
  refreshToken: string;
};

const { ACCESS_TOKEN_SECRET, ACCESS_TOKEN_EXPIRY, REFRESH_TOKEN_SECRET, REFRESH_TOKEN_EXPIRY } = process.env as { [key: string]: string };

export class JwtService {
  genAuthTokens(payload: object): AuthTokens {
    const accessToken = this.sign(payload, ACCESS_TOKEN_SECRET, {
      expiresIn: ACCESS_TOKEN_EXPIRY,
    });
    const refreshToken = this.sign(payload, REFRESH_TOKEN_SECRET, {
      expiresIn: REFRESH_TOKEN_EXPIRY,
    });
    return { accessToken, refreshToken };
  }
  async verify(token: string, secret: string): Promise<jwt.JwtPayload> {
    const decoded: jwt.JwtPayload = await new Promise((resolve, reject) => {
      jwt.verify(token, secret, (err, decoded) => {
        if (err) reject(new HttpException(403, "Forbidden"));
        else resolve(decoded as jwt.JwtPayload);
      });
    });
    return decoded;
  }
  sign(payload: object, secret: string, options?: jwt.SignOptions): string {
    return jwt.sign(payload, secret, options);
  }
}

```
Let's add the above mentioned environmental variables to our .env file. Open the terminal:

```
node
require("crypto").randomBytes(64).toString('hex')
```

(To exit the node, press `Ctrl + C` in the terminal)

These commands will generate a random set of letters and numbers which we'll use as a secret for our jwt token. Run the second command again and generate another secret. Now, we have two. Let's use one for access token secret, and the other for refresh. Open the `.env` file and paste the following:

```typescript
/* other env variables... */
/* paste the generated access secret key here */
ACCESS_TOKEN_SECRET="your_secret"
/* You can change it to hours, e.g: '1h' */
ACCESS_TOKEN_EXPIRY="60s"
/* paste the generated refresh secret key here */
REFRESH_TOKEN_SECRET="your_secret"
/* You can change it to hours, e.g: '1h' */
REFRESH_TOKEN_EXPIRY="1d"
```

Now we will implement the JwtService in our `AuthService.ts`:

```
cd src/services
touch AuthService.ts
cd ../..
```

Open `AuthService.ts`:

```typescript
import type { Role } from "@prisma/client";
import bcrypt from "bcrypt";
import UserService from "./UserService";
import { JwtService, type AuthTokens } from "./JWTService";
import type { LoginUserInput, RegisterUserInput } from "../validations/UserValidations";
import { HttpException } from "../utils/HttpExceptions";
import dotenv from "dotenv";

dotenv.config()

const userService = new UserService();
const jwtService = new JwtService();

export default class AuthService {
  private readonly userService: UserService;
  private readonly jwtService: JwtService;
  constructor() {
    this.userService = userService;
    this.jwtService = jwtService;
  }
  async login(data: LoginUserInput): Promise<AuthTokens> {
    let user;
    if (data.phone) user = await this.userService.getByKey("phone", data.phone);
    else user = await this.userService.getByKey("email", data.email);
    if (!user || !(await bcrypt.compare(data.password, user.password)))
      throw new HttpException(400, "Wrong credentials");
    const { email, roles } = user;
    const { accessToken, refreshToken } = this.jwtService.genAuthTokens({ email, roles });
    await this.userService.update(user.id, { refreshToken });
    return { accessToken, refreshToken };
  }
  async register(data: RegisterUserInput): Promise<AuthTokens> {
    const newUser = await this.userService.create(data);
    const { email, roles } = newUser;
    const { accessToken, refreshToken } = this.jwtService.genAuthTokens({ email, roles });
    await this.userService.update(newUser.id, { refreshToken });
    return { accessToken, refreshToken };
  }
  async refresh(refreshToken: string): Promise<{ accessToken: string }> {
    const user = await this.userService.getByKey("refreshToken", refreshToken);
    if (!user) throw new HttpException(403, "Forbidden");
    const decoded = await this.jwtService.verify(
      refreshToken,
      process.env.REFRESH_TOKEN_SECRET as string
    );
    const isRolesMatch = user.roles.every((role: Role) => decoded.roles.includes(role));
    if (decoded.email !== user.email || !isRolesMatch)
      throw new HttpException(403, "Forbidden");
    const { accessToken } = this.jwtService.genAuthTokens({ email: user.email, roles: user.roles });
    return { accessToken };
  }
  async logout(refreshToken: string) {
    const user = await this.userService.getByKey("refreshToken", refreshToken);
    if (user) return await this.userService.update(user.id, { refreshToken: "" });
  }
}

```
So, what is going on here? In our **login** logic, we are first checking whether the user is trying to login via his phone number or email. If we fail to find a user or if we do, but his password doesn't match with the one that we have found, we throw an error, simple as that. If the user's credentials match then we can generate new access and refresh tokens for him. We update the user by saving a new refresh token, which will come in handy when implementing the AuthMiddleware and refresh method. Next, the **registration** logic: we create a new user using our UserService. This is what I was referring to as "Code reusability". See, how we don't even have to write the logic for creating a new user again from scratch. We can simply inject our UserService class inside of the AuthService and reuse some of it's methods to keep the code simpler and shorter. As for the **refresh** logic, we receive the refresh token which will be extracted from req.cookies in AuthController, how you might ask? Well, we're going to store it there when login or registering a new user. So, we extract it from there and check if the user with this refresh token does exist in our database. If he doesn't we throw an error. If he does we decode the refresh token and extract the payload from it. The payload is the user roles and email that we have encrypted to our tokens when generating them. So now, we're checking if the email and roles from the payload match with one's from the user we have found via the refresh token. If they do, then it's the same user who's sending a request for a "refresh" or a new access token. **Logout** logic: in our AuthController we will check if the refresh token set in cookies is still there. If it is, we will send it to our AuthService, where we will find a user with the same refresh token and will set his refresh token to an empty string.

Now we can go ahead and create the AuthController and implement our auth logic there: 

```
cd src/controllers
touch AuthController.ts
cd ../..
```

Open `AuthController.ts`:

```typescript
import type { Request, Response, NextFunction, CookieOptions } from "express";
import AuthService from "../services/AuthService";

const COOKIE_OPTIONS: CookieOptions = {
  httpOnly: true,
  maxAge: 24 * 60 * 60 * 1000,
  sameSite: "none",
  secure: process.env.NODE_ENV === "production",
};

const authService = new AuthService();

export default class AuthController {
  private readonly authService: AuthService;
  constructor() {
    this.authService = authService;
  }
  async login(req: Request, res: Response, next: NextFunction) {
    try {
      const { accessToken, refreshToken } = await this.authService.login(req.body);
      res
        .cookie("jwt", refreshToken, COOKIE_OPTIONS)
        .status(200)
        .send({ accessToken });
    } catch (err) {
      next(err);
    }
  }
  async register(req: Request, res: Response, next: NextFunction) {
    try {
      const { accessToken, refreshToken } = await this.authService.register(req.body);
      res
        .cookie("jwt", refreshToken, COOKIE_OPTIONS)
        .status(201)
        .send({ accessToken });
    } catch (err) {
      next(err);
    }
  }
  async refresh(req: Request, res: Response, next: NextFunction) {
    try {
      const { accessToken } = await this.authService.refresh(req.cookies.jwt);
      res.status(200).send({ accessToken });
    } catch (err) {
      next(err);
    }
  }
  async logout(req: Request, res: Response, next: NextFunction) {
    try {
      const refreshToken = req.cookies.jwt;
      if (!refreshToken) {
        res.sendStatus(204);
        return;
      }
      const user = await this.authService.logout(refreshToken);
      if (user) {
        res.clearCookie("jwt", COOKIE_OPTIONS).sendStatus(204);
        return;
      }
      res.clearCookie("jwt", COOKIE_OPTIONS).sendStatus(204);
    } catch (err) {
      next(err);
    }
  }
}

```
Done! Now, let's define our auth routes:

```
cd src/routes
touch AuthRoutes.ts
cd ../..
```

Open `AuthRoutes.ts`:

```typescript
import { Router } from "express";
import AuthController from "../controllers/AuthController";
import ValidateRequest from "../middlewares/ValidateRequest";
import { loginUserSchema, registerUserSchema } from "../validations/UserValidations";

const router = Router();
const authController = new AuthController();

router
  .post("/login", ValidateRequest(loginUserSchema), authController.login.bind(authController))
  .post(
    "/register",
    ValidateRequest(registerUserSchema),
    authController.register.bind(authController)
  )
  .post("/refresh", authController.refresh.bind(authController))
  .post("/logout", authController.logout.bind(authController));

export { router as AuthRoutes };
```

Done! Now, let's define the Auth middleware to protect our routes from being entered by unauthorized intruders:

```
cd src/middlewares
touch Auth.ts
cd ../..
```

Open `Auth.ts`:

```typescript
import type { Request, Response, NextFunction } from "express";
import { HttpException, HttpStatusCodes } from "../utils/HttpExceptions";
import jwt from "jsonwebtoken";
import configuration from "../config/configuration";
import type { Role } from "@prisma/client";
import { getPermissionsByRoles } from "../config/permissions";
import { JwtService } from "../services/JWTService";

export interface AuthRequest extends Request {
  user?: {
    email: string;
    roles: Role[];
  };
}


export default class Auth {
  constructor() {}
  async verifyToken(req: AuthRequest, _res: Response, next: NextFunction): Promise<void> {
  try {
    const { authorization } = req.headers;
    if (!authorization) throw new HttpException(401, "Unauthorized");
    const [type, token] = authorization.split(" ");
    if (type !== "Bearer")
      throw new HttpException(401, "Unauthorized");
    const decoded = await new JwtService.verify(token, ACCESS_TOKEN_SECRET);
    req.user = decoded as { email: string; roles: Role[] };
    next();
   } catch (err) {
      next(err)
   }
 }
  verifyRoles(allowedRoles: Role[]) {
    return (req: AuthRequest, _res: Response, next: NextFunction): void => {
      if (!req.user || !req.user?.roles)
        throw new HttpException(403, "Forbidden");
      const hasRoles = req.user.roles.some((role) => allowedRoles.includes(role));
      if (!hasRoles) throw new HttpException(403, "Forbidden");
      next();
    };
  }
  verifyPermissions(permission: string) {
    return (req: AuthRequest, _res: Response, next: NextFunction): void => {
      if (!req.user || !req.user?.roles)
        throw new HttpException(403, "Forbidden");
      const userPermissions = getPermissionsByRoles(req.user.roles);
      if (!userPermissions || !userPermissions.includes(permission))
        throw new HttpException(403, `You are forbidden to ${permission}`);
      next();
    };
  }
}
```

Let's break it down: The authentication middleware executes before the request reaches the controller, acting as a guard to protect routes from unauthorized access. For instance, the **verifyToken** middleware checks if the user has signed in or signed up, as in both cases, the user receives an access token. This token is sent to the frontend and stored, typically in a cookie, session storage, or local storage, and is included in the request headers as an authorization token, e.g., "Bearer access_token" or sometimes "Token access_token". Whenever a user tries to access a protected route or resource, their request must include this token in the authorization header. 

The verifyToken middleware verifies and decodes this token. If the request lacks the token, it is likely that the user is not logged in or has not signed up. If the token is present, it is verified and decoded. If the token is valid (not expired and of the correct type), the user is granted access to the route. Otherwise, a "Forbidden" error is sent. Finally, the middleware sets request.user to the decoded payload (email, roles) extracted from the token during the verification process. verifyRoles Middleware
Next up, we have the **verifyRoles** middleware. As the name implies, this middleware verifies the roles of the user attempting to access a route. It takes an array of allowed roles and checks if the user has any of these roles. If the user has the required roles, they are granted access to the route; otherwise, they are forbidden.

How does verifyRoles work?

This middleware runs after the verifyToken middleware, which sets req.user to the decoded payload from the access token. The payload contains the user's email and roles.
The middleware checks if the user's roles include at least one role from the allowed roles array. If at least one role matches, the user is authorized to proceed. Otherwise, access is denied.

Lastly, we have the **verifyPermissions** middleware. Much like verifyRoles, this middleware relies on the information provided in the access token and accesses user roles. Its purpose however, is to verify whether the user has roles that grant the specified permission.

How does verifyPermissions work?

Like verifyRoles, this middleware operates after the verifyToken middleware.
The "allowed permission" is passed as an argument to verifyPermissions. The middleware extracts the user's roles from req.user and uses a function getPermissionsByRoles (which we haven't yet defined but we will in a moment) to determine the permissions associated with these roles.
If the user's permissions include the specified permission, they are permitted to access the route. Otherwise, access is denied.

To sum up, these middlewares enhance security by ensuring that users have the appropriate roles and permissions to access specific routes or perform certain actions.

To define the permissions and getPermissionsByRoles function do the following: 

```
cd src/config
touch permissions.ts
cd ../..
```

Open `permissions.ts`: 

```typescript
import { Role } from "@prisma/client";

type Permissions = {
  [key: string]: {
    [key: string]: string;
  };
};

export const permissions: Permissions = {
  basic: {
    read: "read",
    create: "create",
    update: "update",
    delete: "delete",
  },
};

const userPermissions = [permissions.basic.read];
const managerPermissions = [...userPermissions, permissions.basic.create, permissions.basic.update];
const adminPermissions = [...managerPermissions, permissions.basic.delete];

const permissionsByRole = {
  [Role.USER]: userPermissions,
  [Role.MANAGER]: managerPermissions,
  [Role.ADMIN]: adminPermissions,
};

export const getPermissionsByRoles = (roles: Role[]) => {
  const permissionsSet = new Set<string>();
  roles.forEach((role) => {
    permissionsByRole[role].forEach((permission) => {
      permissionsSet.add(permission);
    });
  });
  const permissions = Array.from(permissionsSet);
  if (permissions.length === 0) return null;
  return permissions;
};

```

For now, we will keep the permissions simple and straightforward and store them as basic permissions. You can always define your own permissions by expanding the permissions object. Currently, our users are only allowed to "read", while managers and admins can perform any actions except managers can not "delete".

To showcase the use of our Auth middleware let's go ahead and use it to protect the UserRoutes.
At `AppRoutes.ts`:

```typescript
import { Router } from "express";
import { UserRoutes } from "./UserRoutes";
import { AuthRoutes } from "./AuthRoutes";
import Auth from "../middlewares/Auth";
import { Role } from "@prisma/client";

const router = Router();
const authMiddleware = new Auth();
router.use(
  "/users",
  authMiddleware.verifyToken,
  authMiddleware.verifyRoles([Role.MANAGER, Role.ADMIN]),
  UserRoutes
);
router.use("/auth", AuthRoutes);
export { router as AppRoutes };
```

At `UserRoutes.ts`:

```typescript
import { Router } from "express";
import UserController from "../controllers/UserController";
import ValidateRequest from "../middlewares/ValidateRequest";
import { createUserSchema, updateUserSchema } from "../validations/UserValidations";
import Auth from "../middlewares/Auth";

const userController = new UserController();
const authMiddleware = new Auth();
const router = Router();

router
  .get("/", userController.getAll.bind(userController))
  .get("/:id", userController.getById.bind(userController))
  .post("/", ValidateRequest(createUserSchema), userController.create.bind(userController))
  .patch("/:id", ValidateRequest(updateUserSchema), userController.update.bind(userController))
  .delete(
    "/:id",
    authMiddleware.verifyPermissions("delete"),
    userController.delete.bind(userController)
  );

export { router as UserRoutes };
```

Done! Let's test our api to check if everything is working properly and we can finally wrap this whole thing up.

![Postman testing of auth/register route](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rrv4932tbdz6w5gimops.png)

We have just registered a new user! Now let's take this access token and set it in authorization headers. Click on "Headers" tab if you're also testing the api in Postman and set the authorization header as shown in image below:

![Setting authorization headers](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fk7qjrgvjq9trys3zsvs.png)

Set the route to `http://localhost:3000/api/users` and the method to `GET` and click on `Send`:

![Our Api sent us error "Forbidden"](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9sbiobufgd8ojvkes35z.png)

And we got an error, as we were supposed to since the expiry date of our access token is set to 60 seconds and I'm pretty sure that by the time all of you guys reading this article got to the point of pasting the token in headers and clicked on send, the minute has long passed. So, try it again following the same instructions or simply go to your `.env` file and prolong the expiry date of your access token. Let's try it again:

![The Forbidden error again](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ml9exy60q6djc5upjs15.png)

We're still getting an error. That is because we only allow admins or managers to access the user routes. Let's temporarily loosen our security and comment out the role verification.

![Comment verifyRoles middleware applied on user routes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/izrz784vl9e1wvu9rx7g.png)

Now, get the new token and try to access the same `http://localhost:3000/api/users` route:

![The route returned our users](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d5sjftx2bib4b1ngxdo2.png)

Copy any user's id and change the url as follows `http://localhost:3000/api/users/your_user_id` and change the method to `DELETE`, then `Send` (don't forget to refresh your token):

![Delete failed since our user is neither manager nor admin](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tbwhquhcisyu7flbvq2p.png)

There you go. Our "verifyPermissions" middleware has worked properly. Since our user is not an admin he isn't permitted to delete other users. You can now uncomment the verifyRoles middleware at `AppRoutes.ts`.

Congratulations! We have implemented user authentication and authorization in our backend express api. This was a long read for sure, but I hope you guys enjoyed it. If you did, pls be sure to leave a like and sub for more content and if you have any suggestions or complaints regarding the code or implementations then please make sure to inform me in the comments below. Good luck on your programming journey! Bye!