Nest.js + React 풀스택으로 구글 OAuth 구현해봤다.
어쩌다보니
1. Nest 백엔드만 있는 경우
2. 풀스택 모두 가능한 경우
-2.1 React에서 <GoogleLogin/> 을 사용하는 경우
-2.2 React에서 useGoogleLogin을 사용하는 경우
모두 진행해봤다.
보통의 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로부터 받은 인증 토큰을 백으로 넘겨줘야하기 때문에..
<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();
}
}
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를 읽어보면 더 이해가 잘 갈 것이다.