088 Работа JWT

JWT (JSON Web Token) - это объект, который обеспечивает безопасную передачу данных в шифрованном виде.

Токен разбит на 3 части:

  • HEADER - хранит в себе тип и алгоритм шифрования.
  • PAYLOAD - сами передаваемые данные на сервер.
  • SIGNATURE - подпись, по которой идёт проверка. Так же она хранит секрет, по которому будет происходить дешифровка данных на сервере.

089 Создание токена

Устанавливаем АПИ модуль по работе с токенами

npm i jsonwebtoken
npm i -D @types/jsonwebtoken

Метод sign реализует само тело PAYLOAD Конкретно добавление свойства iat позволяет нам избежать утечки токенов и каждый раз они будут создаваться новыми. Так же даже если наш токен утечёт, то злоумышленник сможет им пользоваться только определённый промежуток времени, который мы зададим самостоятельно. Так как у злоумышленника не будет refresh-token, то он не сможет его обновить, а мы на сервере сможем и по нему будем уже давать доступ пользователю.

Добавляем секрет в наше окружение

.env

SALT=10  
SECRET="MYAPP"

И далее реализуем получение jwt. Через ConfigService получаем из окружение секрет. В signJWT() реализуем генерацию токена через метод входа sign()

user.controlle.ts

import { sign } from 'jsonwebtoken';
 
@injectable()
export class UserController extends BaseController implements IUserController {
	constructor(
		@inject(TYPES.ILogger) private loggerService: ILogger,
		@inject(TYPES.UserService) private userService: IUserService,
		// добавляем конфиг сервис, который создан через синглтон
		@inject(TYPES.ConfigService) private configService: IConfigService,
	) {
		/// CODE ...
	}
 
	// реализаця метода логина пользователя
	async login(
		req: Request<{}, {}, UserLoginDto>,
		res: Response,
		next: NextFunction,
	): Promise<void> {
		// получаем ответ от валидатора на основе переданного в него пользователя из тела запроса
		const result = await this.userService.validateUser(req.body);
 
		// если результата нет, то выведем ошибку
		if (!result) {
			return next(new HTTPError(401, 'ошибка авторизации', 'login'));
		}
 
		// получаем наш jwt
		const jwt = await this.signJWT(req.body.email, this.configService.get('SECRET'));
 
		// если всё нормально, то отправляем ответ
		this.ok(res, { jwt });
	}
 
	/// CODE ...
 
	private async signJWT(email: string, secret: string): Promise<string> {
		return new Promise<string>((resolve, reject) => {
			// принимает в себя payload
			sign(
				// payload
				{
					email,
					// обязательно нужно добавить дату создания
					iat: Math.floor(Date.now() / 1000),
				},
				// второй параметр - секрет
				secret,
				// опции - HEAD
				{
					algorithm: 'HS256',
				},
				// дальше идёт функция, которая принимает токен или ошибку
				(err, token) => {
					if (err) {
						reject(err);
					}
					resolve(token as string);
				},
			);
		});
	}
}

И такой ответ мы получаем от сервера

На сайте JWT можно посмотреть раскодированный токен, если вставить его и секрет

090 Middleware для проверки jwt

Сразу нужно отметить, что все действия по работе с JWT можно оптимизировать, используя библиотеку к Express - Passport

Нам далее пригодиться переписать реквест таким образом, чтобы он имел в себе так же и значение пользователя. Интерфейсы имеют свойство объединяться в TS, поэтому декларируем пространство имён и дополняем в нём интерфейс запроса, чтобы в нём было свойство пользователя - таким образом мы обогащаем наш запрос

types > custom.d.ts

declare namespace Express {
	export interface Request {
		user: string;
	}
}

Тут уже реализуем интерфейс аутентификации пользователя

примечение: тут payload будет первое время возвращать ошибку на свойство email ровно до того момента, пока не сделаем spec в тестах, так как там будет указана почта

common > auth.middleware.ts

import { IMiddleware } from './middleware.interface';
import { NextFunction, Response, Request } from 'express';
import { verify } from 'jsonwebtoken';
 
// это посредник для реализации аутентификации в системе
export class AuthMiddleware implements IMiddleware {
	constructor(private secret: string) {}
 
	execute(req: Request, res: Response, next: NextFunction): void {
		// если в ответе есть данные авторизации
		if (req.headers.authorization) {
			// дальше пойдёт проверка токена
			// verify проверяет соответствие токена секрету
			// эта функция асинхронна
			verify(
				// Bearer JWT - так выглядит хранимая информация
				// нам нужно её отделить и получить только один токен без первого слова
				req.headers.authorization.split(' ')[1],
				// сюда мы передаём секрет
				this.secret,
				// функция, которая выполнится по результатам проверки
				(err, payload) => {
					// если выскочила ошибка
					if (err) {
						// то ничего не делаем
						next();
						// если получили данные
					} else if (payload) {
						// то обогащаем ответ дополнительными данными о пользователе
						req.user = payload.email;
						next();
					}
				},
			);
		} else {
			next();
		}
	}
}

