06) 개인 프로젝트) Auth인증 구현 Part 2

Leo·2021년 2월 12일
4

Project-01

목록 보기
6/12
post-thumbnail

인증 시스템에 JWT 내용의 추가

이전 포스트에서 진행 했던 내용의 연속으로 JWT를 이용하여 후속 호출에 사용이 가능하도록 합니다.
두가지 요구사항을 바탕으로 현재 Part를 진행하려고 합니다.

  • 사용자가 사용자이름, 암호로 인증할 수 있도록 허용하고 보호된 API End Point(URL)에 대한 호출에 사용될 JWT를 반환합니다. 현재 사용자이름과 암호로 인증할 수 있으며 JWT를 반환하는 부분을 추가적으로 구현해야합니다.
  • Bearer Token(JWT, OAuth에 대한 토큰)으로 유효한 JWT의 존재를 기반으로 보호되는 API경로를 생성합니다.

JWT 개발을 위한 패키지 다운로드

$ npm i --save @nestjs/jwt passport-jwt
$ npm i --save-dev @types/passport-jwt

@nestjs/jwt는 JWT조작을 도와주는 패키지 입니다.

AuthService에서 Access Token 만들기

/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를 보관할 파일 생성

Secret Key는 외부에 노출되면 안되기 때문에 다른 방법으로 보관해야하지만 Auth Server 테스트 예제를 작성 중이기 때문에 간단하게 코드 파일로 선언하여 사용하겠습니다.

/src/auth/constants.ts

export const jwtConstants = {
  secret: 'TestSecretKey',
};

JwtModule을 AuthModule에 등록

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를 반환하도록 AppController 변경

이제 만들어진 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를 통한 Access Token 확인

POST http://localhost:3000/auth/login

Request

{
    "username": "test1",
    "password": "qwer1234@"
}

Response

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QxIiwic3ViIjoxLCJpYXQiOjE2MTMxMTg2MDIsImV4cCI6MTYxMzExODY2Mn0.IvWUojg92Exp6vJKeOZ8KYJtfsScLAiE-rr3C9MMcgU"
}

Passport JWT 구현

유효한 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 {}

JWT Guard 설정 파일 생성

/src/auth/guards/jwt-auth.guard.ts

import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

보호 경로 및 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;
  }
}

Request를 통한 정상 작동 확인

먼저 auth 인증을 통해 AccessToken을 가져옵니다.

[POST] http://localhost:3000/auth/login

Request

{
    "username": "test1",
    "password": "qwer1234@"
}

Response

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QxIiwic3ViIjoxLCJpYXQiOjE2MTMxMjI1MDUsImV4cCI6MTYxMzEyMjU2NX0.pM3Uz0-ned6VfwvbkbNwyIyN32kOPgDMip7j-hwZZbU"
}

가져온 access token을 헤더에 넣어 다시 요청합니다.
60초 이내에 하지 않을 경우 access token이 만료되어 예외가 발생합니다.

[GET] http://localhost:3000/profile

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의 경로는 공개 경로로 설정하였습니다.

profile
개발자

0개의 댓글