[NestJS] PassportStrategy를 사용해 인증하기

JaeKyung Hwang·2024년 8월 5일
0

TECH

목록 보기
5/16
post-thumbnail

최근 nestjs-boilerplate를 참고하여 PassportStrategy를 사용해 인증 로직을 정리해보았다. nestjs-boilerplate를 기반으로 한 auth 중심의 프로젝트 구조를 알아보고 PassportStrategy를 활용해 인증을 구현해보자.

🔍프로젝트 구조 파악하기

nestjs-boilerplate/
└── src/
    ├── auth-google/
    │   ├── config/
    │   │   ├── google-config.type.ts
    │   │   └── google.config.ts
    │   ├── dto/
    │   │   └── auth-google-login.dto.ts
    │   ├── auth-google.controller.ts
    │   ├── auth-google.module.ts
    │   └── auth-google.service.ts
    ├── auth-twitter/
    └── auth/
        ├── config/
        │   ├── auth-config.type.ts
        │   └── auth.config.ts
        ├── dto/
        ├── strategies/
        │   ├── types/
        │   │   ├── jwt-payload.type.ts
        │   │   └── jwt-refresh-payload.type.ts
        │   ├── anonymous.strategy.ts
        │   ├── jwt-refresh.strategy.ts
        │   └── jwt.strategy.ts
        ├── auth-providers.enum.ts
        ├── auth.controller.ts
        ├── auth.module.ts
        └── auth.service.ts

주요 특징

  1. 모듈 단위로 폴더 분리: 각 모듈이 독립적으로 관리될 수 있도록 설계되어 있다.
  2. 파일명 형식: kebab-case.file-type.ts 형식을 따른다.
  3. 소셜 로그인 분리: 소셜 로그인 기능은 별도의 폴더로 분리되어 있으며, 이메일 기반의 로컬 로그인 및 기타 인증 관련 기능들은 auth/ 폴더에 구현되어 있다.

프로젝트 구조를 살펴보다 보니 다음과 같은 생각이 들었다.

  • 댕댕워크 프로젝트를 할 때 OAuth data(OAuth access/refresh token)도 따로 DB에 저장한 뒤 로그아웃, 토큰 재발급, 회원탈퇴 등을 할 때 관련 OAuth API도 사용해서 OAuth data를 관리해주었다. 다시 생각해보니 사실 상 OAuth, 즉 제 3자 서비스와 연동해 동작하는 기능이 따로 없는 한 굳이 이렇게 구현할 필요는 없었지 않나 싶다.
  • OAuth는 인증 및 초기 데이터 가져오기 용도로만 사용하고, 이후의 관리(로그아웃, 토큰 재발급, 회원탈퇴 등)는 우리 서버에서 처리하는 것이 더 간편하고 유지보수하기 좋은 것 같다.
  • 내 나름대로의 결론을 내려보면 다음과 같다.
    • 사용자 데이터를 제3자 서비스와 동기화하거나, 특정 서비스와의 연결 상태를 관리해야 하는 경우에는 지금처럼 OAuth API의 토큰 재발급, 로그아웃, 회원 탈퇴 등의 기능을 사용하기
    • 사용자의 인증과 초기 데이터 획득만 필요한 경우에는 로그인 기능만 사용하기

