이전 포스트에서 진행 했던 내용의 연속으로 JWT를 이용하여 후속 호출에 사용이 가능하도록 합니다.
두가지 요구사항을 바탕으로 현재 Part를 진행하려고 합니다.
$ npm i --save @nestjs/jwt passport-jwt
$ npm i --save-dev @types/passport-jwt
@nestjs/jwt는 JWT조작을 도와주는 패키지 입니다.
/src/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { User } from 'src/users/user.entity';
import { UsersService } from '../users/users.service';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
) {}
async vaildateUser(username: string, pass: string): Promise<any> {
const user = await this.usersService.find(username);
if (user && user.password === pass) {
const { password, ...result } = user;
return result;
}
return null;
}
async login(user: User) {
const payload = { username: user.username, sub: user.id };
return {
access_token: this.jwtService.sign(payload),
};
}
}
JwtService에서 지원하는 sign()함수를 이용하여 User 정보를 넣고 Access Token을 반환 받습니다.
Secret Key는 외부에 노출되면 안되기 때문에 다른 방법으로 보관해야하지만 Auth Server 테스트 예제를 작성 중이기 때문에 간단하게 코드 파일로 선언하여 사용하겠습니다.
/src/auth/constants.ts
export const jwtConstants = {
secret: 'TestSecretKey',
};
JwtModule을 AuthModule에 등록하며 옵션을 설정해 줍니다.
/src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { UsersModule } from 'src/users/users.module';
import { AuthService } from './auth.service';
import { jwtConstants } from './constants';
import { LocalStrategy } from './strategies/local.strategy';
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '60s' },
}),
],
providers: [AuthService, LocalStrategy],
exports: [AuthService, JwtModule],
})
export class AuthModule {}
AppController에서 AuthService를 사용하기 위하여 export해줍니다.
이제 만들어진 JWT를 반환해야합니다. AuthService의 login 함수를 사용하기 위하여 constructor에 선언해 줘야 합니다.
/src/app.controller.ts
import { Controller, Post, Request, UseGuards } from '@nestjs/common';
import { AuthService } from './auth/auth.service';
import { LocalAuthGuard } from './auth/guards/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);
}
}
Request
{
"username": "test1",
"password": "qwer1234@"
}
Response
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QxIiwic3ViIjoxLCJpYXQiOjE2MTMxMTg2MDIsImV4cCI6MTYxMzExODY2Mn0.IvWUojg92Exp6vJKeOZ8KYJtfsScLAiE-rr3C9MMcgU"
}
유효한 JWT를 요구하여 엔드 포인트를 보호합니다. JwtStrategy를 만듭니다.
src/strategies/jwt.strategy.ts
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { jwtConstants } from '../constants';
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret,
});
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}
jwtFromRequest : JWT 추출 방법을 제공합니다. Request의 Authorization 헤더에 토큰을 제공하는 방식입니다.
ignoreExpiration : false라면 JWT가 만료되었는지 확인하고 만료되었다면 401 예외를 발생합니다.
secretOrKey : 다칭키를 제공하는 옵션입니다.
현재 JwtStrategy의 validate의 절차는 Passport에서 먼저 JWT의 서명을 확인 후 JWT Json을 디코딩합니다.
디코딩 된 JSON을 단일 매개 변수로 전달하는 메소드를 호출합니다.
/src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { UsersModule } from 'src/users/users.module';
import { AuthService } from './auth.service';
import { jwtConstants } from './constants';
import { JwtStrategy } from './strategies/jwt.strategy';
import { LocalStrategy } from './strategies/local.strategy';
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '60s' },
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService, JwtModule],
})
export class AuthModule {}
/src/auth/guards/jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
보호된 경로와 관련 가드를 구현합니다.
/src/app.controller.ts
import { Controller, Get, Post, Request, UseGuards } from '@nestjs/common';
import { AuthService } from './auth/auth.service';
import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';
import { LocalAuthGuard } from './auth/guards/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('profile')
getProfile(@Request() req) {
return req.user;
}
}
먼저 auth 인증을 통해 AccessToken을 가져옵니다.
Request
{
"username": "test1",
"password": "qwer1234@"
}
Response
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QxIiwic3ViIjoxLCJpYXQiOjE2MTMxMjI1MDUsImV4cCI6MTYxMzEyMjU2NX0.pM3Uz0-ned6VfwvbkbNwyIyN32kOPgDMip7j-hwZZbU"
}
가져온 access token을 헤더에 넣어 다시 요청합니다.
60초 이내에 하지 않을 경우 access token이 만료되어 예외가 발생합니다.
Header
Authorization : Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QxIiwic3ViIjoxLCJpYXQiOjE2MTMxMjI4NjAsImV4cCI6MTYxMzEyMjkyMH0.mKyhYEdG_hxamQZEx5wAUb9kDkyDbWdC-FGUdDGmMvk|
Response
{
"userId": 1,
"username": "test1"
}
전역 가드를 사용하여 이전에 만들었던 UsersController의 접근에 가드가 생성되어 Header인증 없이는 접속이 불가능 합니다.
/src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersModule } from './users/users.module';
import { AuthModule } from './auth/auth.module';
import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';
@Module({
imports: [TypeOrmModule.forRoot(), UsersModule, AuthModule],
controllers: [AppController],
providers: [AppService, { provide: APP_GUARD, useClass: JwtAuthGuard }],
})
export class AppModule {}
앞서 전역 가드에 의하여 모든 경로에 대한 접근이 제한 됩니다. 공개된 경로가 필요하기 때문에 공개된 경로 설정을 위한 custom decorator를 만들어줍니다.
/src/skip-auth.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
/src/auth/auth.controller.ts
import { Controller, Get, Post, Request, UseGuards } from '@nestjs/common';
import { AuthService } from './auth/auth.service';
import { LocalAuthGuard } from './auth/guards/local-auth.guard';
import { Public } from './skip-auth.decorator';
@Controller()
export class AppController {
constructor(private authService: AuthService) {}
@Public()
@UseGuards(LocalAuthGuard)
@Post('auth/login')
async login(@Request() req) {
return this.authService.login(req.user);
}
@Get('profile')
getProfile(@Request() req) {
return req.user;
}
}
/src/users/users.controller.ts
... 생략
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Public()
@Get()
findAll(): Promise<User[]> {
return this.usersService.findAll();
}
... 생략
}
/src/auth/guards/jwt-auth.guard.ts
import { ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { IS_PUBLIC_KEY } from 'src/skip-auth.decorator';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
}
위와 같은 설정을 통해 토큰을 가져올 경우와 findAll의 경로는 공개 경로로 설정하였습니다.