Github 로그인으로 알아보는 OAuth 작동방식

Shyuuuuni·2022년 11월 3일
59

📚 Tech-Post

목록 보기
1/9
post-thumbnail

OAuth란?

  • Open standard for Authorization, Open Authorization
  • 권한 부여를 위한 공개된 인증 프로토콜 (접근 권한을 위임하는 개방형 표준 프로토콜)
    • OAuth는 인증을 위한 프로토콜? X
    • Authorization = 인가, 즉 권한 부여를 위한 프로토콜이다. (다만 인가를 받기 위해서는 인증이 선행되어야 하기 때문에 보통은 같이 사용되는 것 같다!)
  • 대표적인 예시 : SNS 로그인, Google 로그인, Github 로그인, Apple 로그인 등

OAuth 역사

OAuth 1.0

  • OAuth 등장 이전, 기본적인 ID/PW 인증과 같은 방식은 보안상 취약점이 분명 존재했고, 조금 더 안전한 표준 인증 방식이 존재하지 않았다.
  • API로 접근 권한을 관리하는 방법을 찾아보았고, 구글의 AuthSub이나 야후의 BBAuth 등을 참고하여 OAuth 1.0을 발표하게 되었다.
  • OAuth에 대한 커뮤니티가 성장하면서 여러 논의가 오갔고, 보안 문제를 해결한 OAuth 1.0a 등 지속적으로 발전했다.
  • 이후 RFC5849 The OAuth 1.0 Protocol 라는 OAuth 1.0 에 대한 프로토콜 표준안이 발표되었다.

OAuth 2.0

  • OAuth 1.0의 구조적인 문제점을 해결하기 위해서 등장한 WRAP 을 기반으로 개발한 프레임워크
    • OAuth 의 흐름이 비-브라우저 기반으로 변경 (OAuth 1.0에서는 원하는 서비스의 브라우저를 열기 - 해당 브라우저에서 서비스 인증 - 원래 애플리케이션으로 토큰 복사 와 같은 UX적인 문제점이 있었다.)
    • HTTPS 를 필수로 사용해야 한다. (따라서 클라이언트 애플리케이션 내에서 별도의 암호화-기존에는 HMAC 알고리즘 등- 이 필요하지 않아졌다.)
    • 전자서명 단순화
    • 액세스 토큰의 생명주기 감소 (기존 OAuth 1.0 액세스토큰은 1년 이상 보관 가능) + 리프래시 토큰 발급
  • 가장 큰 변화는 요청을 처리하는 서버와 사용자 인증을 위한 서버의 역할을 명확히 구분한다.
  • OAuth 1.0과 2.0은 개념만 약간 비슷하고 완전히 다르므로 상호호환되지 않는다.
  • OAuth 2.0은 매커니즘이 하나로 통일되지 않아 4가지 타입으로 발표되었다.
  • 특이하게 OAuth 2.0은 프레임워크라고 한다. RFC 6749 : The OAuth 2.0 Authorization Framework

OAuth의 구성 요소

  • OAuth 2.0은 하나의 통일된 방식이 아니기 때문에 약간의 차이가 있을 수 있다.
  • 많이 사용되는 예시인 OAuth를 이용한 소셜 로그인을 기준으로 알아보자.

OAuth 에서 총 4가지 요소가 상호작용하며 작동한다.

1. 자원 소유자 (Resource Owner)

  • 자원 소유자 혹은 개인이 클라이언트에 접근중일 경우에는 사용자라고 한다.
  • 보호 자원에 대한 접근 권한을 가지고 있으며, 인증 정보를 제공해서 액세스 권한을 허용할 수 있다.
  • 예: Github 로그인을 하려고 하는 사람

2. 클라이언트 (Client)

  • 클라이언트 혹은 자원 소유자가 사용중인 서비스나 애플리케이션 등으로 불린다.
  • 보호 자원에 대한 접근 권한을 요청하여 발급받고, 발급받은 권한을 통해 자원 서버에 자원을 요청한다.
  • 인가 서버, 자원 서버 등 서버와 직접 통신하는 대상이여서 클라이언트라고 불린다고 추측중.
  • 예: Velog 등 OAuth 로그인을 지원하는 애플리케이션

