[Nest.js] Login Authentication 구현 ( passport + jwt strategy )
Nest.js로 Login 구현하기 (2)
이전글에서는 데이터베이스에서 사용자를 검색하여 로그인부분까지 구현하였다. 이번글에서는 로그인 성공하고 사용자 정보가 아닌 jwt를 반환하고 API 호출 시 유효한 토큰인지 검증하려고 한다.
패키지 설치 및 모듈 생성
$ npm install --save @nestjs/jwt passport-jwt
$ npm install --save-dev @types/passport-jwt
JwtModule 및 token 생성
// auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { UserService } from '../user/user.service';
import * as bcrypt from 'bcrypt';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(
private userService: UserService,
private jwtService: JwtService,
) {}
async validateUser(id: string, password: string): Promise<any> {
const user = await this.userService.findOne(id);
if (!(await bcrypt.compare(password, user?.password ?? ''))) {
return null;
}
return user;
}
async login(user: any) {
const payload = { userUid: user.userUid };
return {
access_token: this.jwtService.sign(payload),
};
}
}
// app.controller.ts
import { Controller, Post, UseGuards, Request } from '@nestjs/common';
import { AuthService } from './auth/auth.service';
import { LocalAuthGuard } from './auth/guard/local-auth.guard';
@Controller()
export class AppController {
constructor(private authService: AuthService) {}
@UseGuards(LocalAuthGuard)
@Post('auth/login')
async login(@Request() req) {
return this.authService.login(req.user);
}
}
login()
를 추가하여 AppController
에서 호출한다. passport-local
전략으로 비밀번호가 맞는지 확인하였고 jwt를 응답한다.
이때 JwtService
를 AuthService
에 주입시키려면 JwtModule
을 추가해야한다.
$ npm i --save @nestjs/config
// auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../user/user.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { LocalStrategy } from './local.strategy';
import { ConfigService } from '@nestjs/config';
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.registerAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
secret: config.get<string>('JWT_SECRET_KEY'),
signOptions: { expiresIn: '60s' },
}),
}),
],
providers: [AuthService, LocalStrategy],
exports: [AuthService],
})
export class AuthModule {}
나는 환경변수 값을 불러오기 위해 ConfigService를 사용하였다.
$ curl -X POST http://localhost:3000/auth/login -d '{"id": "nofunfromdev", "password": "test"}' -H "Content-Type: application/json
$ # {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyVWlkIjoxLCJpYXQiOjE2NDkxNzA2NjAsImV4cCI6MTY0OTE3MDcyMH0.9xAr3khXvnXKTDlnOp8Aav1OybB_En9cZJ-QZhw8ku8"}
passport jwt Strategy
passport-jwt strategy을 사용하여 Request에 유효한 jwt가 존재하도록 요구하여 엔드포인트를 보호할 수 있다.
// auth/jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get('JWT_SECRET_KEY'),
});
}
async validate(payload: any) {
return { userUid: payload.userUid };
}
}
super()
에 초기 옵션을 설정한다.
jwtFromRequest
: Request에서 JWT를 추출하는 방법을 설정한다. Authorization header에 bearer이 표준이다.ignoreExpiration
: 기본적으로 fasle 설정을 한다. 이 설정은 JWT 검증을 Passport
모듈에 위임한다. 만료된 JWT가 제공되면 Request
는 거부되고 401 Unauthorized
응답이 전송된다.secretOrKey
: 토큰 발급에 쓰일 시크릿 키. 노출금지!
validate()
: jwt-strategy인경우 passport는 먼저 JWT의 서명을 확인하고 JSON을 해독한다. 그 후 디코딩된 JSON을 단일파라미터로 가지는 validate()
를 호출한다. 반환값으로 Request 객체에 user의 속성으로 설정된다. ex) req.user
// auth/guard/jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
jwt guard를 생성하고 엔드포인트에 추가한다.
import { Controller, Post, UseGuards, Request, Get } from '@nestjs/common';
import { AuthService } from './auth/auth.service';
import { JwtAuthGuard } from './auth/guard/jwt-auth.guard';
import { LocalAuthGuard } from './auth/guard/local-auth.guard';
@Controller()
export class AppController {
constructor(private authService: AuthService) {}
@UseGuards(LocalAuthGuard)
@Post('auth/login')
async login(@Request() req) {
return this.authService.login(req.user);
}
@UseGuards(JwtAuthGuard)
@Get('me')
me(@Request() req) {
return req.user;
}
}
$ curl http://localhost:3000/me -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyVWlkIjoxLCJpYXQiOjE2NDkxNzA2NjAsImV4cCI6MTY0OTE3MDcyMH0.9xAr3khXvnXKTDlnOp8Aav1OybB_En9cZJ-QZhw8ku8"
$ # {"userUid" : 1}
validate()에서 반환되는 값은 자동으로 사용자 개체를 생성하여 req.user로 Request에 할당된다.
터미널에서 실행하면 정상적으로 사용자 값이 return 된다.