Google OAuth 로그인 풀스택 구현 (feat. JWT 구글 OAuth 로그인 버튼 커스텀)

김지환·2024년 8월 6일

Nest.js + React 풀스택으로 구글 OAuth 구현해봤다.

어쩌다보니
1. Nest 백엔드만 있는 경우
2. 풀스택 모두 가능한 경우
-2.1 React에서 <GoogleLogin/> 을 사용하는 경우
-2.2 React에서 useGoogleLogin을 사용하는 경우
모두 진행해봤다.

1. Nest 백엔드만 있는 경우

보통의 OAuth의 흐름은
클라이언트에서 google 이라는 회사에 어떠한 토큰을 발급받아
이를 백엔드에 넘겨준 후
백엔드에서 해당 토큰을 가공해서 사용자 정보를 얻거나
accessToken 등의 토큰을 만들어준다.

하지만 프론트가 없을 때에도 아래의 방법으로 토큰을 발급받을 수 있다.

  // auth.controller.ts
  // 백엔드 api 설계 중 토큰이 필요할 때 사용
  @Get('to-google')
  @UseGuards(GoogleAuthGuard)
  async googleAuth(@Request() req) {}

  @Get('google')
  @UseGuards(GoogleAuthGuard)
  async googleAuthRedirect(@Request() req, @Response() res) {
    const user = req.user;
	// user를 정보를 바탕으로 토큰 생성하는 서비스 작성 필요
    const tokens = await this.authService.getTokens(user);

    res.json(tokens);
  }
//GoogleAuthGuard.ts 

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

@Injectable()

export class GoogleAuthGuard extends AuthGuard('google') {
  async canActivate(context: any): Promise<boolean> {
    const result = (await super.canActivate(context)) as boolean;
    const request = context.switchToHttp().getRequest();
    await super.logIn(request);
    return result;
  }
}

이 부분은 블로그에 더 자세한 설명이 나와있다.

이렇게 google 로그인 기능을 만들고 React 프로젝트에서 사용하려고 했는데
당연히 될 리가 없다.
프론트가 있으면 프론트에서 google로부터 받은 인증 토큰을 백으로 넘겨줘야하기 때문에..

2.1 React에서 <GoogleLogin/> 을 사용하는 경우

<GoogleLogin/>을 사용하면 구글 로그인 버튼을 사용자가 커스텀 할 수 없는 단점이 있다.
하지만, 구현은 조금 더 간단하다.

react-oauth/google 라이브러리를 활용했다.

  const handleGoogleLoginSuccess = async (response: any) => {
    // 이 부분이 2.2에서 다르다.
    const credential = response.credential;
    console.log(response);

    if (credential) {
      // 백엔드에 credential 전달
      mutation.mutate(credential);
    } else {
      console.error('Credential is missing');
    }
  };

  const handleGoogleLoginError = () => {
    console.log('Login Failed');
  };

<GoogleOAuthProvider clientId={import.meta.env.VITE_GOOGLE_CLIENT_ID}>
          <GoogleLogin
            onSuccess={handleGoogleLoginSuccess}
            onError={handleGoogleLoginError}
          />
</GoogleOAuthProvider>

Nest 백엔드는 다음과 같다.

// auth.controller.ts
 @Post('google')
  @UsePipes(new ValidationPipe())
  async googleAuthRedirect(
    @Body() googleOAuthDto: GoogleOAuthDto,
    @Response() res,
  ) {
    const payload =
      await this.authService.validateGoogleOAuthDto(googleOAuthDto);

    const user = await this.authService.findUserByEmailOrSave(
      payload.email,
      payload.family_name + payload.given_name,
      payload.sub,
    );

    const myTokens = await this.authService.getTokens(user);
    res.json(myTokens);
  }
//auth.service.ts
  async validateGoogleOAuthDto(googleOAuthDto: GoogleOAuthDto) {
    try {
 	  const { token } = googleOAuthDto;

      // Google 토큰 검증
      const ticket = await this.oauthClient.verifyIdToken({
        idToken: token,
        audience: this.configService.get<string>('GOOGLE_CLIENT_ID'),
      });
	  // decode 유저 정보 반환
      return ticket.getPayload();
    } catch {
      // TODO:
      console.log('error while verify googleOAuthDto');
      throw new BadRequestException();
    }
  }

2.2 React에서 useGoogleLogin 을 사용하는 경우

이 경우는 구글 로그인 버튼 커스텀이 가능하다

import styled from 'styled-components';
import { useGoogleLogin } from '@react-oauth/google';
import { useAuth } from '../../hooks/api/auth/useAuth';

const handleGoogleLoginSuccess = async (response: any) => {
  // 이 부분이 2.1과 다르다
  const credential = response.code;

  if (credential) {
    mutation.mutate(credential);
  } else {
    console.error('Credential is missing');
  }
};

export default function CustomGoogleLoginButton() {
  const { handleGoogleLoginSuccess, handleGoogleLoginError } = useAuth();
  const login = useGoogleLogin({
    onSuccess: handleGoogleLoginSuccess,
    onError: handleGoogleLoginError,
    flow: 'auth-code',
  });
  return (
    <StyledButton type="button" onClick={() => login()}>
      <span>Google 계정으로 로그인</span>
    </StyledButton>
  );
}
// auth.service.ts
async validateGoogleOAuthDto(googleOAuthDto: GoogleOAuthDto) {
  try {
    // 구글 code(dto : token) 검증
    // 이 부분이 추가됐다.
    const { tokens } = await this.oauthClient.getToken(googleOAuthDto.token);
    // Google id 토큰 검증
    const ticket = await this.oauthClient.verifyIdToken({
      idToken: tokens.id_token,
      audience: this.configService.get<string>('GOOGLE_CLIENT_ID'),
    });
    // decode 유저 정보 반환
    return ticket.getPayload();
  } catch {
    // TODO:
    console.log('error while verify googleOAuthDto');
    throw new BadRequestException();
  }
}

이러면 2.2에서는 버튼 커스텀이 가능하다.
JWT(access, refresh)도 구글 api를 통해서 발급받을 수 있지만
언제 변경될 지 모르기 때문에 JWT 발급 부분은 passport를 활용해서 따로 구현하였다.

구글 로그인 버튼 커스텀 하려다가 찍먹에서 푹먹해버렸다.

구글 로그인 버튼 커스텀에 어려움을 겪고 있다면
github issue를 읽어보면 더 이해가 잘 갈 것이다.

profile
세상의 문제 해결을 즐기는 프론트엔드 개발자

0개의 댓글