Далее нам нужно реализовать использование аутентификации через основной метод использования посредников

app.ts

useMiddleware(): void {
	this.app.use(json());
	// инициализируем аутентификацию и передаём в неё секрет
	const authMiddleware = new AuthMiddleware(this.configService.get('SECRET'));
	// биндим контекст запуска
	this.app.use(authMiddleware.execute.bind(authMiddleware));
}

Добавим в контроллер пользователя метод info, который будет возвращать данные о пользователе

users.controller.interface.ts

import { NextFunction, Request, Response } from 'express';
 
export interface IUserController {
	login: (req: Request, res: Response, next: NextFunction) => void;
	register: (req: Request, res: Response, next: NextFunction) => void;
	// будет возвращать информацию о пользователе, который к нам пришёл
	info: (req: Request, res: Response, next: NextFunction) => void;
}

И теперь тут инициализируем middleware и создём метод получения информации о пользователе

users.controller.ts

constructor(
	@inject(TYPES.ILogger) private loggerService: ILogger,
	@inject(TYPES.UserService) private userService: IUserService,
	@inject(TYPES.ConfigService) private configService: IConfigService,
) {
	super(loggerService);
	this.bindRoutes([
		{
			path: '/register',
			method: 'post',
			func: this.register,
			middlewares: [new ValidateMiddleware(UserRegisterDto)],
		},
		{
			path: '/login',
			method: 'post',
			func: this.login,
			middlewares: [new ValidateMiddleware(UserLoginDto)],
		},
		// Конкретно тут обрабатывается запрос получения информации о пользователе
		{
			path: '/info',
			method: 'get',
			func: this.info,
			middlewares: [],
		},
	]);
}
 
async info({ user }: Request, res: Response, next: NextFunction): Promise<void> {
	this.ok(res, { email: user });
}

Так же в нодмон нужно добавить флаг --files, который позволит подтянуть кастомные типы

nodemon.json

{
	"watch": [
		"src"
	],
	"ext": "ts,json",
	"ignore": [
		"src/**/*.spec.ts"
	],
	"exec": "ts-node --files ./src/main.ts"
}

И теперь можно увидеть, что сервер возвращает данные о пользователе, передав в хедер запроса токен пользователя

091 Упражнение - Guard авторизации

Далее нам нужно реализовать guard, который будет ограничивать вход пользователей в приложение, если они не будут соответствовать определённым требованиям

common > auth.guard.ts

import { IMiddleware } from './middleware.interface';
import { NextFunction, Request, Response } from 'express';
 
export class AuthGuard implements IMiddleware {
	execute(req: Request, res: Response, next: NextFunction): void {
		// если в ответе будет находиться пользователь
		if (req.user) {
			// завершаем исполнение функции и продолжаем выполнение посредников
			return next();
		}
 
		// если нет тела в ответе, то отправляем ошибку
		res.status(401).send({ error: 'Вы не авторизован' });
	}
}

Далее для get запроса добавим гуарда

users.controller.ts

constructor(
	@inject(TYPES.ILogger) private loggerService: ILogger,
	@inject(TYPES.UserService) private userService: IUserService,
	@inject(TYPES.ConfigService) private configService: IConfigService,
) {
	super(loggerService);
	this.bindRoutes([
		{
			path: '/register',
			method: 'post',
			func: this.register,
			middlewares: [new ValidateMiddleware(UserRegisterDto)],
		},
		{
			path: '/login',
			method: 'post',
			func: this.login,
			middlewares: [new ValidateMiddleware(UserLoginDto)],
		},
		{
			path: '/info',
			method: 'get',
			func: this.info,
			middlewares: [new AuthGuard()],
		},
	]);
}

И мы получаем такой результат при запросе, так как мы не обходим гуарда

Уже тут нужно будет реализовать метод получения данных, отправляя запрос в репозиторий (нельзя нарушать слоёную архитектуру и отправлять запрос напрямую в базу)

user.service.interface.ts

export interface IUserService {
	createUser: (dto: UserRegisterDto) => Promise<UserModel | null>;
	validateUser: (dto: UserLoginDto) => Promise<boolean>;
	// добавляем метод получения информации о пользователе
	getUserInfo: (email: string) => Promise<UserModel | null>;
}

user.service.ts

async info({ user }: Request, res: Response, next: NextFunction): Promise<void> { 
	// получаем информацию о пользователе
	const userInfo = await this.userService.getUserInfo(user);  
	this.ok(res, { email: userInfo?.email, id: userInfo?.id });  
}
 
async getUserInfo(email: string): Promise<UserModel | null> {
	return this.usersRepository.find(email);
}

И теперь мы можем увидеть то, что мы прочитали непосредственно из базы - почту и id пользователя