Установим библиотеку для работы с шифрованием паролей (чтобы не хранить в базе пароли в открытом виде)
Заменим имя модели на UserModel вместо AuthModel, чтобы точнее указать, что мы тут работаем с моделью пользователя
src > auth > user.model.ts
Заменим UserModel на AuthModel в зависимостях модуля
src > auth > auth.module.ts
Далее нужно провалидировать ДТОшку того объекта для регистрации и аутентификации, который приходит к нам с клиента
src > auth > dto > auth.dto.ts
Далее реализуем логику сервиса:
Инжектим модель UserModel
добавляем методы createUser для создания нового пользователя (в return возвращается созданный пользователь + вызывается функция для сохранения его в базе через функцию save()) и findUser для поиска уже существующего пользователя в базе
src > auth > auth.service.ts
Далее уже опишем контроллер:
Сюда мы вставляем зависимость от сервиса AuthService
Далее реализуем метод register, который будет сначала искать старого пользователя, если он его найдёт, то вернёт ошибку неверного запроса, а если не найдёт, то отправит запрос в сервис на создание пользователя
сам метод регистрации оборачиваем в декоратор @UsePipes(new ValidationPipe()), чтобы работала валидация по ДТОшке (в ней работает class-validator)
src > auth > auth.controller.ts
Тут мы сохраним строковую константу с ошибкой
src > auth > auth.constants.ts
При первом запросе на регистрацию мы получим полные данные по пользователю
При повторной попытке на те же данные мы получим ошибку
002 Как работает JWT
Основные причины появления JWT:
Приход SPA, которые не использовали куки
Потребность разделять авторизацию и сервер, который имеет приватные роуты
Схема работы с JWT:
Клиент делает запрос к серверу авторизации и передаёт в него данные авторизации
Далее сервис логина выпускает клиенту JWT-токен. Сервис подписывает JWT некоторым секретом, который знает только сервер
Далее, когда пользователь обращается к приватным роутам, guard на бэке проверяет, что у клиента используется валидный JWT-токен
Токен разбит на 3 части:
HEADER - хранит в себе тип (typ) и алгоритм(alg) шифрования.
PAYLOAD - сами передаваемые данные на сервер (почту, пароль, iat - время создания токена).
SIGNATURE - подпись, по которой идёт проверка. Так же она хранит секрет, по которому будет происходить дешифровка данных на сервере.
На сайте JWT можно посмотреть пример работы JWT-токена
Если мы злоумышленник и хотим что-то изменить в передаваемых данных, то у нас это не получится, так как изменение данных не работает без перекодировки от секрета
003 Авторизация и генерация JWT
Устанавливаем модуль для работы с JWT внутри неста
Добавляем переменную секрета в конфиг окружения
.env
Добавляем функцию для генерации конфига JWT. Конкретно тут нам нужен будет только секрет
src > configs > jwt.config.ts
Добавляем в модуль аутентификации зависимость от JwtModule.
Эта зависимость будет в себя принимать нестовские ConfigModule и ConfigService и фектори, который в себя принимает функцию-генератор конфига для формирования JWT
src > auth > auth.module.ts
В сервис добавляем два метода:
validateUser - метод валидации пользователья, который
сначала ищет пользователя в базе по почте с помощью метода findUser (если не найдёт, то выкинет ошибку почты),
затем проверяет пароль пользователя через сравнение с зашифрованной версией в базе (если не сходятся, то выведет ошибку пароля)
и уже в конце возвращает почту пользователя
login - этот метод формирует JWT, который зашифрует в себе объект payload (почту пользователя)
src > auth > auth.service.ts
Сейчас добавим две константы с текстом ошибки, которые будут возвращаться на фронт из нашего контроллера
src > auth > auth.constants.ts
Далее добавляем в контроллер метод логина, который
получает на вход логин и пароль по модели аутентификации
из метода валидации пользователя validateUser получает почту
возвращает на фронт JWT-токен с помощью метода login (внутрь которого как payload передаём почту) из сервиса
src > auth > auth.controller.ts
При логине мы получаем токен для доступа:
Если ввели неверную почту
Если ввели неверный пароль:
004 JWT стратегия и Guard
Существует огромное количество стратегий для защиты входа
Установим зависимости:
паспорт неста
паспорт
стратегию для паспорта (аутентификация через JWT)
типы для стратегии
Далее нам нужно реализовать конфиг стратегии, который будет
возвращать класс с функциональностью PassportStrategy,
конфиг стратегии, который передаём в super()
и дополнительные методы (например, наша валидация, которая возвращает почту в силу того, что валидация у нас уже прошла ранее)
src > auth > strategies > jwt.strategy.ts
Далее нам нужно будет:
добавить ConfigModule в модуль аутентификации, чтобы мы могли добавить в провайдера нашу JwtStrategy, которая использует ConfigService
добавить PassportModule для подключения работы паспорта
и добавить в провайдеры JwtStrategy
src > auth > auth.module.ts
Тут мы уже описываем наш гуард
Создаём класс JwtAuthGuard, который будет являться просто алиасом (будет повторять функционал оригинального класса из неста, но имея другое имя) для класса AuthGuard с типом jwt. Такой подход будет удобнее для дальнейшего использования в декораторах
src > auth > guards > jwt.guard.ts
Далее очень просто через декоратор @UseGuards(имя_гуарда) мы можем добавить любой наш гуард на запрос по роуту. Конкретно мы добавим JwtAuthGuard, который будет сверять JWT из хедера запроса у пользователя
src > review > review.controller.ts
Получает клиент JWT при авторизации
Если у нас не будет JWT, то все запросы по закрытым роутам будут неавторизованными
Если же мы добавим JWT в Bearer, то наш запрос уже будет авторизован и мы сможем получить данные с сервера (только для правильной работы запроса нужно использовать строку подобной сгенерированной с помощью new Types.ObjectId().toHexString())
005 Декоратор для получения пользователя
Далее напишем собственный декоратор для получения данных из запроса (аналог @Param или @Body для вытаскивания значений из нужных частей запроса на сервер)
Для реализации данной цели сильно помогает встроенная в нест функция createParamDecorator для создания декораторов из параметров запроса. Конкретно эта функция помогает нам работать с получаемым контекстом и данными.
Тут мы создали декоратор для получения почты пользователя из запроса
src > decorators > user-email.decorator.ts
Получаем с помощью декоратора почту пользователя и выводим в консоль
src > review > review.controller.ts
И при запросе на сервер мы получили почту пользователя
006 Тесты с авторизацией
Сейчас наши тесты проходят с ошибкой, так как запросы на удаление постов не проходят по гуардам (в запросе нет JWT-токена)
Чтобы добавить токен в тесты:
добавим данные для входа пользователя loginDto
далее создадим res, который будет хранить в себе body ответа от сервера с токеном
далее сохраняем токен в переменную, получая его из body.access_token
далее в тестах, где нужен JWT, добавляем в чейн метод set(), который позволяет установить заголовок запрос
в запросе устанавливаем имя Authorization и в его значение кладём Bearer с токеном
test > review.e2e-spec.ts
И далее все тесты проходят успешно
007 Упражнение 3 - Тесты логина
Далее нам нужно будет создать отдельные e2e тесты для проверки логина пользователя
Делаем проверку на
удачный логин
ошибку в пароле
ошибку в логине
В методе expect() в ошибках мы можем передать не только статускод, но и ответ от сервера, который нами ожидается