Как настроить JWT аутентификацию с Passport в NestJS?
Настройка JWT аутентификации с Passport в NestJS
Установка зависимостей
Для работы с JWT и Passport необходимы следующие пакеты:
npm install @nestjs/passport @nestjs/jwt passport passport-jwt
npm install -D @types/passport-jwt
Создание JwtStrategy
JwtStrategy отвечает за валидацию токена при каждом запросе. Passport автоматически извлекает токен из заголовка Authorization: Bearer <token> и передаёт декодированный payload в метод validate.
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private configService: ConfigService) {
super({
// Извлекаем токен из заголовка Authorization: Bearer
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
// Запрещаем просроченные токены
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET'),
});
}
// Payload уже проверен библиотекой — здесь только бизнес-логика
async validate(payload: { sub: number; email: string }) {
return { userId: payload.sub, email: payload.email };
}
}
Создание AuthGuard
Стандартный подход — расширить AuthGuard('jwt') для повторного использования:
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
AuthModule: регистрация JwtModule и Passport
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './strategies/jwt.strategy';
@Module({
imports: [
PassportModule,
// Асинхронная конфигурация для получения секрета из env
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
secret: config.get<string>('JWT_SECRET'),
signOptions: { expiresIn: '1h' },
}),
}),
],
providers: [AuthService, JwtStrategy],
controllers: [AuthController],
exports: [AuthService],
})
export class AuthModule {}
AuthService: выдача токена
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
) {}
async validateUser(email: string, password: string) {
const user = await this.usersService.findByEmail(email);
if (user && await bcrypt.compare(password, user.passwordHash)) {
const { passwordHash, ...result } = user;
return result;
}
return null;
}
async login(user: { id: number; email: string }) {
// sub — стандартное поле JWT для идентификатора субъекта
const payload = { sub: user.id, email: user.email };
return { access_token: this.jwtService.sign(payload) };
}
}
Защита маршрутов
import { Controller, Get, UseGuards, Request } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
@Controller('profile')
export class ProfileController {
// Только аутентифицированные пользователи могут получить профиль
@UseGuards(JwtAuthGuard)
@Get()
getProfile(@Request() req) {
// req.user заполняется результатом JwtStrategy.validate()
return req.user;
}
}
Глобальная защита через APP_GUARD
Если большинство маршрутов требуют аутентификации, можно применить guard глобально и использовать @Public() для исключений:
// Декоратор для публичных маршрутов
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
Что хочет услышать интервьюер
Знание роли каждого пакета: @nestjs/passport как обёртка, passport-jwt как реализация стратегии, @nestjs/jwt для подписи и верификации токенов
Понимание потока аутентификации: запрос → Guard → Strategy.validate() → req.user → контроллер
Объяснение, зачем payload в JWT содержит поле sub, и почему ignoreExpiration должен быть false
Умение хранить JWT_SECRET в переменных окружения через ConfigService, а не хардкодить
Знание разницы между глобальным APP_GUARD и декоратором @UseGuards на методе/контроллере
Пример: JwtStrategy — валидация токена
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET'),
});
}
async validate(payload: { sub: number; email: string }) {
// Возвращаемое значение станет req.user
return { userId: payload.sub, email: payload.email };
}
}
Пример: AuthModule — регистрация зависимостей
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtStrategy } from './strategies/jwt.strategy';
import { AuthService } from './auth.service';
@Module({
imports: [
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
secret: config.get<string>('JWT_SECRET'),
signOptions: { expiresIn: '1h' },
}),
}),
],
providers: [AuthService, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
Пример: Защита маршрута через JwtAuthGuard
import { Controller, Get, UseGuards, Request } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
@Controller('profile')
export class ProfileController {
@UseGuards(JwtAuthGuard)
@Get()
getProfile(@Request() req) {
// req.user содержит { userId, email } из JwtStrategy.validate()
return req.user;
}
}
Типичные ошибки
Хранение JWT_SECRET напрямую в коде вместо использования ConfigService и .env файла
Возврат лишних данных (например, passwordHash) из метода validate() в JwtStrategy — они попадают в req.user
Установка ignoreExpiration: true в продакшне, что делает токены вечными даже после истечения срока
Забывают экспортировать JwtModule или AuthService из AuthModule, из-за чего другие модули не могут их использовать
Путаница между LocalStrategy (логин по паролю) и JwtStrategy (защита маршрутов) — используют JwtStrategy для логина


