Node.js + TypeScript + MongoDB: JWT Authentication - Phần cuối


Chào các bạn đây là phần 2 cũng là phần cuối của loạt bài viết "Node.js + TypeScript + MongoDB: JWT Authentication", xem lại phần 1 tại đây nhé.
Tạo Authentication Controller
Authentication Controller chịu trách nhiệm về mọi thứ liên quan đến xác thực người dùng. Dưới đây là một số công việc mà controller có thể thực hiện:
Đăng ký người dùng mới
Đăng nhập người dùng vào tài khoản của anh ấy
Gửi email đặt lại mật khẩu cho người dùng quên email hoặc mật khẩu của mình
Đặt lại mật khẩu của người dùng
Cập nhật mật khẩu của người dùng hiện đang đăng nhập
Xác thực bằng Google OAuth
Xác thực bằng GitHub OAuth
...
src/controllers/auth.controller.ts
import config from 'config';
import { CookieOptions, NextFunction, Request, Response } from 'express';
import { CreateUserInput, LoginUserInput } from '../schema/user.schema';
import { createUser, findUser, signToken } from '../services/user.service';
import AppError from '../utils/appError';
// Exclude this fields from the response
export const excludedFields = ['password'];
// Cookie options
const accessTokenCookieOptions: CookieOptions = {
expires: new Date(
Date.now() + config.get<number>('accessTokenExpiresIn') * 60 * 1000
),
maxAge: config.get<number>('accessTokenExpiresIn') * 60 * 1000,
httpOnly: true,
sameSite: 'lax',
};
// Only set secure to true in production
if (process.env.NODE_ENV === 'production')
accessTokenCookieOptions.secure = true;
export const registerHandler = async (
req: Request<{}, {}, CreateUserInput>,
res: Response,
next: NextFunction
) => {
try {
const user = await createUser({
email: req.body.email,
name: req.body.name,
password: req.body.password,
});
res.status(201).json({
status: 'success',
data: {
user,
},
});
} catch (err: any) {
if (err.code === 11000) {
return res.status(409).json({
status: 'fail',
message: 'Email already exist',
});
}
next(err);
}
};
export const loginHandler = async (
req: Request<{}, {}, LoginUserInput>,
res: Response,
next: NextFunction
) => {
try {
// Get the user from the collection
const user = await findUser({ email: req.body.email });
// Check if user exist and password is correct
if (
!user ||
!(await user.comparePasswords(user.password, req.body.password))
) {
return next(new AppError('Invalid email or password', 401));
}
// Create an Access Token
const { accessToken } = await signToken(user);
// Send Access Token in Cookie
res.cookie('accessToken', accessToken, accessTokenCookieOptions);
res.cookie('logged_in', true, {
...accessTokenCookieOptions,
httpOnly: false,
});
// Send Access Token
res.status(200).json({
status: 'success',
accessToken,
});
} catch (err: any) {
next(err);
}
};
Đây là phân tích những gì tôi đã làm trong tệp auth.controller.ts:
Chúng ta có hai hàm, registerHandler và loginHandler .
Khi người dùng cung cấp email, tên và mật khẩu của mình để đăng ký tài khoản, registerHandler sẽ được gọi sau đó registerHandler cũng sẽ gọi service createUser với thông tin xác thực người dùng được yêu cầu.
Service createUser sau đó sẽ giao tiếp với mô hình người dùng để tạo người dùng mới.
Trong khối catch của registerHandler, tôi đã kiểm tra xem lỗi có mã 11000 hay không, đây là mã lỗi MongoDB của một trường duy nhất trùng lặp.
Khi mã lỗi là 11000 thì điều đó có nghĩa là người dùng đã tồn tại, vì vậy chúng tôi sẽ gửi thông báo lỗi và mã trạng thái thích hợp.
Tiếp theo, trong loginHandler, tôi đã kiểm tra xem người dùng có email đó có tồn tại trong cơ sở dữ liệu MongoDB của chúng tôi hay không. Nếu người dùng tồn tại thì chúng tôi kiểm tra xem mật khẩu có giống với mật khẩu được mã hóa trong cơ sở dữ liệu hay không.
Sau đó, chúng tôi tạo mã thông báo truy cập JWT mới và gửi mã đó cho người dùng dưới dạng phản hồi và cookie.
Tạo bộ User Controller để test Authorization
Trong User Contrller có hai function
getMeHandler trả về thông tin hồ sơ người dùng hiện đang đăng nhập.
Hàm getAllUsersHandler chỉ dành cho Quản trị viên để lấy tất cả người dùng.
src/controllers/user.controller.ts
import { NextFunction, Request, Response } from 'express';
import { findAllUsers } from '../services/user.service';
export const getMeHandler = (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const user = res.locals.user;
res.status(200).json({
status: 'success',
data: {
user,
},
});
} catch (err: any) {
next(err);
}
};
export const getAllUsersHandler = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const users = await findAllUsers();
res.status(200).json({
status: 'success',
result: users.length,
data: {
users,
},
});
} catch (err: any) {
next(err);
}
};
Viết hàm để giải mã người dùng
Việc "giải mã" trong ngữ cảnh này là quá trình chuyển đổi một đối tượng User đã được mã hóa thành dạng tệp hoặc chuỗi... trở lại thành đối tượng trong ngôn ngữ lập trình.
Middleware này có trách nhiệm lấy mã thông báo JWT (JWT Authorization bearer token) và cookie từ tiêu đề (headers) và đối tượng cookie tương ứng.
Sau đó, nó xác thực mã thông báo, kiểm tra xem người dùng có phiên hợp lệ hay không, kiểm tra xem người dùng vẫn còn tồn tại và thêm người dùng vào res.locals nếu không có bất kỳ lỗi nào xảy ra.
src/middleware/deserializeUser.ts
import { NextFunction, Request, Response } from 'express';
import { findUserById } from '../services/user.service';
import AppError from '../utils/appError';
import redisClient from '../utils/connectRedis';
import { verifyJwt } from '../utils/jwt';
export const deserializeUser = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
// Get the token
let access_token;
if (
req.headers.authorization &&
req.headers.authorization.startsWith('Bearer')
) {
access_token = req.headers.authorization.split(' ')[1];
} else if (req.cookies.access_token) {
access_token = req.cookies.access_token;
}
if (!access_token) {
return next(new AppError('You are not logged in', 401));
}
// Validate Access Token
const decoded = verifyJwt<{ sub: string }>(access_token);
if (!decoded) {
return next(new AppError(`Invalid token or user doesn't exist`, 401));
}
// Check if user has a valid session
const session = await redisClient.get(decoded.sub);
if (!session) {
return next(new AppError(`User session has expired`, 401));
}
// Check if user still exist
const user = await findUserById(JSON.parse(session)._id);
if (!user) {
return next(new AppError(`User with that token no longer exist`, 401));
}
// This is really important (Helps us know if the user is logged in from other controllers)
// You can do: (req.user or res.locals.user)
res.locals.user = user;
next();
} catch (err: any) {
next(err);
}
};
Định nghĩa một hàm để kiểm tra xem người dùng đã đăng nhập hay chưa
Middleware này sẽ được gọi sau middleware deserializeUser để kiểm tra xem người dùng có tồn tại trong res.locals hay không.
src/middleware/requireUser.ts
import { NextFunction, Request, Response } from 'express';
import AppError from '../utils/appError';
export const requireUser = (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const user = res.locals.user;
if (!user) {
return next(new AppError(`Invalid token or session has expired`, 401));
}
next();
} catch (err: any) {
next(err);
}
};
Định nghĩa một Middleware để Hạn chế Truy cập Trái phép.
Middleware này kiểm tra xem vai trò (role) của người dùng có tồn tại trong mảng allowedRoles hay không. Nếu vai trò của người dùng có trong mảng này, điều đó có nghĩa là người dùng được phép thực hiện hành động đó, ngược lại nó sẽ tạo ra một lỗi.
src/middleware/restrictTo.ts
import { NextFunction, Request, Response } from 'express';
import AppError from '../utils/appError';
export const restrictTo =
(...allowedRoles: string[]) =>
(req: Request, res: Response, next: NextFunction) => {
const user = res.locals.user;
if (!allowedRoles.includes(user.role)) {
return next(
new AppError('You are not allowed to perform this action', 403)
);
}
next();
};
Tạo Authentication Routes
Bây giờ, hãy tạo hai thư mục định tuyến (route) có tên user.route.ts và auth.route.ts trong thư mục src.
Một route trong Express được coi như một ứng dụng nhỏ. Khi một yêu cầu phù hợp với route trong ngăn xếp middleware, Express sẽ ủy quyền yêu cầu đó cho trình xử lý route tương ứng của route đó.
src/routes/user.route.ts
import express from 'express';
import {
getAllUsersHandler,
getMeHandler,
} from '../controllers/user.controller';
import { deserializeUser } from '../middleware/deserializeUser';
import { requireUser } from '../middleware/requireUser';
import { restrictTo } from '../middleware/restrictTo';
const router = express.Router();
router.use(deserializeUser, requireUser);
// Admin Get Users route
router.get('/', restrictTo('admin'), getAllUsersHandler);
// Get my info route
router.get('/me', getMeHandler);
export default router;
Tệp user.route.ts chứa các route để:
Lấy tất cả người dùng (chỉ bởi Admin)
Lấy thông tin đăng nhập hiện tại (người dùng đang đăng nhập)
src/routes/auth.route.ts
import express from 'express';
import { loginHandler, registerHandler } from '../controllers/auth.controller';
import { validate } from '../middleware/validate';
import { createUserSchema, loginUserSchema } from '../schema/user.schema';
const router = express.Router();
// Register user route
router.post('/register', validate(createUserSchema), registerHandler);
// Login user route
router.post('/login', validate(loginUserSchema), loginHandler);
export default router;
Tệp auth.route.ts chứa các route để:
Đăng nhập người dùng
Đăng ký người dùng
Cập nhật tệp app.ts để sử dụng các route đã được định nghĩa.
Tiếp theo, cập nhật tệp app.ts để sử dụng các middleware sau:
Body Parser middleware để phân tích cú pháp (parse) nội dung yêu cầu và gắn nó vào req.body
Morgan để ghi log yêu cầu HTTP vào terminal
Cors để hỗ trợ chia sẻ tài nguyên qua nguồn gốc khác nhau (Cross-Origin Resource Sharing)
Cookie Parser để phân tích cú pháp (parse) cookie và gắn nó vào req.cookies
Bộ định tuyến người dùng (user router)
src/app.ts
require('dotenv').config();
import express, { NextFunction, Request, Response } from 'express';
import morgan from 'morgan';
import config from 'config';
import cors from 'cors';
import cookieParser from 'cookie-parser';
import connectDB from './utils/connectDB';
import userRouter from './routes/user.route';
import authRouter from './routes/auth.route';
const app = express();
// Middleware
// 1. Body Parser
app.use(express.json({ limit: '10kb' }));
// 2. Cookie Parser
app.use(cookieParser());
// 3. Logger
if (process.env.NODE_ENV === 'development') app.use(morgan('dev'));
// 4. Cors
app.use(
cors({
origin: config.get<string>('origin'),
credentials: true,
})
);
// 5. Routes
app.use('/api/users', userRouter);
app.use('/api/auth', authRouter);
// Testing
app.get('/healthChecker', (req: Request, res: Response, next: NextFunction) => {
res.status(200).json({
status: 'success',
message: 'Welcome to CodevoWeb????',
});
});
// UnKnown Routes
app.all('*', (req: Request, res: Response, next: NextFunction) => {
const err = new Error(`Route ${req.originalUrl} not found`) as any;
err.statusCode = 404;
next(err);
});
// Global Error Handler
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
err.status = err.status || 'error';
err.statusCode = err.statusCode || 500;
res.status(err.statusCode).json({
status: err.status,
message: err.message,
});
});
const port = config.get<number>('port');
app.listen(port, () => {
console.log(`Server started on port: ${port}`);
// ? call the connectDB function here
connectDB();
});
Kiếm tra JWT Authentication Rest API
Chạy lệnh sau để khởi động mongo và redis container
docker-compose up -d
Tiếp theo chúng ta khởi động server
Register users
Đăng ký user với route sau http://localhost:8000/api/auth/register
Sau khi đăng ký thành công sử dụng ứng dụng mongoCompass để truy cập và kiểm tra dữ liệu
Login User
Sau khi tạo user ở bước trên tiếp theo chúng ta sẽ login và kiểm tra
Ngoài kết quả được trả về ở body, accessToken cũng được trả về ở Cookies
Bây giờ chúng ta sẽ kiểm tra thông tin của user đang được lưu ở redis
Nếu nhìn kỹ hình trên bạn sẽ thấy thời gian của bản ghi còn 44 phút, tức là sau 44 phút bản ghi sẽ bị xóa, đây cũng là thời gian còn hiệu lực của access-token
Lấy thông tin hiện tại của người dùng đang login
Sau khi đăng nhập, hãy sao chép access-token và thêm vào header để có thể sử dụng được api /api/users/me
Admin Get All Users
Quay lại mongoCompas và truy cập vào collection user chọn 1 user bất kỳ đổi role thành admin, như tôi đang đổi user inovationthinking@gmail.com thành admin.Sau đó sử dụng api /api/users để get danh sách user
Tổng Kết
Vậy là tôi và các bạn đã đi qua hết loạt bài xác thực với người dùng sử dụng jwt trong nodejs.Hi vọng mọi người có thêm kiến thức để làm việc trong tương lai.Nếu có bất kỳ câu hỏi nào có thể comment xuống dưới để tôi có thể xem và hỗ trợ nhé.
Subscribe to my newsletter
Read articles from Eminel directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Eminel
Eminel
Hello, my name is Eminel a software engineer specializing in scalable software architecture, microservices, and AI-powered platforms. Passionate about mentoring, performance optimization, and building solutions that drive business success.