본문 바로가기

프로젝트/세컨핸드

[기능 구현] OAuth를 통한 로그인 기능 구현

- 지역 기반 중고거래 사이트를 만드는 프로젝트에서 로그인 기능을 구현해야했고, 팀원들과 상의 결과 자체 로그인과 github OAuth 로그인 두 가지를 사용하기로 했다.

- 이 글에서는 OAuth에 대한 간단한 설명과 실제 프로젝트에서 github OAuth 로그인을 구현한 부분에 대해 설명하겠다.

OAuth(Open Authorization)란?

- OAuth는 권한 부여를 위한 공개된 인증 프로토콜(접근 권한을 위임하는 개방형 표준 프로토콜), 제3자 애플리케이션에 대한 보안 인증 및 권한 부여 프로토콜이다.

- 쉽게 설명해서 애플리케이션(Client)에서 별도의 회원가입 없이 사용자(Resource owner)를 대신하여 리소스 서버(Resource server)에서 제공하는 자원에 대한 접근 권한을 위임하는 방식으로, 인증(Authentication)과 인가(Authorization)를 받기 위한 것으로 볼 수 있다. 즉, 애플리케이션에서 사용자가 자신이 누구인지 확인하고, 원하는 정보를 얻도록 하는 과정을 말한다.

 

- 다음은 OAuth 주요 용어들에 대한 설명이다.

- 인증(Authentication) : 인증, 접근 자격이 있는지 검증하는 단계

- 인가(Authorization) : 인가, 자원에 접근할 권한을 부여하는 것으로 인가가 완료되면 리소스 접근 권한이 담긴 Access Token이 서비스 제공자(Client)에게 부여됨

- 사용자(Resource owner) : 리소스 소유자 또는 사용자. 보호된 자원에 접근할 수 있는  자격을 부여해 주는 주체

- 서비스 제공자(Client) : 보호된 자원을 사용하려고 접근 요청을 하는 애플리케이션

- Authorization Server : 인증을 담당하는 서버

- Resource Server : 리소스를 서비스 제공자(Client)에게 제공해주는 서버

 

OAuth 흐름

- 프로젝트에서 사용한 OAuth의 흐름은 다음과 같다.

- Resource owner는 로그인을 원하는 사용자, Client는 프로젝트의 애플리케이션, Authorization Server는 github 인증서버, Resource owner는 github 데이터 서버이다.

1 ~ 4. 로그인 요청 / 로그인 페이지 제공 / ID,PW 제공

- 사용자가 "Github 계정으로 로그인" 버튼을 누르면 github app에 등록된 client id, redirectUri(로그인이 성공하면 이동할 주소), socpe(필요한 정보의 범위)와 함께 github의 OAuth 인증 프로세스를 시작하기 위한 주소로 이동한다.

- redirectUri는 React 라우트로 AuthPage로 지정합니다. 즉, 사용자가 github 로그인을 완료하면 AuthPage로 이동한다.

const scope = 'user';
const redirectUri = `${URL}/redirect/oauth`;
const clientId = OAUTH_CLIENT_ID;

const githubLoginBtnHandler = () => {
    window.location.href = `https://github.com/login/oauth/authorize?response_type=code&redirect_uri=${redirectUri}&client_id=${clientId}&scope=${scope}`;
  };
// routes.tsx
export const router = createBrowserRouter([
  {
    path: '/',
    children: [
      {
        path: '/',
        element: <HomePage />,
      },
      { path: 'redirect/oauth', element: <AuthPage /> },
      ...
    ],
  },
]);

- 사용자는 github가 제공하는 로그인 페이지에서 github의 id와 pw를 제공하여 로그인을 한다.

 

5 ~ 12.  Authorization Code 발급

- 로그인에 성공한 사용자는 쿼리스트링에 Authorization Code를 포함하여 redirectUri에 등록된 AuthPage로 이동한다.

- AuthPage 컴포넌트에서는 쿼리스트링에 포함된 Authorization Code를 api를 통해 백엔드 서버로 전송한다.

- 백엔드단에서는 전송받은 Authorizaion Code로 github에서 Access Token을 발급 받고 Access Token으로 github에서 사용자 정보를 받는다.