3. 인가 서버 (Authorization Server)

  • 인가 서버 혹은 권한 서버라고 불린다.
  • 자원 소유자를 인증하는 과정을 통해 권한에 대한 유효성을 검증한다.
  • 모든 검증이 올바르게 끝나면 최종적으로 자원 서버에 접근하여 자원 요청 권한을 가질 수 있는 토큰을 발급하여 클라이언트에게 응답한다.
  • 예: Github 로그인 토큰을 발급하는 인가 서버 (https://github.com/login/oauth/)

4. 자원 서버 (Resource Server)

  • 자원 서버는 접근 권한이 있는 보호된 자원을 호스팅한다.
  • 경우에 따라 인가 서버와 자원 서버는 같은 서버일 수 있다.
  • 예: Github API 서버 (https://api.github.com/)

작동 과정 with Github OAuth

이제 위와 같은 4가지 구성 요소들이 어떻게 OAuth 를 처리하는지 알아보자.

세부적인 작동 과정 이전에 간략하게 과정을 요약하면 다음과 같다.

  1. 사용자가 클라이언트에 OAuth 권한을 부여하기 위해 인가 서버에 접근
  2. 인가 서버에 적절한 인증 정보를 제공하여 클라이언트에 인가 코드 발급 및 최종적으로 토큰 발급
  3. 토큰을 통해 자원 서버에 접근하여 클라이언트에서 필요한 자원에 접근

이제 이러한 과정을 사용자가 Velog 서비스를 Github 소셜 로그인을 하는 시나리오로 알아보자. (내부적으로는 100% 동일하지 않을 수 있지만, 전체적인 과정을 이해하는데 예시를 든 것)

Github Login 구성 요소

작동 과정

[사용자 - 클라이언트] Github 인증 요청 및 유효성 검사

  1. 자원 소유자인 사용자가 클라이언트인 Velog 서비스에서 OAuth인 Github 로그인을 버튼을 클릭한다.
  2. 서비스인 Velog 에서는 Github 로그인을 위해 인가 서버인 Github OAuth 서버에 권한 인증을 요청한다.
  • 여기서 특이한 점은 Github OAuth는 사전에 클라이언트별로 Client IDClient Secret, Callback URL 등을 설정하여 유효한 Github OAuth 로 접근했는지 검증한다.
  • 이러한 설정은 다음 챕터에서 실제로 구현해보면서 설정하고, 여기서는 서비스에서 사전에 설정해둔 정보들로 요청한다고 생각하자.

[사용자 - 인가 서버] 사용자 인증 및 인가 코드 발급

  1. 인가 서버는 클라이언트의 Client ID, Client Secret, Callback URL 등을 사전에 등록한 값과 일치하는지 검증한다.
  2. 클라이언트의 인증 정보를 확인했다면, 사용자의 Github 유저 인증이 필요하므로 로그인 페이지를 제공한다.
  3. 사용자는 제공한 양식에 맞추어 ID/PW 와 같은 로그인 인증 정보를 전달한다.

  1. 로그인이 성공한다면 인가 서버에서 클라이언트에 제공할 권한 목록을 표시하는 창을 표시한다.
  2. 사용자는 인가 서버에서 표시한 제공할 권한을 확인하고 수락 여부를 결정한다.

  • params : client_id, redirect_uri, login, scope, state, allow_signup

  • 수락 버튼을 누르면 Callback URL(redirect_uri) 의 code 쿼리스트링에 인가 코드를 포함하여 리다이렉트된다.

[클라이언트 - 인가 서버] 액세스 토큰 발급

  1. 인가 코드를 받은 클라이언트에서는 기존의 인증 정보인 Client ID, Client Secret, Callback URL 등과 함께 인가 코드를 인가 서버에 POST 메소드로 요청한다.
  2. 인가 서버는 요청받은 데이터를 다시 검증하여 최종적으로 자원 서버에 접근할 수 있는 액세스 토큰을 발급한다.
  • 클라이언트에서는 적절한 방법으로 액세스 토큰을 보관한다.
  • 마찬가지로 Github OAuth 래퍼런스에 Params 별 기능이 자세히 나와있다.

[클라이언트 - 자원 서버] 자원 요청

  • 필요한 자원이 있다면 자원 서버인 Github API 서버 (https://api.github.com/)에 발급받은 액세스 토큰과 함께 요청을 전송한다.

  • 또한 Github 에서 설정으로 리프래시 토큰을 함께 발급받을 수 있다. 기본값으로는 리프래시 토큰을 발급해주지 않는다.

적용해보기

  • 전체 코드: https://github.com/shyuuuuni/my-oauth-app
  • Github OAuth를 활용하는 방법은 굉장히 많을 것이다.
  • 그 중에서도 간단히 위 작동 과정을 적용해 볼 수 있는 react 앱을 만들어 보려고 한다.
  • 로그인 버튼을 누르면 Github 에서 프로필 정보를 가져와서 화면에 표시하는 간단한 어플리케이션을 만들어보자.

Github OAuth Apps 등록

  • 앞서 과정에서는 위와 같은 인증 정보를 클라이언트에서 가지고 있다고 가정했다.
  • 이러한 값들은 Github 에서 따로 어플리케이션을 등록을 해 주어야 얻을 수 있다.
  • 같은 내용을 Github OAuth 래퍼런스 - New OAuth Application 에서 확인할 수 있다.

1. OAuth Apps 이동

  • Github 로그인
  • 우측 상단 프로필 탭 - Settings - Developer settings - OAuth Apps 를 순서대로 클릭

2. App 정보 입력

  • Register a new application 버튼을 클릭
  • Application name 칸에는 어플리케이션 이름을 작성한다. 이름에는 민감 정보를 제외하고 공개해도 되는 이름으로 작성한다.
  • Homepage URL 칸에는 해당 어플리케이션의 전체 URL을 작성한다.
  • Application description 칸에는 설명을 적고 싶다면 작성한다.
  • Authorization callback URL 칸에는 인가 과정을 거친 후 인가 코드를 포함하여 리다이렉트 될 URL을 작성한다.

  • 마지막 Enable Device Flow 는 말 그대로 Device Flow를 사용할 지 체크하는 부분이다. Device Flow는 약간 작동 방식이 다른 것으로 보이며 자세한 내용은 Github OAuth 래퍼런스에 적혀있다. 이번 예제에서는 체크하지 않고 진행하였다.

생성된 인증 정보 확인

  • 추가적으로 Generate a new client secret 을 클릭하여 Client Secret 정보를 생성한다.
  • 생성된 정보로 Client ID, Client Secret, Authorization callback URL을 기억하자.
  • 참고) 위 정보는 포스팅 이후 초기화한 상태이다.

Create React App으로 간단한 프로젝트 생성

npx create-react-app my-oauth-app
cd my-oauth-app

proxy 설정

  • 추후 액세스 토큰을 발급받는 과정에서 Github 인가 서버에서 응답을 보내면 CORS 오류가 발생할 수 있다.
  • 인가 서버의 주소가 https://github.com/login/oauth/... 이고, API 서버의 user API 주소가 https://api.github.com/user 이므로 http-proxy-middleware를 통해 두 서버 모두에 대한 프록시를 설정해주자.
npm install --save http-proxy-middleware
// src/setupProxy.js
const { createProxyMiddleware } = require("http-proxy-middleware");

module.exports = function (app) {
  app.use(
    createProxyMiddleware("/login", {
      target: "https://github.com",
      changeOrigin: true,
    })
  );
  app.use(
    createProxyMiddleware("/user", {
      target: "https://api.github.com",
      changeOrigin: true,
    })
  );
};

styled-component 라이브러리 설치

  • 스타일을 입히기 위해 스타일드 컴포넌트 라이브러리를 설치해주었다.
npm install --save styled-components

라우팅을 위한 react-router-dom 라이브러리 설치

npm install react-router-dom

OAuth App 만들기

우리가 구현해야 할 페이지는 다음과 같다.

  • 로그인 이전 Github 소셜 로그인 버튼을 보여주는 컴포넌트
  • 로그인 이후 Github API로 유저 정보를 가져와서 표현하는 컴포넌트

이를 위해서 간단히 로직을 설계하면

  • 로그인 버튼을 클릭하면 Github 인가 서버에 GET 요청을 전달하여 인가 코드를 발급받는다.
  • 인가 코드를 발급받으면 인가 코드와 함께 Github 인가 서버에 POST 요청을 전달하여 액세스 토큰을 발급받는다.
  • 발급받은 액세스 토큰으로 Github API에 프로필 정보를 요청하여 랜더링한다.

Login 컴포넌트 레이아웃

  • 일단 기능보다는 로그인 버튼을 보여줄 컴포넌트 레이아웃을 구성하였다.
  • components/Login.js 위치에 아래와 같은 코드를 작성하였다.

// components/Login.js
import React from "react";
import styled from "styled-components";

const Login = () => {
  return (
    <LoginPage>
      <Title>로그인이 필요합니다.</Title>
      <GithubLoginBtn>Login with GitHub OAuth</GithubLoginBtn>
    </LoginPage>
  );
};

const LoginPage = styled.div`
  width: 300px;
  height: 500px;
`;

const Title = styled.div`
  width: 100%;

  text-align: center;
  font-size: 24px;
`;

const GithubLoginBtn = styled.button`
  width: 100%;
  height: 50px;

  border-radius: 10px;

  color: white;
  background-color: hsl(210, 8%, 20%);

  &:hover {
    cursor: pointer;
  }
`;

export default Login;
  • 이를 감싸고 있는 App.js 파일도 수정해주자.
// src/App.js
import Login from "./components/Login";

function App() {
  return <Login />;
}

export default App;

인가 코드 발급

이제 구현해야 하는 부분을 살펴보자.

  1. 서비스 접근 = 로그인 버튼 클릭
  2. 권한 인증 요청 = https://github.com/login/oauth/authorize 주소로 GET 요청
    3~6. Github 에서 제공한다.
  3. 우리가 작성했던 Callback URL의 쿼리스트링으로 code가 포함되어 리다이렉트된다.
  • 먼저 자주 사용될 것 같은 상수를 따로 관리해주었다. (사실 CLIENT_ID와 같은 정보는 숨겨야한다. 편의상 그냥 사용하겠다.)
  • constants/oauth.js
export const CLIENT_ID = "8401b2ab1be3f5f33623";
export const CLIENT_SECRETS = "1eec25b6176d61a43bbb083b3e6770a45cfb3d39";
export const CALLBACK_URL = "http://localhost:3000/oauth/callback";

export const GITHUB_AUTH_CODE_SERVER = "/login/oauth/authorize";
export const GITHUB_AUTH_TOKEN_SERVER = "/login/oauth/access_token";
export const GITHUB_API_SERVER = "/user";

먼저 로그인 버튼을 클릭하면 GITHUB_AUTH_SERVER에 GET 요청을 전달하는 기능을 구현해보자.

  • button 의 href 속성으로 CLIENT_ID와 CALLBACK_URL이 포함된 URL을 전달하자.
  • 다른 속성을 포함해도 되지만 그냥 간단한 버전으로 사용하겠다. 자세한 정보는 래퍼런스를 참고.

// components/Login.js
// 생략
const Login = () => {
  const AUTHORIZATION_CODE_URL = `${GITHUB_AUTH_CODE_SERVER}?client_id=${CLIENT_ID}&redirect_url=${CALLBACK_URL}`;

  // 인가 서버에 GET 요청을 전송한다.
  const fetchAuthCode = () => {
    window.location.assign(AUTHORIZATION_CODE_URL);
  };

  return (
    <LoginPage>
      <Title>로그인이 필요합니다.</Title>
      <GithubLoginBtn onClick={fetchAuthCode}>Login with GitHub OAuth</GithubLoginBtn>
    </LoginPage>
  );
};

// 생략
  • 여기까지 진행했으면 버튼을 누르면 아래 화면까지 진행된다.
  • 아래 화면의 Authorize [닉네임] 을 클릭하게 되면 http://localhost:3000/oauth/callback?code=24fafb16019382cba7e4와 같이 사전에 설정한 CALLBACK_URL에 쿼리스트링으로 인가 코드가 추가되어 리다이렉트된다.

액세스 토큰 발급

  • 지금까지 잘 따라왔다면 우리는 CALLBACK_URL?code=인가코드 와 같은 주소로 리다이렉트 되어 있을 것이다.
  • CALLBACK_URL로 리다이렉트 될 경우 리액트 라우터에서 컴포넌트를 이동해주도록 App.js 파일을 수정한다.
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Login from "./components/Login";
import Callback from "./components/Callback";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route exact path="/" element={<Login />} />
        <Route exact path="/oauth/callback" element={<Callback />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;
  • useEffect 훅으로 랜더링 시 URL에서 인가 코드를 분리하여 Github 인가 서버의 access_token 주소로 액세스 토큰을 POST 요청하는 컴포넌트를 구현했다.
// components/Callback.js
import React, { useEffect } from "react";
import { CLIENT_ID, CLIENT_SECRETS, GITHUB_AUTH_TOKEN_SERVER } from "../constants/oauth";

const Callback = () => {
  useEffect(() => {
    const fetchAccessToken = async () => {
      // 쿼리스트링에서 Authorization Code를 가져옵니다.
      const location = new URL(window.location.href);
      const code = location.searchParams.get("code");
      const ACCESS_TOKEN_URL = `${GITHUB_AUTH_TOKEN_SERVER}?client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRETS}&code=${code};

      return fetch(ACCESS_TOKEN_URL, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Accept: "application/json",
        },
      });
    };

    fetchAccessToken()
      .then((response) => response.json())
      .then((data) => console.log(data))
      .catch((err) => console.log(err));
  });

  return <div>로딩중 ...</div>;
};

export default Callback;

  • 이제 얻어온 액세스 토큰을 가져다가 사용하면 된다!

유저 프로필 보여주기

  • 가져온 토큰을 프로필을 검색할 Profile 컴포넌트로 useNavigation 훅으로 전달해주었다.
// components/Callback.js
// ...생략
    fetchAccessToken()
      .then((response) => response.json())
      .then((data) => {
        navigate("/profile", { state: data.access_token });
      })
      .catch((err) => console.log(err));
  • 라우터에 Profile 컴포넌트를 추가해주자.
// src/App.js
// ...생략
    <BrowserRouter>
      <Routes>
        <Route exact path="/" element={<Login />} />
        <Route exact path="/profile" element={<Profile />} />
        <Route exact path="/oauth/callback" element={<Callback />} />
      </Routes>
    </BrowserRouter>
  • 마지막으로 Github API 양식에 맞추어 GET 요청을 보내는 Profile 컴포넌트를 작성한다.
import React, { useEffect, useState } from "react";
import { useLocation } from "react-router-dom";
import { GITHUB_API_SERVER } from "../constants/oauth";

const Profile = () => {
  const location = useLocation();
  const [userName, setUserName] = useState();

  useEffect(() => {
    const fetchGithubUser = () => {
      const accessToken = location.state;

      return fetch(GITHUB_API_SERVER, {
        method: "GET",
        headers: {
          Authorization: `Bearer ${accessToken}`,
          Accept: "application/json",
        },
      });
    };

    fetchGithubUser()
      .then((response) => response.json())
      .then(({ login }) => setUserName(login))
      .catch((err) => console.log(err));
  });

  return <div>로그인 된 사용자 : {userName ?? "로딩중.."}</div>;
};

export default Profile;
  • 위의 예시는 단순히 유저 정보를 가져와서 닉네임인 login 속성을 userName 상태로 보여주는 컴포넌트이다.

전체 로그인 과정

회고

  • 가능하면 무작정 따라해볼 수 있는 좋은 글을 써보자! 라는 생각으로 시작했는데, 확실히 되게 어려운 것 같았다.. 구글에 많은 좋은 아티클들 정말 감사합니다..
  • 그 전에 한번 꼭 정리하고 지나가고 싶어서 기록해봤는데 아쉬움이 좀 남는 것 같은데 예를들면..
  • 예시에서 express 서버를 따로 안 쓰려고 노력했는데, 오히려 프록시 설정같이 귀찮은 부분이 더 많이 생긴게 아닐까..?
  • 스타일드 컴포넌트 뭔가 많이 쓸 수 있을 줄 알았는데, 막상 쓸데가 없더라.. 등등
  • 그래도 개념을 먼저 공부하고 Github OAuth를 사용해보니까 생각보다 머릿속에 많이 남는듯!

래퍼런스

https://itwiki.kr/w/OAuth

https://justee.tistory.com/1

https://www.okta.com/kr/identity-101/whats-the-difference-between-oauth-openid-connect-and-saml/

https://developers.payco.com/guide

https://devkkiri.com/post/542d8419-6fb1-4e28-bbbe-d68f832fd78a

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

https://gist.github.com/ninanung/2ad24c760e81401ed65f13f634a25e73

https://woodcock.tistory.com/17

profile
배짱개미 개발자 김승현입니다 🖐

6개의 댓글

comment-user-thumbnail
2022년 11월 4일

LGTM
다음부터 승현님꺼 보면서 깃헙 OAuth 적용할래요

1개의 답글
comment-user-thumbnail
2022년 11월 12일

좋은글 감사합니다

1개의 답글
comment-user-thumbnail
2022년 11월 13일

와 대박
거짓말 많이 보태서 공식문서인줄 알았네요

1개의 답글