config/ 폴더

  • 설정 타입을 저장하는 파일 및 설정 파일을 저장하는 폴더이다.
  • app.module.ts 파일의 ConfigModule에서 설정 객체들을 로드하여 환경 변수들을 가져오도록 설정한다.
    @Module({
      imports: [
        ConfigModule.forRoot({
          isGlobal: true,
          load: [
            databaseConfig,
            authConfig,
            appConfig,
            googleConfig,],
          envFilePath: ['.env'],
        }),
  • ex. google.config.ts
    import { registerAs } from '@nestjs/config';
    
    import { IsOptional, IsString } from 'class-validator';
    import validateConfig from '../../utils/validate-config';
    import { GoogleConfig } from './google-config.type';
    
    class EnvironmentVariablesValidator {
      @IsString()
      @IsOptional()
      GOOGLE_CLIENT_ID: string;
    
      @IsString()
      @IsOptional()
      GOOGLE_CLIENT_SECRET: string;
    }
    
    export default registerAs<GoogleConfig>('google', () => {
      validateConfig(process.env, EnvironmentVariablesValidator);
    
      return {
        clientId: process.env.GOOGLE_CLIENT_ID,
        clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      };
    });
    • EnvironmentVariablesValidator 클래스에서 class-validator를 사용해 config type들의 검증 방식을 정의한다.
    • registerAs 함수를 사용해 환경 변수를 검증하고 'google'이라는 이름의 namespaced 설정 객체를 반환한다.
      • 환경 변수 객체 가져오기
        const google = configService.get('google');
        // 한경 변수를 담은 객체이므로 다음과 같이 사용 가능
        google.clientId
        google.clientSecret
      • 객체 요소 바로 가져오기
        configService.get('google.clientId');
        configService.get('google.clientSecret');

dto/ 폴더

  • 데이터 전송 객체(Data Transfer Object)를 저장하는 폴더이다.
  • 파일 하나 당 하나의 DTO를 정의한다.

types/ 폴더

  • 객체 타입을 저장하는 폴더이다.
  • 보통 파일 하나 당 하나의 type을 정의한다.

🛂passport strategy를 활용해 guard 만들기

NestJS에서 인증/인가를 위한 guard를 작성하는 방법은 크게 두 가지이다.

  1. Custom Guard

    • class CustomGuard implements CanActivate와 같이 CanActivate 인터페이스를 구현하여 원하는 로직을 작성한다.
    • Guards | NestJS 참고
  2. Passport Strategy

    • Passport의 Strategy를 확장하여 Custom Strategy를 구현하고, 해당 Strategy를 실행하는 Guard를 만든다.
    • passport | NestJS 참고

JWT 토큰을 쿠키에 담아 사용하는 경우, 다음과 같은 패키지를 설치해야 한다.

npm install --save @nestjs/passport passport passport-jwt
npm install -D @types/passport-jwt

Custom Strategy 구현

기본적으로 class CustomStrategy extends PassportStrategy(Strategy, 'name')와 같이 passport-jwt의 Strategy를 확장하여 custom strategy를 구현한다. 여기서 name 부분은 따로 지정하지 않으면 'jwt'가 기본값이다.

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: any) {
    return { userId: payload.sub, username: payload.username };
  }
}

Guard 구현

Custom Strategy를 사용하여 Guard를 구현한다.

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

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  canActivate(context: ExecutionContext) {
    // 추가 로직이 필요한 경우 여기에 작성
    return super.canActivate(context);
  }
}

Guard는 Controller에서 @UseGuards(JwtAuthGuard)로 사용하면 된다.
따로 canActivate 추가 로직이 없다면 @UseGuards(AuthGuard('jwt'))와 같이 바로 사용해도 된다.

🚀적용해보기

https://github.com/do0ori/login-with-OAuth/tree/main/backend

  • auth.strategy.ts

    import { Injectable } from '@nestjs/common';
    import { ConfigService } from '@nestjs/config';
    import { PassportStrategy } from '@nestjs/passport';
    import { Strategy as JwtStrategy } from 'passport-jwt';
    
    import { AllConfigType } from '../../config/config.type';
    import { TokenValidationHelper } from '../../helpers/token-validation.helper';
    import { AccessTokenPayload } from '../../token/interfaces/access-token-payload.interface';
    import { extractJwtFromCookie } from '../../utils/cookie-extractor.util';
    
    @Injectable()
    export class AuthStrategy extends PassportStrategy(JwtStrategy, 'auth') {
        constructor(
            private readonly tokenValidationHelper: TokenValidationHelper,
            configService: ConfigService<AllConfigType>,
        ) {
            super({
                jwtFromRequest: extractJwtFromCookie('access_token'),
                secretOrKey: configService.get('token.accessSecret', { infer: true }),
            });
        }
    
        // TODO: Replace return type any with User type later
        async validate(payload: AccessTokenPayload): Promise<any | never> {
            return this.tokenValidationHelper.validatePayload(payload);
        }
    }
    • 참고로 passport-jwt의 ExtractJwt 객체에는 cookie에서 token을 추출하는 method는 존재하지 않아 내가 따로 extractJwtFromCookie라는 이름의 util 함수를 구현해 사용했다.
  • auth.guard.ts

    import { Injectable } from '@nestjs/common';
    import { AuthGuard } from '@nestjs/passport';
    
    @Injectable()
    export class JwtAuthGuard extends AuthGuard('auth') {}
    • AuthStrategy를 정의할 때 적어준 name인 'auth'를 AuthGuard에 넘겨주면 JwtAuthGuard가 AuthStrategy를 실행한다.

댕댕워크 프로젝트를 할 때에는 passport에 대해 잘 모르기도 했고 어려워 보여서 사용하지 않았었는데 확실히 token을 추출하고 검증한 후 사용자 정보를 request.user에 담아 반환하는 등의 인증 로직을 strategy를 통해 분리할 수 있어서 더 코드가 깔끔해진 것 같다. 인가는 그대로 기존 Custom Guard를 작성하는 방식으로 진행하면 될 것 같다.

profile
이것저것 관심 많은 개발자👩‍💻

0개의 댓글