Nest.js passport를 이용한 OAuth2.0 구현

정민교·2024년 4월 20일
0

ai-diary

목록 보기
1/2

안녕하세요 굉장히 오랜만에 Nest.js 글로 돌아왔다.

현재 사이드 프로젝트를 기획하고 작업중에 있는데

사이드 프로젝트에서 Oauth2.0 구글과 카카오 로그인 기능이 필요해서 구현하려고 한다.(우선은 자체 로그인 기능보단 사용자 편의성이 좋은 소셜 로그인부터 구현, 추후에 네이버나, 애플 같은 다른 Oauth 로그인도 들어갈 것 같다.)

Nest.js에서 Passport 사용법을 자세히 살펴보고 분석 및 구현해보도록 할 것이다.

📒Nest.js와 Passport

Passport는 node.js 진영의 백엔드 애플리케이션에서 굉장히 유명한 라이브러리다.

Passport 라이브러리를 nest.js 애플리케이션에서 사용하기 위해서는 @nestjs/passport 모듈과 함께 사용할 수 있다.

Passport는 다음과 같은 일련의 과정을 수행하여 사용자 인증 및 인증 상태를 관리한다.

  • credentials(ex. 사용자 이메일/비밀번호, JWT, Identity Provider로부터 제공받은 identy token 등)을 이용해서 인증을 진행
  • 세션을 생성하거나, JWT를 발행하여 인증 상태를 관리
  • 인증된 사용자에 대한 정보를 route handler에서 사용할 수 있도록 Request 객체에 추가

Passport는 다양한 인증 메커니즘을 구현한 굉장히 많은 전략(strategy)들을 가지고 있다.

Passport는 다양한 전략을 표준 패턴으로 추상화 하고 있고, @nestjs/passport 모듈은 이 패턴을 Nest 구조로 래핑하고 표준화 해 놓았다.

📒필요한 라이브러리

Nest.js에서 Passport를 활용하여 인증을 구현하기 위해서 필요한 라이브러리는 다음과 같다.(구글 로그인)

npm install @nestjs/passport passport passport-google-oauth20
npm install @types/passport-google-oauth20 --save-dev

어떤 Passport strategy를 선택하든 @nestjs/passportpassport는 항상 필요하다.

📒인증 기능 구현하기

인증 전략을 구성하기 위해서는 우선 두 가지가 필요한데,

  1. 특정 전략에 대한 옵션 세트

  2. verify 콜백
    이 콜백 함수에 사용자 검증을 어떻게 할 것인지 정의한다.

    Passport 라이브러리는 이 콜백이 성공시 검증된 user 정보를, 실패 시 null을 return 해야하도록 되어있다..

@nestjs/passport를 사용하여 Passport strategy를 구성하기 위해서 PassportStrategy 클래스를 확장해야 한다.

import { PassportStrategy } from '@nestjs/passport';
import { Profile, Strategy } from 'passport-google-oauth20';
import { Injectable } from '@nestjs/common';
import * as process from 'node:process';

import {
  ENV_GOOGLE_CALLBACK_URL_KEY,
  ENV_GOOGLE_CLIENT_ID_KEY,
  ENV_GOOGLE_CLIENT_SECRET_KEY,
} from '../../common/const/env-keys.const';

@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
  constructor() {
    super({
      clientID: process.env[ENV_GOOGLE_CLIENT_ID_KEY],
      clientSecret: process.env[ENV_GOOGLE_CLIENT_SECRET_KEY],
      callbackURL: process.env[ENV_GOOGLE_CALLBACK_URL_KEY],
      scope: ['email', 'profile'],
    });
  }

super()를 호출해서 전략 생성을 위한 옵션 세트(위의 1번)를 제공한다.

그리고 validate() 메소드를 구현해야 한다.

  async validate(accessToken: string, refreshToken: string, profile: Profile) {
    const { name, emails, provider } = profile;
    
    const user = {
      email: emails[0].value,
      firstName: name.givenName,
      lastName: name.familyName,
      socialProvider: provider,
      externalId: profile.id,
      accessToken,
      refreshToken,
    };

    return user;
  }

이 메소드에 사용자의 credentials를 검증하는 로직을 포함한다. 아직 DB에 User 테이블을 따로 두지 않았기 때문에 우선은 위와 같이 구현해놓았다.

✔️PassportStrategy 뜯어보기

👉 PassportStrategy 인자

PassportStrategy@nestjs/passport 모듈에 있다. 위에서 PassportStratey 클래스라고 이야기했지만 실제로는 함수인데 코드는 다음과 같다.

import * as passport from 'passport';
import { Type } from '../interfaces';

export function PassportStrategy<T extends Type<any> = any>(
  Strategy: T,
  name?: string | undefined,
  callbackArity?: true | number
): {
  new (...args): InstanceType<T>;
} {
  abstract class MixinStrategy extends Strategy {
    abstract validate(...args: any[]): any;

    constructor(...args: any[]) {
      const callback = async (...params: any[]) => {
        const done = params[params.length - 1];
        try {
          const validateResult = await this.validate(...params);
          if (Array.isArray(validateResult)) {
            done(null, ...validateResult);
          } else {
            done(null, validateResult);
          }
        } catch (err) {
          done(err, null);
        }
      };

      if (callbackArity !== undefined) {
        const validate = new.target?.prototype?.validate;
        const arity =
          callbackArity === true ? validate.length + 1 : callbackArity;
        if (validate) {
          Object.defineProperty(callback, 'length', {
            value: arity
          });
        }
      }
      super(...args, callback);

      const passportInstance = this.getPassportInstance();
      if (name) {
        passportInstance.use(name, this as any);
      } else {
        passportInstance.use(this as any);
      }
    }

    getPassportInstance() {
      return passport;
    }
  }
  return MixinStrategy;
}

