Create Authentication system with NodeJS, ExpressJS, TypeScript and Jest E2E testing -- PART 2.
JWT Middleware
Let's create the middleware to handle the authentications. Create this file src/middlewares/jwt.middleware.ts
.
This middleware takes the Authorization
header (Bearer [token]) and get the token. jsonwebtoken
packages handles the process of validating the token behind the scene.
If the token doesn't exist, the access is denied (401 Error). Else, we validated the token, the package handles the token and return an error if it's expired or invalid (in case not expired, then it's invalid). Otherwise, it's valid, and we move on to the next handler (The route handler probably).
import { Response, NextFunction } from 'express';
import jwt, { TokenExpiredError } from 'jsonwebtoken';
import { ResponseHandler } from '@/utils/responseHandler';
import appConfig from '@/config/app.config';
export function authenticateJWT(req: IRequest, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (authHeader) {
const token = authHeader.split(' ')[1];
jwt.verify(token, appConfig.jwt.secret, (err, user) => {
if (err || !user) {
if (err instanceof TokenExpiredError) {
const resBody = ResponseHandler.Unauthorized('Unauthenticated');
res.status(resBody.error!.code).json(resBody);
} else {
const resBody = ResponseHandler.Forbidden('Access forbidden: Invalid token');
res.status(resBody.error!.code).json(resBody);
}
return;
}
req.user = user;
next();
});
} else {
const resBody = ResponseHandler.Unauthorized('Access denied: No token provided');
res.status(resBody.error!.code).json(resBody);
}
}
The IRequest
interface is a globally declared interface, it extends default Express Request, and add user payload (userId
). We can use this userId
in other route handler to get the logged in user. Create src/index.d.ts
// Refrence files, to import the types from other definition files. We will be importing them here instead of importing the types in each file.
/// <reference path="./schemas/auth/auth.schema.d.ts" />
/// <reference path="./types/auth.d.ts" />
import { Request as ExpressRequest } from 'express';
declare global {
interface IRequest extends ExpressRequest {
user?: any;
}
}
Auth Router
Router
As I mentioned in the Architecture section, the code will be separated, the router will only handle routing and listening. The logic will be in repositories files. Let's create the file src/routes/v1/auth.route.ts
import { NextFunction, Request, Response, Router } from 'express';
import { validate } from '@/middlewares/validateRequest.middleware';
import { AuthRepository } from '@/repositories/auth.repo';
import { authenticateJWT } from '@/middlewares/jwt.middleware';
import HttpStatusCode from '@/utils/HTTPStatusCodes';
import { AuthZODSchema } from '@/schemas/auth.schema';
const AuthRoutes = Router();
AuthRoutes.post(
'/login',
validate(AuthZODSchema.authSchema),
async (req: Request, res: Response, next: NextFunction) => {
try {
const body: TAuthSchema = req.body;
const resBody = await AuthRepository.loginUser(body);
res.status(resBody.error ? resBody.error.code : HttpStatusCode.OK).json(resBody);
next();
} catch (err) {
next(err);
}
}
);
AuthRoutes.post(
'/refresh-token',
validate(AuthZODSchema.refreshTokenSchema),
async (req: Request, res: Response, next: NextFunction) => {
try {
const body: TRefreshTokenSchema = req.body;
const resBody = await AuthRepository.refreshToken(body);
res.status(resBody.error ? resBody.error.code : HttpStatusCode.OK).json(resBody);
next();
} catch (err) {
next(err);
}
}
);
AuthRoutes.post(
'/register',
validate(AuthZODSchema.registerSchema),
async (req: Request, res: Response, next: NextFunction) => {
try {
const body: TRegisterSchema = req.body;
const resBody = await AuthRepository.createUser(body);
res.status(resBody.error ? resBody.error.code : HttpStatusCode.OK).json(resBody);
next();
} catch (err) {
next(err);
}
}
);
AuthRoutes.post(
'/forget-password',
validate(AuthZODSchema.forgetPasswordSchema),
async (req: Request, res: Response, next: NextFunction) => {
try {
const body: TForgetPasswordSchema = req.body;
const resBody = await AuthRepository.forgotPassword(body);
res.status(resBody.error ? resBody.error.code : HttpStatusCode.OK).json(resBody);
next();
} catch (err) {
next(err);
}
}
);
AuthRoutes.post(
'/reset-password',
validate(AuthZODSchema.resetPasswordSchema),
async (req: Request, res: Response, next: NextFunction) => {
try {
const body: TResetPasswordSchema = req.body;
const resBody = await AuthRepository.resetPassword(body);
res.status(resBody.error ? resBody.error.code : HttpStatusCode.OK).json(resBody);
next();
} catch (err) {
next(err);
}
}
);
AuthRoutes.post(
'/update-password',
authenticateJWT,
validate(AuthZODSchema.updatePasswordSchema),
async (req: IRequest, res: Response, next: NextFunction) => {
try {
const body: TUpdatePasswordSchema = req.body;
// We get the userId from the request. It's passed by "authenticateJWT"
const resBody = await AuthRepository.updatePassword(body, req.user.userId);
res.status(resBody.error ? resBody.error.code : HttpStatusCode.OK).json(resBody);
next();
} catch (err) {
next(err);
}
}
);
AuthRoutes.post(
'/confirm-update-password',
validate(AuthZODSchema.validateUserSchema),
async (req: Request, res: Response, next: NextFunction) => {
try {
const body: TValidateUserSchema = req.body;
const resBody = await AuthRepository.confirmUpdatePassword(body);
res.status(resBody.error ? resBody.error.code : HttpStatusCode.OK).json(resBody);
next();
} catch (err) {
next(err);
}
}
);
AuthRoutes.post(
'/verify-user',
validate(AuthZODSchema.validateUserSchema),
async (req: Request, res: Response, next: NextFunction) => {
try {
const body: TValidateUserSchema = req.body;
const resBody = await AuthRepository.verifyUser(body);
res.status(resBody.error ? resBody.error.code : HttpStatusCode.OK).json(resBody);
next();
} catch (err) {
next(err);
}
}
);
export default AuthRoutes;
This router depends on 3 other files; The types and ZOD schema, the ZOD validator middleware, and the Auth Repo.
ZOD Validator Middleware.
All middlewares are injected in src/middlewares
directory. So, let's create the middleware. Create validateRequest.middleware.ts
file.
import { Request, Response, NextFunction } from 'express';
import { ZodError, ZodSchema } from 'zod';
import { ResponseHandler } from '@/utils/responseHandler';
function parseZodErrors(errors: ZodError) {
return errors.errors.map((err) => `${err.path.join(', ')}: ${err.message}`);
}
export function validate(schema: ZodSchema) {
return (req: Request, res: Response, next: NextFunction) => {
try {
schema.parse(req.body);
next();
} catch (error: any) {
const resBody = ResponseHandler.InvalidBody({
message: 'Validation Error',
errors: parseZodErrors(error),
});
res.status(resBody.error!.code).json(resBody);
}
};
}
This middleware is simple. ZOD validates the schema passed through the middleware in Router handler. If it's valid, move to next handler. Otherwise, return a 422 response with the ZOD error to know which property isn't valid and what's the error. That's the role of parseZodErrors
. It parses the ZodErrors
to the ApiResponseBody
message property.
ZOD Schemas
I used a class with static ZOD schema object to have a clean import section. And used .d.ts
to declare the types. So, we got two files; the schema objects file, and type definitions file. Create a file src/schemas/auth/auth.schema.ts
import { z } from 'zod';
export class AuthZODSchema {
static readonly authSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
type: z.enum(['DOCTOR', 'PATIENT', 'ADMIN']),
});
static readonly registerSchema = z.object({
name: z.string().min(2),
phone: z.string().refine((phone) => /^\+\d{10,15}$/.test(phone), 'Invalid phone number'),
email: z.string().email(),
password: z.string().min(8),
type: z.enum(['DOCTOR', 'PATIENT']),
});
static readonly refreshTokenSchema = z.object({
refreshToken: z.string().uuid(),
});
static readonly forgetPasswordSchema = z.object({
email: z.string().email(),
type: z.enum(['DOCTOR', 'PATIENT', 'ADMIN']),
});
static readonly resetPasswordSchema = z.object({
newPassword: z.string().min(8),
token: z.string().uuid(),
});
static readonly validateUserSchema = z.object({
token: z.string().uuid(),
});
static readonly updatePasswordSchema = z.object({
oldPassword: z.string().min(8),
newPassword: z.string().min(8),
type: z.enum(['DOCTOR', 'PATIENT', 'ADMIN']),
});
}
Now create the definition file src/schemas/auth/auth.schema.d.ts
import { AuthZODSchema } from './auth.schema';
import { z } from 'zod';
declare global {
type TAuthSchema = z.infer<typeof AuthZODSchema.authSchema>;
type TRegisterSchema = z.infer<typeof AuthZODSchema.registerSchema>;
type TForgetPasswordSchema = z.infer<typeof AuthZODSchema.forgetPasswordSchema>;
type TResetPasswordSchema = z.infer<typeof AuthZODSchema.resetPasswordSchema>;
type TUpdatePasswordSchema = z.infer<typeof AuthZODSchema.updatePasswordSchema>;
type TValidateUserSchema = z.infer<typeof AuthZODSchema.validateUserSchema>;
type TRefreshTokenSchema = z.infer<typeof AuthZODSchema.refreshTokenSchema>;
}
This definition is already declared in index.d.ts
. We won't need to import each type in each file we use.
Setting up prisma
Before we start with the repository, we need to create the prisma models and migrate them. Create the schema in prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum UserType {
DOCTOR
PATIENT
ADMIN
}
model User {
id String @id @unique @default(uuid())
name String
email String @unique
phone String @unique
password String
verifiedEmail Boolean @default(false)
userType UserType
refreshToken RefreshToken[]
resetPasswordToken ResetPasswordToken?
updatePasswordToken UpdatePasswordToken?
verifyEmailToken VerifyEmailToken?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("users")
}
model ResetPasswordToken {
id String @id @unique @default(uuid())
token String @unique
expiresAt DateTime
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("reset_password_tokens")
}
model UpdatePasswordToken {
id String @id @unique @default(uuid())
token String @unique
newPassword String
expiresAt DateTime
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("update_password_tokens")
}
model VerifyEmailToken {
id String @id @unique @default(uuid())
token String @unique
expiresAt DateTime
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("verify_email_tokens")
}
model RefreshToken {
id String @id @default(uuid())
token String @unique
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
expiresAt DateTime
createdAt DateTime @default(now())
@@map("refresh_tokens")
}
Now we migrate our schema. Make sure you created the DB and added the URL in .env
npx prisma migrate dev --name init
And because we will handle also the TEST DB, I created a script to migrate the migrations in that DB. Create the file in prisma/scripts/migrate.ts
.
import { execSync } from 'child_process';
import { config } from 'dotenv';
config()
async function runMigrations() {
try {
console.log('Running migrations...');
execSync(`cross-env DATABASE_URL=${process.env.TEST_DATABASE_URL} npx prisma migrate deploy`, {
stdio: 'inherit',
});
console.log('Migrations completed successfully.');
} catch (error) {
console.error('Error running migrations:', error);
process.exit(1);
}
}
runMigrations()
Install cross-env
before, and make sure you installed ts-node
like mentioned before.
npm i -D cross-env
After that, you can run the script
npx ts-node prisma/scripts/migrate.ts
Subscribe to my newsletter
Read articles from Mehdi Jai directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Mehdi Jai
Mehdi Jai
Result-oriented Lead Full-Stack Web Developer & UI/UX designer with 5+ years of experience in designing, developing and deploying web applications.