[NestJS] - 19. JWT 토큰 인가 - PassportStrategy/Guard 사용과 토큰 재발급

2023. 3. 4. 04:54
반응형

 

이전 포스팅에서는 인증 후 JWT를 발급하는 방법에 대해서 알아보았다. 

이번에는 로그인된 사용자에게만 API 접근을 허용하도록 하는 인가에 대해서 다루어보겠다.

JWT 토큰 인증 방식에 대해서는 아래의 포스팅을 참고하면 좋을 것 같다.

https://sjh9708.tistory.com/46

 

[Web] 인증과 인가 - JWT 토큰 인증

앞 포스팅에서 세션 방식의 인증과, 성능 개선을 위한 방법들에 대해서 다루어 보았었는데 이번에는 언급했던 토큰 인증 방식에 대해서 알아보려고 한다. 토큰 인증 세션 인증 방식과 달리 인증

sjh9708.tistory.com

 

 


패키지 설치

 

Express에서는 인가의 방법으로 Passport 패키지를 주로 사용했었다.

NestJS에서도 Passport를 사용할 수 있다. 다음의 패키지를 설치해주자.

yarn add passport passport-jwt @nestjs/passport

 

Refresh Token의 쿠키를 가져오기 위해서 cookie-parser도 미리 설치해두자

yarn add cookie-parser

 

 

 


인증 방식 생성

 

이제 API에 접근하기 전에 JWT 토큰을 확인하고, 검증 이후 통과되면 API를 이용할 수 있도록 막는 문지기를 만들어서 인가를 구현해야 한다.

아래와 같이 commons/auth 디렉터리 안에 strategy.ts 파일을 두개 만들어주었다.

각각 Access Token의 검증과 Refresh Token의 검증을 담당하는 문지기이다.

 

 

 

jwt-access.strategy.ts

import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt } from 'passport-jwt';

//PassportStrategy(인증 방식, 이름)
export class JwtAccessStrategy extends PassportStrategy(Strategy, 'access') {
  //NestJS Docs
  constructor() {
    super({
      //   jwtFromRequest: (req) => {
      //     원래 이 안에서 Request에서 무엇을 가져올 지 적어줘야 한다. Bearer 문자열 분리도 하는데 아래 메서드가 대신 해줌
      //   },

      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), //Request Header에서 Bearer auth 뽑아내기
      secretOrKey: process.env.ACCESS_TOKEN_SECRET_KEY,
    });
  }

  //검증 성공 시 실행, 실패 시는 에러
  // ### Passport는 validate에 성공할시 리턴값을 request.user에 저장함!!! ###
  validate(payload) {
    console.log(payload); //email, sub가 payload에 있음
    return {
      email: payload.email,
      id: payload.sub,
    };
  }
}
  • @nestjs/passport 패키지에서 제공하는 PassportStrategy를 상속 및 오버라이딩하여 사용한다.
  • 인자는 PassportStrategy(인증 방식, 코드에서 사용할 Guard 이름) 이다.
  • 생성자
    • jwtFromRequest : Request에서 토큰을 가져오는 로직을 작성한다. 위의 메서드는 Header에서 Bearer 토큰을 가져와준다.
    • secretOrKey : Access Token을 생성할 때 사용한 비밀키를 입력한다.
    • 위 두가지를 입력해주면 Passport가 JWT 토큰을 알아서 검증해준다.
  • validate : Passport는 사용자를 Request.user에 저장한다. 검증이 성공했을 때의 로직을 작성하며, Return값은 user 객체에 사용되게 된다.

 

 

 jwt-refresh.strategy.ts

import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt } from 'passport-jwt';

//PassportStrategy(인증 방식, 이름)
export class JwtRefreshStretagy extends PassportStrategy(Strategy, 'refresh') {
  //NestJS Docs
  constructor() {
    super({
      jwtFromRequest: (req) => {
        const cookie = req.cookies['refreshToken'];
        return cookie;
      },
      secretOrKey: process.env.REFRESH_TOKEN_SECRET_KEY,
    });
  }

  //검증 성공 시 실행, 실패 시는 에러
  // ### Passport는 validate에 성공할시 리턴값을 request.user에 저장함!!! ###
  validate(payload) {
    console.log(payload); //email, sub가 payload에 있음
    return {
      email: payload.email,
      id: payload.sub,
    };
  }
}
  • 마찬가지로 Refresh Token을 담당하는 PassportStrategy를 생성한다.
  • jwtFromRequest에서는 Header가 아니라 이번에는 Request의 쿠키에서 JWT 토큰을 가져온다.

 

▶ common.module.ts

import { Module } from '@nestjs/common';
import { JwtRefreshStretagy } from './auth/jwt-refresh.strategy';
import { JwtAccessStrategy } from './auth/jwt-access.strategy';

@Module({
  imports: [],
  providers: [JwtRefreshStretagy, JwtAccessStrategy],
  exports: [JwtRefreshStretagy, JwtAccessStrategy],
})
export class CommonModule {}

▶ app.module.ts

@Module({
  imports: [
    //use env
    ConfigModule.forRoot(),
    CommonModule,
	//...
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

해당 Strategy들을 전역으로 사용하게 하기 위해서 다음과 같이 모듈을 작성한다.

 

 


API 사용 인가

 

  @UseGuards(AuthGuard('access'))
  @Get(':id')
  selectProduct(@Param('id') id: number): Promise<Product> {
    return this.productService.findOne(id);
  }
  • @UserGuard 데코레이터를 통해서 API 사용 이전에 인가를 거치게 된다.
  • 'access'는 방금 만들어 둔 Access Token에 대한 PassportStrategy를 사용하겠다는 뜻이다.
  • 이제 해당 요청을 처리하기 이전에 Access Token의 검증이 이루어지고, 인증에 실패하면 결과 대신 403 에러를 반환하게 된다.

 

 


토큰 재발급 로직 구현

 

앞에 로그인에서 Access Token과 Refresh Token 두 개를 발급했었다.

평소 API 사용의 인가 처리에는 Access Token을 사용하게 되지만, 만료되었을 때의 재발급은 로그인이 아닌 Refresh Token을 이용해서 Access Token을 재발급하게 만들려고 한다.

 

▶ auth.userInterface.ts

export interface IOAuthUser {
  user: Pick<User, 'email' | 'name' | 'age' | 'point'>;
}

▶ auth.controller.ts

  //리프레쉬 토큰으로 새로운 엑세스 토큰 발급
  @UseGuards(AuthGuard('refresh'))
  @Post('refresh')
  restoreAccessToken(@Req() req: Request & IOAuthUser) {
    return this.authService.getAccessToken({ user: req.user });
  }
  • 이번에는 Refresh Token에 대한 PassportStrategy를 사용한다.
  • Refresh Token에 대한 인가가 통과되면, 새로운 Access Token을 재발급한다.
  • @Req() req : Request & IOAuthUser
    • Express의 Request 객체를 사용한다.  
    • Request 객체의 User 타입을 사용하기 위해서 인터페이스를 만들어 타입을 명시해주었다.
    • Passport를 통과하면 req.user를 통해 Strategy의 validate()에서 리턴된 user 객체를 사용할 수 있다.

 

반응형

BELATED ARTICLES

more