간편 로그인(OAuth 2.0) 3분만에 정복하기
아이디 & 비밀번호 없이 소셜로그인을 구현해보자
간편 로그인이란?
: 비밀번호를 제공하지 않고 다른 웹사이트 서비스에 접근할 수 있는 접근성 위임 기능
이런 거 있잖아요
저걸 만드는 명세의 표준이 바로 OAuth 2.0이다.
왜 2.0인가?
출처: Stackacademic
이것만큼 OAuth 1.0과 2.0의 차이에 대해서 잘 설명한 그림이 없어서 들고와봤다. 사실 Token 교환을 보여주는 것이라면 더 잘 설명한 그림이 있겠으나, 이 그림을 좋아하는 이유는 "User Flow" 단위로 보여주기 때문이다. 브라우저가 되어서 플로우를 따라가면서 무슨 일이 일어나는지 체험할 수 있다.
난 공식 Docs를 보는 간지나는 행동을 추구하는 사람이기 때문에 명세 표준을 보면서 한번 어떻게 동작하는 물건인지 이해해보자.
RFC 6750-OAuth 2.0 Authorization Framework
개발자들답게 무슨 Flow도 아스키 아트로 그려놨다. 먼저 네 가지의 요소가 존재해야 하며, Client/Resource Owner/Authorization Server/Resource Server이 바로 그것이다. 이 그림은 유저와 서버 간의 관계가 아니라, 특정 서비스를 제공하는 서버와 인증 서버 간의 관계를 표현한 것이라는 데에 주의를 기울여야 한다.
카카오톡 소셜로그인을 개발한다고 하자. 카카오톡 서버가 Resource Owner 겸 Authorization Server의 역할을 하고 (아마 리버스 프록시같은 걸 써서 도메인별로 분리를 할 것이다.) Client는 내가 이용하고자 하는 특정 서비스의 서버이다. 유저가 로그인 요청을 하면 카카오톡 로그인 페이지로 Redirection을 해 주고 Authorization을 요청해서 승인을 받은 다음, 이 승인을 다시 액세스 토큰으로 바꿔먹는다. 그 다음부터는 Access Token을 활용해 내 서비스의 서버가 카카오톡 리소스 서버에서 유저 정보에 관한 내용을 마음껏 빼먹을 수 있다.
직접 개발한 서비스 중에 Graduart의 코드를 참고해보겠다. Google OAuth 2.0을 이용한 소셜로그인 기능이 구현되었다.
먼저 프론트엔드 퍼블리싱 요소를 보자.
return (
<div onClick={handleSignIn} align={"center"}>
<img
alt={"Google Login Button"}
src={`/assets/googleLoginImg.png`}
width={"50%"}
></img>
</div>
);
이렇게 구글 로그인 이미지를 가져오는 컴포넌트를 반환한다. 클릭시에 호출되는 handleSignIn함수가 중요한데, 로직을 살펴보면
const handleSignIn = async (response) => {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: "google",
options: {
redirectTo: import.meta.env.VITE_FRONT_URL,
},
});
if (error) {
console.error("Error signing in with Google:", error.message);
} else {
const { user } = data;
if (user) {
localStorage.setItem("supabase.auth.token", JSON.stringify(user));
navigate("/");
}
}
};
supabase 백엔드를 활용했기 때문에, 관련 내용이 보인다. 우리 백엔드에 redirection URL을 포함해 "너 구글이랑 잘 연락해서 우리 유저 받아봐" 라는 요청을 날리라고 시키는 내용이다. 이 뒤에서 요청을 받은 후 일어나는 백엔드 로직을 살펴보자.
(여기부터는 Supabase 코드 아님, 임의로 짠 코드) NestJS를 통해서 받아보자.
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-oauth2';
import axios from 'axios';
@Injectable()
export class KakaoOAuthStrategy extends PassportStrategy(Strategy, 'kakao') {
constructor() {
super({
authorizationURL: 'https://kauth.kakao.com/oauth/authorize',
tokenURL: 'https://kauth.kakao.com/oauth/token',
clientID: process.env.KAKAO_CLIENT_ID,
clientSecret: process.env.KAKAO_CLIENT_SECRET,
callbackURL: 'http://localhost:3000/auth/kakao/callback',
});
}
async validate(accessToken: string, refreshToken: string, profile: any, done: Function) {
try {
const response = await axios.get('https://kapi.kakao.com/v2/user/me', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
const { id, kakao_account } = response.data;
const user = {
id,
email: kakao_account.email,
nickname: kakao_account.profile.nickname,
};
done(null, user);
} catch (err) {
done(err, false);
}
}
}
먼저 src/auth/strategies/kakao.strategy.ts 파일을 생성한다. 이 파일을 적용시킬 모듈은
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { KakaoOAuthStrategy } from './strategies/kakao-oauth.strategy';
import { AuthController } from './auth.controller';
@Module({
imports: [PassportModule.register({ defaultStrategy: 'kakao' })],
controllers: [AuthController],
providers: [KakaoOAuthStrategy],
})
export class AuthModule {}
이렇게 만들어주면 된다. (Passport 패키지 설치 필요) 마지막으로
import { Controller, Get, Req, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Request } from 'express';
@Controller('auth')
export class AuthController {
@Get('kakao')
@UseGuards(AuthGuard('kakao'))
async kakaoLogin() {
// 카카오 로그인 페이지로 리디렉션합니다.
}
@Get('kakao/callback')
@UseGuards(AuthGuard('kakao'))
async kakaoLoginCallback(@Req() req: Request) {
// 카카오 로그인 성공 후 리디렉션된 후 처리 로직
const user = req.user;
return {
message: 'Kakao login successful',
user,
};
}
}
이렇게 짜주면 우리 백엔드에서 user을 반환하게 된다. JSON을 파싱하면 이렇게 생겼다. (동의항목을 통해 가져오는 추가 유저 정보에 대한 고려는 일단 하지 말자.)
{
id: 1234567890,
email: 'user@example.com',
nickname: '새싹버섯'
}
ID의 경우 카카오 서버에서 내부적으로 유저들에게 부여하는 고유 ID이다. 뭔가 남의 서비스 DB에 있는 은밀한 정보를 빼내오는 느낌이라 내 ID가 궁금해지는 부분이다.
NestJS를 잘 알지 못하지만 지금 개발중인 서비스가 NestJS를 써야 해서 공부하면서 코드를 가져와봤는데, @UseGuards 데코레이터를 처음 봐서 좀더 알아보았다. Guard는 HTTP 요청이 처리되기 전에 실행되어, 특정 요청에 대한 접근을 허용하거나 거부하는 역할을 한다. 클라이언트가 요청을 보내면 해당 Guard가 먼저 실행되어 인증 또는 권한을 검증한 후에, 요청을 처리할 수 있는지 여부를 결정하는 방식이라고 한다.
카카오톡을 예시로 OAuth 2.0을 이용한 간편로그인의 개념과 진행과정, 그리고 백엔드단에서 필요한 코드에 대해 간략히 알아봤다. 앞으로 잘 써먹어야겠다.