위에서부터 천천히 살펴보자.

PassportStrategy 함수 실행을 위해서는 필수 인자 한 개, 선택 인자 두 개가 필요하다.

  • Strategy: PassportStrategy 함수 실행 시 첫 번째 인자로 넘겨준 Strategy
  • name: 전략 이름을 name의 값으로 지정, 이 이름을 사용해서 Passport 인스턴스에 전략을 등록할 수 있다.
  • callbackArity: 콜백 함수의 매개변수 수를 정의한다고 하는데... 이건 잘 쓰는지 모르겠다.

👉PassportStrategy 반환 타입

반환 타입은 PassportStrategy 함수로 넘겨준 T(google-oauth-20 의 Strategy)의 인스턴스 타입이다.

👉구현부

  abstract class MixinStrategy extends Strategy {
    abstract validate(...args: any[]): any;

    constructor(...args: any[]) {
      const callback = async (...params: any[]) => {
        const done = params[params.length - 1];
        try {
          const validateResult = await this.validate(...params);
          if (Array.isArray(validateResult)) {
            done(null, ...validateResult);
          } else {
            done(null, validateResult);
          }
        } catch (err) {
          done(err, null);
        }
      };

      if (callbackArity !== undefined) {
        const validate = new.target?.prototype?.validate;
        const arity =
          callbackArity === true ? validate.length + 1 : callbackArity;
        if (validate) {
          Object.defineProperty(callback, 'length', {
            value: arity
          });
        }
      }
      super(...args, callback);

      const passportInstance = this.getPassportInstance();
      if (name) {
        passportInstance.use(name, this as any);
      } else {
        passportInstance.use(this as any);
      }
    }

    getPassportInstance() {
      return passport;
    }
  }
  return MixinStrategy;

PassportStrategy 함수는 실제로 Strategy(google-oauth-20 의 Strategy)를 확장한 MixinStrategy 클래스를 반환한다.

MixinStrategy가 abstract 클래스고 이 validate 메소드가 abstract 메소드이기 때문에 우리가 GoogleStrategy 클래스를 작성할 때 validate 메소드를 반드시 구현해야 한다.

이번엔 생성자 부분을 살펴보면,

생성자에서는 callback 함수를 정의하고 있다. 이 callback 함수에서 우리가 작성한 validate 메소드를 실행한다.

callback 함수의 마지막 인자인 done 또한 콜백함수 인데,

사용자 검증이 완료되었으면, 첫 번째 인자로는 null, 두 번째 인자로는 사용자 정보가 들어가고 done이 호출된다.

에러가 발생했다면 첫 번째 인자에 err가 들어가고 done이 호출된다.

그 밑에 callbackArity 부분은 써 본적도 없어서... 잘 모르기도 하고 굳이 살펴보진 않겠다.

그 밑에서는 전달받은 name 값을 이용해서 passport 인스턴스에 해당 이름으로 전략을 등록해주고 있는 모습이다.

📒작성된 GoogleStrategy

import { PassportStrategy } from '@nestjs/passport';
import { Profile, Strategy } from 'passport-google-oauth20';
import { Injectable } from '@nestjs/common';
import * as process from 'node:process';

import {
  ENV_GOOGLE_CALLBACK_URL_KEY,
  ENV_GOOGLE_CLIENT_ID_KEY,
  ENV_GOOGLE_CLIENT_SECRET_KEY,
} from '../../common/const/env-keys.const';

@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
  constructor() {
    super({
      clientID: process.env[ENV_GOOGLE_CLIENT_ID_KEY],
      clientSecret: process.env[ENV_GOOGLE_CLIENT_SECRET_KEY],
      callbackURL: process.env[ENV_GOOGLE_CALLBACK_URL_KEY],
      scope: ['email', 'profile'],
    });
  }

  async validate(accessToken: string, refreshToken: string, profile: Profile) {
    const { name, emails, provider } = profile;
    console.log(profile);
    const user = {
      email: emails[0].value,
      firstName: name.givenName,
      lastName: name.familyName,
      socialProvider: provider,
      externalId: profile.id,
      accessToken,
      refreshToken,
    };

    return user;
  }
}

작성된 GoogleStrategy는 위와 같다.

📒auth 모듈 생성하기

자 이제 인증/인가 관련 요청을 받고 로직을 수행할 auth 모듈을 생성한다.

nest g resource auth

auth 모듈을 생성해주고 auth.controller.ts 에 route handler를 작성해준다.

import { Controller, Get, Req, UseGuards } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthGuard } from '@nestjs/passport';

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Get('google')
  @UseGuards(AuthGuard('google'))
  async googleAuthenticate() {}

  @Get('google/callback')
  @UseGuards(AuthGuard('google'))
  async googleRedirect(@Req() req) {
    console.log(req);
  }
}

{{host}}/auth/google 경로로 api를 요청하면 구글 로그인 페이지로 redirection 되며,

사용자의 구글 인증이 완료되면, Google OAuth provider가 GCP 프로젝트 생성 시에 등록한 callback url로 redirect 해준다.

📒정리

오늘은 인증 전략 구현에 대해서 살펴보았다.

Passport 라이브러리와 nestjs/passport 모듈을 사용하여 인증/인가를 간편하게 관리할 수 있다.

많은 걸 대신해주고 있기 때문에 우리가 할 일은 PassportStrategy 클래스를 확장한 클래스를 작성하고, 그 안에 validate 메소드를 작성하여 인증 로직만 작성해주면 된다.

User Entity와 테이블을 생성하여 사용자를 DB에 저장하는 작업까지 해보고, AuthGuard에 대해서 알아봐야겠다.

profile
백엔드 개발자

0개의 댓글