[Front] Next.js에서 OAuth 구현하기(without 라이브러리) 🔑

devicii·2024년 11월 29일
0

Front

목록 보기
4/8

NextJS 14에서 카카오 OAuth 구현하기 🔑

0. 들어가기 전에

프로젝트에서 소셜 로그인을 구현하게 되었는데, 실제로 구현하면서 복잡한 부분들이 많았다.
특히 OAuth 플로우를 이해하고 구현하는 게 처음에는 계속 헷갈렸다.

1. OAuth가 뭔데?

쉽게 설명하면 "다른 서비스의 인증을 빌려쓰는 것"

OAuth는 인터넷 사용자들이 비밀번호를 제공하지 않고 다른 웹사이트 상의 자신들의 정보에 대해 웹사이트나 애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단으로서 사용되는, 접근 위임을 위한 개방형 표준이다.(위키백과)

ex)
1. 카카오톡으로 로그인하기 버튼 누르기
2. 카카오에서 "이 서비스에 로그인 해줄까요?" 하고 물어보는 모달이 생성
3. 아이디, 비밀번호 입력 후 OK 누르면 우리 서비스에 로그인이 된다

얼핏 보면 간단해도 이게 내부적으로는 꽤 복잡한 과정을 가진다.

2. OAuth 플로우 이해하기

내가 구현한 OAuth(카카오) 로그인의 전체 플로우는 아래와 같다.

  1. 프론트에서 카카오로 로그인 요청

    // 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;
    };
  2. 카카오 동의 화면 -> 리다이렉션

    • 사용자가 동의하면 이미 설정된 리다이렉트 URI로 리다이렉션 된다.
    • 이때 URI주소 뒤에 code 파라미터가 붙는다.

    https://<도메인>/oauth/success/kakao?code=P2F0HGlyDP6seEjGnHbANy_d0YmXgUXHLz5Fp1LS1ws6oxjEevXS2AAAAAQKPXRpAAAB42BDz1szkZmFRA

  3. 인가 코드 추출하기

    // app/oauth/kakao/page.tsx
    export default function KakaoCallback() {
      const searchParams = useSearchParams();
      const code = searchParams.get('code');
      
      useEffect(() => {
        if (code) {
          sendCodeToBackend(code);
        }
      }, [code]);
      
      // ...
    }
  4. 백엔드에서 인증 처리 (백엔드)

    • 받은 코드로 백엔드 개발자가 카카오 서버에 실제 토큰을 요청
    • 유저 정보를 받아온다
    • JWT 토큰을 만들어서 프론트로 보내줌

3. 구현하기

3-1. 환경 설정

// env.local
NEXT_PUBLIC_KAKAO_CLIENT_ID="your_client_id" // 유출되어서는 안됌
NEXT_PUBLIC_KAKAO_REDIRECT_URI="http://localhost:3000/oauth/kakao"

3-2. 로그인 버튼 만들기

// 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>
  );
}

3-3. 콜백 처리하기

// 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>
  );
}

4. 주의할 점

  1. 리다이렉트 URI 설정

    • 카카오 개발자 센터에 등록한 URI와 정확히 일치해야 한다. 슬래시(/) 하나 차이로도 에러가 난다.
  2. 환경변수 관리

    • NEXT_PUBLIC_ 접두어를 무조건 붙여야 함. 클라이언트에서 쓰는 변수라면 무조건 써야 한다. 근데 중요한 정보라면 NEXT_PUBLIC 붙여서 쓰면 안됨. 클라이언트에서 관리하기 때문에 노출이 되는 정보이다.
  3. 토큰 보안

    • 액세스 토큰은 보안에 유의해야 한다.

4-1 토큰 보안

  • localStorage에 토큰을 저장하면 JavaScript로 쉽게 접근할 수 있다. 만약 XSS(Cross-Site Scripting) 공격을 당하게 된다면 악성 스크립트가 localStorage에 접근해서 토큰을 탈취할 수 있다. 토큰이 탈취된다면 탈취된 토큰으로 해커가 사용자인 척 API를 호출할 수 있다. (훔친 민증으로 술, 담배 사는?)

  • 그래서 액세스 토큰을 쿠키로 관리하고, HttpOnly 옵션을 활용하는 것이 좋다. 쿠키가 브라우저에서 JavaScript로 접근할 수 없도록 하는 옵션이다. 서버에서만 읽고 쓸 수 있게 된다. 이 과정은 다음 블로그로 추가로 기술하겠다.

5. 더 고려해야할 점

  1. 액세스 토큰 만료 이슈
    • 리프레시 토큰으로 자동 갱신하는 로직 추가. 액세스 토큰 만료 시간은 짧게 두고, 리프레쉬 토큰 만료 시간을 길게 가져가는 방법을 대부분 취한다.
    const refreshAccessToken = async (refreshToken: string) => {
      // 토큰 갱신 로직
    };

6. 마무리

OAuth 구현하면서 느낀 점. 플로우만 잘 이해하면 생각보다 어렵지 않다고는 하는데 그 플로우가 계속 헷갈리면서 삽질을 여러 번 했다. 로그인이라는게 보안과 관련된 부분이라 빨리 빨리 라는 마인드로 넘길 수 없어서 시간은 꽤나 걸렸지만 리프레쉬 토큰까지 잘 활용해 구현해서 좋은 경험을 할 수 있었다.

참고

OAuth로그인 참고 블로그

생활코딩 - JWT

생활코딩 - OAuth2

profile
흘러가는대로 사는

0개의 댓글