프로젝트에서 소셜 로그인을 구현하게 되었는데, 실제로 구현하면서 복잡한 부분들이 많았다.
특히 OAuth 플로우를 이해하고 구현하는 게 처음에는 계속 헷갈렸다.
쉽게 설명하면 "다른 서비스의 인증을 빌려쓰는 것"
OAuth는 인터넷 사용자들이 비밀번호를 제공하지 않고 다른 웹사이트 상의 자신들의 정보에 대해 웹사이트나 애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단으로서 사용되는, 접근 위임을 위한 개방형 표준이다.(위키백과)
ex)
1. 카카오톡으로 로그인하기 버튼 누르기
2. 카카오에서 "이 서비스에 로그인 해줄까요?" 하고 물어보는 모달이 생성
3. 아이디, 비밀번호 입력 후 OK 누르면 우리 서비스에 로그인이 된다
얼핏 보면 간단해도 이게 내부적으로는 꽤 복잡한 과정을 가진다.
내가 구현한 OAuth(카카오) 로그인의 전체 플로우는 아래와 같다.
프론트에서 카카오로 로그인 요청
// pages/login.tsx
const handleKakaoLogin = () => {
const KAKAO_AUTH_URL = `https://kauth.kakao.com/oauth/authorize?client_id=${KAKAO_CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code`;
window.location.href = KAKAO_AUTH_URL;
};
카카오 동의 화면 -> 리다이렉션
code
파라미터가 붙는다.https://<도메인>/oauth/success/kakao?code=P2F0HGlyDP6seEjGnHbANy_d0YmXgUXHLz5Fp1LS1ws6oxjEevXS2AAAAAQKPXRpAAAB42BDz1szkZmFRA
인가 코드 추출하기
// app/oauth/kakao/page.tsx
export default function KakaoCallback() {
const searchParams = useSearchParams();
const code = searchParams.get('code');
useEffect(() => {
if (code) {
sendCodeToBackend(code);
}
}, [code]);
// ...
}
백엔드에서 인증 처리 (백엔드)
// env.local
NEXT_PUBLIC_KAKAO_CLIENT_ID="your_client_id" // 유출되어서는 안됌
NEXT_PUBLIC_KAKAO_REDIRECT_URI="http://localhost:3000/oauth/kakao"
// components/KakaoLoginButton.tsx
export default function KakaoLoginButton() {
const handleLogin = () => {
const KAKAO_AUTH_URL = `https://kauth.kakao.com/oauth/authorize?client_id=${process.env.NEXT_PUBLIC_KAKAO_CLIENT_ID}&redirect_uri=${process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI}&response_type=code`;
window.location.href = KAKAO_AUTH_URL;
};
return (
<button
onClick={handleLogin}
className="w-full bg-[#FEE500] text-[#000000] py-2 rounded-md"
>
카카오로 시작하기
</button>
);
}
// app/oauth/kakao/page.tsx
'use client';
import { useEffect } from 'react';
import { useSearchParams, useRouter } from 'next/navigation';
export default function KakaoCallback() {
const searchParams = useSearchParams();
const router = useRouter();
const code = searchParams.get('code');
useEffect(() => {
if (code) {
// 1. 백엔드로 인가 코드 전송
fetch('/api/auth/kakao', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ code }),
})
.then((res) => res.json())
.then((data) => {
// 2. 백엔드에서 받은 JWT 토큰 저장
localStorage.setItem('accessToken', data.accessToken);
localStorage.setItem('refreshToken', data.refreshToken);
router.push('/');
})
.catch((error) => {
// 3. 로그인이 실패한다면 /login으로 리다이렉션
console.error('Login failed:', error);
router.push('/login');
});
}
}, [code, router]);
return (
<div className="flex justify-center items-center min-h-screen">
<div className="animate-spin">로그인 처리 중...</div>
</div>
);
}
리다이렉트 URI 설정
환경변수 관리
NEXT_PUBLIC_
접두어를 무조건 붙여야 함. 클라이언트에서 쓰는 변수라면 무조건 써야 한다. 근데 중요한 정보라면 NEXT_PUBLIC
붙여서 쓰면 안됨. 클라이언트에서 관리하기 때문에 노출이 되는 정보이다.토큰 보안
localStorage에 토큰을 저장하면 JavaScript로 쉽게 접근할 수 있다. 만약 XSS(Cross-Site Scripting) 공격을 당하게 된다면 악성 스크립트가 localStorage에 접근해서 토큰을 탈취할 수 있다. 토큰이 탈취된다면 탈취된 토큰으로 해커가 사용자인 척 API를 호출할 수 있다. (훔친 민증으로 술, 담배 사는?)
그래서 액세스 토큰을 쿠키로 관리하고, HttpOnly 옵션을 활용하는 것이 좋다. 쿠키가 브라우저에서 JavaScript로 접근할 수 없도록 하는 옵션이다. 서버에서만 읽고 쓸 수 있게 된다. 이 과정은 다음 블로그로 추가로 기술하겠다.
const refreshAccessToken = async (refreshToken: string) => {
// 토큰 갱신 로직
};
OAuth 구현하면서 느낀 점. 플로우만 잘 이해하면 생각보다 어렵지 않다고는 하는데 그 플로우가 계속 헷갈리면서 삽질을 여러 번 했다. 로그인이라는게 보안과 관련된 부분이라 빨리 빨리 라는 마인드로 넘길 수 없어서 시간은 꽤나 걸렸지만 리프레쉬 토큰까지 잘 활용해 구현해서 좋은 경험을 할 수 있었다.