- 이후 백엔드는 프론트에게 JWT 토큰과 사용자 정보를 전송하고 프론트는 JWT 토큰과 사용자 정보를 localStorage에 저장하고 사용자 인증이 필요한 API 요청의 경우 헤더에 JWT 토큰을 담아 전송한다.

// AuthPage.tsx
import { useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { API_URL } from '@constants/apis';
import { USER_INFO_KEY } from '@constants/login';

export const AuthPage = () => {
  // useLocation을 사용해 현재 URL 정보를 획득
  const location = useLocation();
  // 로그인이 완료되면 HomePage로 이동하기 위한 navigate 선언
  const navigate = useNavigate();
  // new URLSearchParams를 사용하여 현재 URL의 쿼리 문자열에서 code 매개변수 값을 추출
  const queryCode = new URLSearchParams(location.search).get('code');

  // 백엔드에게 Authorization Code를 전송하고 JWT 토큰과
  // 사용자 정보(id, 프로필 사진, 지역정보 등)를 받기 위한 함수
  const getTokenAndUserInfo = async (code: string | null) => {
    const response = await fetch(
      `https://www.guardiansofthecodesquad.site/login/oauth/github?code=${code}`
    );
    const data = await response.json();
    return data;
  };

  const runGetLoginUserInfoAPI = async () => {
    const userInfoData = await getGithubLoginToken(queryCode);
    if (!userInfoData) {
      return;
    }
    
    const token = userInfoData.data.token;
    const memberInfo = userInfoData.data.memberInfo;
    
    // JWT 토큰과 사용자 정보를 localStorage에 저장
    localStorage.setItem('loginToken', token);
    localStorage.setItem(USER_INFO_KEY, JSON.stringify(memberInfo));
    
    // 모든 작업 완료 후 HomePage로 이동
    navigate('/')
   }

  useEffect(() => {
    runGetLoginUserInfoAPI();
  }, []);  
  
  return <></>;
};
// JWT 토큰을 헤더에 담아 사용자 인증을 하는 예시
// 사용자가 선택한 지역을 사용자 정보에 PUT 하는 함수
export const putUserLocation = async (
  mainLocationIdx: number | null | undefined,
  subLocationIdx: number | null | undefined
) => {
  try {
    const token = localStorage.getItem('loginToken');
    const response = await fetch(`${API_URL}/location`, {
      method: 'PUT',
      headers: {
        Authorization: `Bearer ${token}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        main: mainLocationIdx,
        sub: subLocationIdx,
      }),
    });

    const userLocationData = await response.json();

    if (!response.ok) {
      throw new Error(ERROR_MESSAGE['UNDEFINED']);
    }
    return userLocationData.data;
  } catch (error) {
    return error;
  }
};

배운 점과 느낀 점

- OAuth의 개념과 기본 용어부터 학습할 내용이 많았지만, 정확하게 이해하고 넘어가기 위해 프로젝트 기간 중 시간을 많이 투자했던 기능이었다.

- 기능 구현 중 어려운 부분이 있었지만, OAuth 로그인 구현 경험이 있던 프론트엔드 동료 스눕과 백엔드 동료 로이, 고뭉남 덕분에 github OAuth 로그인 기능을 구현해서 기뻤고, 함께해준 동료들에게 감사의 인사를 전하고 싶다🙇‍♂️


학습 단계로 잘못된 정보가 있을 수 있습니다. 잘못된 부분에 대해 알려주시면 정정하도록 하겠습니다.

참고 : https://datatracker.ietf.org/doc/html/rfc6749#section-4.1

https://rrecoder.tistory.com/148

https://blog.naver.com/mds_datasecurity/222182943542

https://velog.io/@shyuuuuni/OAuth-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0-with-github-%EB%A1%9C%EA%B7%B8%EC%9D%B8

https://showerbugs.github.io/2017-11-16/OAuth-%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C

https://barbera.tistory.com/62

https://velog.io/@chuu1019/%EC%95%8C%EA%B3%A0-%EC%93%B0%EC%9E%90-JWTJson-Web-Token

https://velog.io/@kyeongsoo5196/OAuth-%EC%A0%95%EB%A6%AC