본문으로 건너뛰기

NestJS-passport-구글-로그인-구현

NestJS에서 passport로 구글 로그인 인증 구현에 대한 내용을 정리하는 글입니다.

Passport

Passport는 Node.js 용으로 널리 사용되는 인증 미들웨어입니다. 수백개의 인증 전략을 지원하여 로그인 및 인증 관리를 쉽게 구현할 수 있도록 도와줍니다.

현재 구현한 방식은 passport-google-oauth20 Strategy 으로 구글 로그인 후 얻은 정보로 로컬에서 jwt토큰을 다시 만들었습니다. 여러개의 소셜 프로바이더가 추가되어도 단일 토큰으로 관리할 수 있어서 이후 개발이 간편해지기 때문입니다.

구글 로그인 구현

yarn add @nestjs/passport passport passport-google-oauth20 passport-jwt
yarn add -D @types/passport-google-oauth20 @types/passport-jwt

필요한 라이브러리들을 설치합니다.

auth 모듈

// auth.module.ts
import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';

import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { GoogleStrategy } from './strategy/google.strategy';
import { JwtStrategy } from './strategy/jwt.strategy';

@Module({
imports: [
JwtModule.registerAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
secret: config.get('AUTH_JWT_SECRET'),
signOptions: {
expiresIn: '3d',
},
}),
}),
],
controllers: [AuthController],
providers: [AuthService, GoogleStrategy, JwtStrategy],
})
export class AuthModule {}

Auth 모듈을 생성합니다.

먼저 import 문에서 JwtModule을 등록합니다. 이 모듈을 등록해야 인증 후 jwt 토큰을 생성할 jwtService를 사용할 수 있습니다. 위 코드에선 ConfigModule을 사용하고 있어서 비동기적으로 환경변수가 사용가능할 때 등록하도록 registerAsync 메서드를 사용했습니다. jwt의 시크릿과 만료일자를 설정합니다.

그리고 로그인에 사용할 컨트롤러와 서비스, 인증 전략들도 모듈에 등록합니다.

구글 strategy

// google.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-google-oauth20';

@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
constructor() {
super({
clientID: process.env.AUTH_GOOGLE_CLIENT_ID,
clientSecret: process.env.AUTH_GOOGLE_SECRET,
callbackURL: 'http://localhost:3001/auth/google/redirect',
scope: ['email', 'profile'],
});
}

async validate(
accessToken: string,
refreshToken: string,
profile: any,
): Promise<any> {
return {
provider: 'google',
providerId: profile.id,
name: profile.displayName,
email: profile.emails[0].value,
picture: profile.photos[0].value,
};
}
}

구글 클라우드 콘솔 에서 사용자 인증정보를 등록 후 clientId, clientSecret, callbackUrl을 등록합니다.

validate에서는 설정한 프로필 정보 중 필요한 정보들을 추려서 리턴합니다. 리턴값은 req.user로 설정되어 추후 req에 접근할 수 있는 미들웨어나 컨트롤러에서 활용할 수 있습니다.

컨트롤러

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

import { AuthService } from './auth.service';

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

@Get('/google')
@UseGuards(AuthGuard('google'))
googleAuth() {
return null;
}

@Get('/google/redirect')
@UseGuards(AuthGuard('google'))
googleAuthRedirect(@Req() req) {
return this.authService.login(req.user);
}
}

구글 로그인에 사용할 path를 등록합니다.

/auth/google 접속시 구글도메인의 로그인 페이지로 리다이렉트 되며, 로그인을 마치면 /auth/google/redirect 페이지로 되돌아옵니다.

백-프론트로 나눠진 구조에서는 프론트엔드에서 인증 후 리다이렉트 받을 url을 쿼리로 같이 보낼텐데요, 해당 url로 jwt토큰과 함께 리다이렉트 시켜야 하지만 아직 프론트와 연동은 진행하지 않았기에 생략합니다.

서비스

import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthService {
constructor(private jwtService: JwtService) {}

login(user: { provider: string; providerId: string }) {
const { provider, providerId } = user;

return this.jwtService.sign({
provider,
providerId,
});
}
}

서비스에는 동작 확인용도로 wt 토큰을 만들어서 리턴해줬습니다.

원래라면 데이터베이스에서 유저 조회를 하고, 만약 해당하는 유저가 없으면 유저 생성도 해야 하지만 아직 데이터베이스 연동을 진행하지 못했기에 생략했습니다.

jwt 토큰에는 많은 유저를 식별하는데 필요한 최소 정보로 provider와 providerId만 payload에 넣었습니다.

여기까지 하면 구글 로그인이 정상적으로 작동함을 확인 할 수 있습니다.

jwt 인증 처리

jwt 인증 처리

// jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.AUTH_JWT_SECRET,
});
}

async validate(payload: any) {
return { provider: payload.provider, providerId: payload.providerId };
}
}

가드에 사용할 jwt 인증 전략입니다.

프론트엔드에서 Http 헤더의 Authorization 값에 Bearer {token} 포맷으로 jwt 토큰을 전달합니다. 이 토큰위치를 식별하기 위해 jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken() 를 설정합니다.

ignoreExpiration 속성은 액세스 토큰 만료시 자동으로 401에러를 반환합니다.

// jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

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

JwtAuthGuard를 추가했습니다.

해당 구현에서 canActivate 메서드를 오버라이드 하여 토큰 유효성 검사를 커스텀 할 수 있습니다.

import { Controller, Get, Req, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';

@Controller()
export class AppController {
@Get('/profile')
@UseGuards(JwtAuthGuard)
getProfile(@Req() req) {
return req.user;
}
}

위의 가드를 사용하여 컨트롤러에 적용한 예시입니다.