[NestJS] OAuth 2.0 로그인/회원가입 3: Passport로 구현하기

JaeKyung Hwang·2024년 8월 12일
1

TECH

목록 보기
8/16
post-thumbnail

기존 https://github.com/do0ori/login-with-OAuth에서 REST API를 기반으로 소셜 로그인을 구현했었다. 이번에는 이전 글의 마지막에서 이야기했듯이 provider 별 passport를 사용해 기존 repo의 passport branch에 추가로 구현해보았다.
로그인 과정은 기존과 동일하다.
참고로 이 글에서 환경변수를 process.env로 가져왔으나 이는 코드 설명을 위한 것이고 실제로는 ConfigService를 활용했다.
또한 코드 구조는 다 비슷하기 때문에 google 위주로 설명한다.

🟦passport-google-oauth20

passport-google을 검색해보니 여러 개였는데 가장 다운로드 수가 많은 strategy로 선택했다.

설치

npm i passport-google-oauth20
npm i -D @types/passport-google-oauth20

코드

  • google.strategy.ts

    import { Injectable } from '@nestjs/common';
    import { PassportStrategy } from '@nestjs/passport';
    import { Profile, Strategy } from 'passport-google-oauth20';
    
    @Injectable()
    export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
        constructor() {
            super({
                clientID: process.env.CLIENT_ID,
                clientSecret: process.env.CLIENT_SECRET,
                callbackURL: process.env.REDIRECT_URI,
                scope: ['email', 'profile'],
            });
        }
    
        async validate(accessToken: string, refreshToken: string, profile: Profile): Promise<any> {
            const { id, displayName, emails, photos, provider } = profile;
            const user = {
                id,
                name: displayName,
                email: emails[0].value,
                profileImageUrl: photos[0].value,
                provider,
            };
            return user;
        }
    }
    • pasport-google의 Strategy를 extends해서 작성한다.
      'google'이라는 이름이 passport의 AuthGuard에 추가되어 추후 사용할 수 있다.

    • super에 넘겨주는 객체에 환경변수 값을 넣어준다.

    • validate method의 profile 매개변수에 사용자 정보가 모두 담겨있다.
      보통은 done 함수까지 매개변수로 받아서 done(null, user); 이런 식으로 반환하기는 하지만 바로 user 객체를 반환해도 문제없다.
      kakao, naver도 마찬가지이다.

    • request.user에 반환된 user 객체 값이 담긴다.

  • auth-google.controller.ts

    import { Controller, Get, Res, UseGuards } from '@nestjs/common';
    import { AuthGuard } from '@nestjs/passport';
    import { Response } from 'express';
    import { AuthService } from 'src/auth/auth.service';
    
    import { SocialData } from '../auth/interfaces/social-data.interface';
    import { CookieSettingHelper } from '../helpers/cookie-setting.helper';
    import { SocialProfile } from '../users/decorators/user.decorator';
    
    @Controller('auth/google')
    export class AuthGoogleController {
        constructor(
            private readonly authService: AuthService,
            private readonly cookieSettingHelper: CookieSettingHelper,
        ) {}
    
        @Get('login')
        @UseGuards(AuthGuard('google'))
        async googleAuthorize(): Promise<void> { // 로그인 페이지로 redirect }
    
        @Get('callback')
        @UseGuards(AuthGuard('google'))
        async googleCallback(
            @SocialProfile() socialData: SocialData,
            @Res({ passthrough: true }) response: Response,
        ): Promise<void> {
            const loginData = await this.authService.validateSocialLogin(socialData);
            this.cookieSettingHelper.setCookies(response, loginData);
            response.redirect('http://localhost:3000/me');
        }
    }
    • AuthGuard('google')을 사용해 앞서 작성한 GoogleStrategy를 사용하도록 해준다.

    • @SocialProfile()request.user에 담긴 값을 가져오는 decorator로 GoogleStrategy의 validate method에서 반환한 user 객체를 꺼내준다. (user.decorator.ts 참고)

    • 기존에는 직접 REST API로 authorizeCode를 access 및 refresh token으로 교환하고 이 token으로 사용자 정보를 가져왔는데, GoogleStrategy가 이 부분까지 맡아서 사용자 정보까지 가져와준다.
      kakao, naver도 마찬가지이다.
      그 이후의 로직은 동일하다.

  • auth-google.module.ts에서 providers에 GoogleStrategy를 추가해준다.

🟨passport-kakao

설치

npm i passport-kakao
npm i -D @types/passport-kakao

코드

  • kakao.strategy.ts

    import { Injectable } from '@nestjs/common';
    import { PassportStrategy } from '@nestjs/passport';
    import { Profile, Strategy } from 'passport-kakao';
    
    @Injectable()
    export class KakaoStrategy extends PassportStrategy(Strategy, 'kakao') {
        constructor() {
            super({
                clientID: process.env.CLIENT_ID,
                clientSecret: process.env.CLIENT_SECRET,
                callbackURL: process.env.REDIRECT_URI,
            });
        }
    
        async validate(accessToken: string, refreshToken: string, profile: Profile): Promise<any> {
            const { id, displayName, _json, provider } = profile;
            const user = {
                id: id.toString(),
                name: displayName,
                email: _json.kakao_account.email,
                profileImageUrl: _json.properties.profile_image,
                provider,
            };
            return user;
        }
    }
    • GoogleStrategy에서와는 달리 super에 넘겨주는 객체에 scope는 없다.

    • validate method의 profile 매개변수에 사용자 정보가 모두 담겨있다.
      email과 프로필 사진은 _json에서 꺼내야 한다.
      Kakao의 OAuth ID는 number type이지만 다른 provider들의 OAuth ID와 맞추기 위해 string type으로 변환해주었다.

    • request.user에 반환된 user 객체 값이 담긴다.

  • auth-kakao.controller.ts

    import { Controller, Get, Res, UseGuards } from '@nestjs/common';
    import { AuthGuard } from '@nestjs/passport';
    import { Response } from 'express';
    import { AuthService } from 'src/auth/auth.service';
    
    import { SocialData } from '../auth/interfaces/social-data.interface';
    import { CookieSettingHelper } from '../helpers/cookie-setting.helper';
    import { SocialProfile } from '../users/decorators/user.decorator';
    
    @Controller('auth/kakao')
    export class AuthKakaoController {
        constructor(
            private readonly authService: AuthService,
            private readonly cookieSettingHelper: CookieSettingHelper,
        ) {}
    
        @Get('login')
        @UseGuards(AuthGuard('kakao'))
        async googleAuthorize(): Promise<void> { // 로그인 페이지로 redirect }
    
        @Get('callback')
        @UseGuards(AuthGuard('kakao'))
        async googleCallback(
            @SocialProfile() socialData: SocialData,
            @Res({ passthrough: true }) response: Response,
        ): Promise<void> {
            const loginData = await this.authService.validateSocialLogin(socialData);
            this.cookieSettingHelper.setCookies(response, loginData);
            response.redirect('http://localhost:3000/me');
        }
    }
    • AuthGuard('kakao')를 사용해 앞서 작성한 KakaoStrategy를 사용하도록 해준다.
  • auth-kakao.module.ts에서 providers에 KakaoStrategy를 추가해준다.

🟩passport-naver

github를 찾아보니 네이버에서 만들었다.

설치

npm i passport-naver
npm i -D @types/passport-naver

코드

  • naver.strategy.ts

    import { Injectable } from '@nestjs/common';
    import { PassportStrategy } from '@nestjs/passport';
    import { Profile, Strategy } from 'passport-naver';
    
    @Injectable()
    export class NaverStrategy extends PassportStrategy(Strategy, 'naver') {
        constructor() {
            super({
                clientID: process.env.CLIENT_ID,
                clientSecret: process.env.CLIENT_SECRET,
                callbackURL: process.env.REDIRECT_URI,
            });
        }
    
        async validate(accessToken: string, refreshToken: string, profile: Profile): Promise<any> {
            const { id, _json, provider } = profile;
            const user = {
                id,
                name: _json.nickname,
                email: _json.email,
                profileImageUrl: _json.profile_image,
                provider,
            };
            return user;
        }
    }
    • GoogleStrategy에서와는 달리 super에 넘겨주는 객체에 scope는 없다.

    • validate method의 profile 매개변수에 사용자 정보가 모두 담겨있다.
      id와 provider를 제외한 값들은 _json에서 꺼내야 한다.

    • request.user에 반환된 user 객체 값이 담긴다.

  • auth-naver.controller.ts

    import { Controller, Get, Res, UseGuards } from '@nestjs/common';
    import { AuthGuard } from '@nestjs/passport';
    import { Response } from 'express';
    import { AuthService } from 'src/auth/auth.service';
    
    import { SocialData } from '../auth/interfaces/social-data.interface';
    import { CookieSettingHelper } from '../helpers/cookie-setting.helper';
    import { SocialProfile } from '../users/decorators/user.decorator';
    
    @Controller('auth/naver')
    export class AuthNaverController {
        constructor(
            private readonly authService: AuthService,
            private readonly cookieSettingHelper: CookieSettingHelper,
        ) {}
    
        @Get('login')
        @UseGuards(AuthGuard('naver'))
        async googleAuthorize(): Promise<void> { // 로그인 페이지로 redirect }
    
        @Get('callback')
        @UseGuards(AuthGuard('naver'))
        async googleCallback(
            @SocialProfile() socialData: SocialData,
            @Res({ passthrough: true }) response: Response,
        ): Promise<void> {
            const loginData = await this.authService.validateSocialLogin(socialData);
            this.cookieSettingHelper.setCookies(response, loginData);
            response.redirect('http://localhost:3000/me');
        }
    }
    • AuthGuard('naver')를 사용해 앞서 작성한 NaverStrategy를 사용하도록 해준다.
  • auth-naver.module.ts에서 providers에 NaverStrategy를 추가해준다.

Frontend(Client)에서 provider 별 로그인 API url로 "이동"하면 각각 소셜 로그인을 사용할 수 있다. (GET 요청 X)

REST API 기반으로 소셜 로그인을 구현하면서 로그인 과정을 모두 이해한 후에 provider 별 passport를 사용해보니 적용하기도 쉽고 간편해서 좋았다. 😁

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

0개의 댓글