서론

OAuth 2.0에 대해 상세히 설명하려고 한다. GitHub OAuth를 예로 들어 전체적인 흐름을 자세히 살펴본 후, 프론트엔드백엔드에서의 역할을 구분하여 구현 예제를 제시하겠다.

OAuth 2.0이란?

OAuth 2.0은 사용자 데이터에 대한 제 3자 접근 권한을 안전하게 위임하기 위한 표준 프로토콜이다. 쉽게 말해, 사용자가 다른 애플리케이션에 자신의 데이터에 대한 접근 권한을 부여할 때 사용되는 프로토콜이다. 주로 로그인 시스템을 구현할 때, 사용자가 다른 서비스의 계정으로 로그인할 수 있도록 하는데 사용된다.

OAuth 2.0의 용어 정리

설명하기에 앞서, OAuth 2.0에서 사용되는 주요 용어를 정리하고 넘어가자.

  1. Resource Owner: 보호된 자원의 소유자, 즉 사용자이다.
  2. Client: Resource Owner를 대신하여 보호된 자원에 접근하려는 애플리케이션이다 (우리가 만드는 애플리케이션).
  3. Resource Server: 보호된 자원을 호스팅하는 서버이다 (예: GitHub의 API 서버).
  4. Authorization Server: 인증을 처리하고 액세스 토큰을 발급하는 서버이다.

OAuth 2.0 흐름

GitHub OAuth를 예로 들어 전체 흐름을 단계별로 상세히 설명해 보겠다.

1. 애플리케이션 등록

OAuth 흐름을 시작하기 전에, 개발자는 GitHub에 애플리케이션을 등록해야 한다.

  • GitHub의 Developer Settings에서 새 OAuth App을 생성한다.
  • 애플리케이션 이름, 홈페이지 URL, Authorization callback URL을 입력한다.
  • GitHub는 Client IDClient Secret을 발급한다.

2. 권한 요청 (Authorization Request)

사용자가 “GitHub로 로그인” 버튼을 클릭하면 다음 과정이 시작된다:

  1. 프론트 Client는 사용자를 GitHub의 Authorization 엔드포인트로 리다이렉트한다.
  • URL 구조:
    1
    2
    3
    4
    5
    
    https://github.com/login/oauth/authorize?
    client_id=YOUR_CLIENT_ID
    &redirect_uri=YOUR_CALLBACK_URL
    &scope=user
    &state=RANDOM_STRING
    
  • client_id: GitHub에서 발급받은 Client ID
  • redirect_uri: 인증 후 리다이렉트될 URL
  • scope: 요청하는 권한 범위 (예: user, repo 등)
  • state: CSRF 공격을 방지하기 위한 랜덤 문자열
  1. 사용자는 GitHub 로그인 페이지에서 자신의 credentials를 입력한다.

  2. GitHub는 사용자에게 요청된 권한을 보여주고 승인을 요청한다.

3. 권한 부여 (Authorization Grant)

  1. 사용자가 권한을 승인하면, GitHub는 사용자를 권한 요청 시 설정한 redirect_uricodestate 쿼리를 추가해서 리다이렉트한다.
  • 리다이렉트 URL 예:
    1
    
    https://your-app.com/callback?code=TEMPORARY_CODE&state=RANDOM_STRING
    
  • code: 임시 인증 코드
  • state: 요청 시 전송한 state 값과 동일해야 한다
  1. 프론트 Client는 이 임시 코드를 받아 백엔드 Client로 전송한다.

4. 액세스 토큰 요청 (Access Token Request)

  1. Client의 백엔드는 받은 임시 코드, client_id, client_secret을 GitHub의 토큰 엔드포인트로 전송한다.
  • POST 요청을 https://github.com/login/oauth/access_token로 보낸다.
  • 요청 본문 예:
    1
    2
    3
    4
    
    client_id=YOUR_CLIENT_ID
    &client_secret=YOUR_CLIENT_SECRET
    &code=TEMPORARY_CODE
    &redirect_uri=YOUR_CALLBACK_URL
    
  1. GitHub는 이 정보를 검증한다.

5. 액세스 토큰 발급 (Access Token Grant)

  1. 검증이 성공하면, GitHub는 액세스 토큰백엔드 Client에게 발급한다.
  • 응답 예:
    1
    2
    3
    4
    5
    
    {
        "access_token": "gho_16C7e42F292c6912E7710c838347Ae178B4a",
        "token_type": "bearer",
        "scope": "user"
    }
    
  1. 백엔드 Client는 이 액세스 토큰을 안전하게 저장한다.

6. 보호된 리소스 접근 (Protected Resource Access)

  1. 백엔드 Client는 발급받은 액세스 토큰을 사용하여 GitHub API에 사용자 정보를 요청한다.
  • GET 요청을 https://api.github.com/user로 보낸다.
  • 헤더에 액세스 토큰을 포함시킨다:
    1
    
    Authorization: token ACCESS_TOKEN
    
  1. GitHub는 토큰을 검증하고, 요청된 사용자 정보를 반환한다.

7. 사용자 인증 완료

  1. 백엔드 Client는 받은 사용자 정보를 사용하여 자체 시스템에서 사용자를 인증하거나 계정을 생성한다. (예: 회원가입)

  2. 사용자는 이제 Client 애플리케이션에 로그인된 상태가 된다.

이로써 GitHub OAuth 흐름이 완료된다.

프론트엔드와 백엔드의 역할 구분

프론트엔드 (React)에서 해야 할 일:

  1. GitHub로 로그인 버튼을 생성하고, 사용자가 이를 클릭하면 GitHub의 Authorization 페이지로 리다이렉트한다.
  2. 인증 후, 콜백 URL에서 임시 인증 코드를 받아 백엔드로 전송한다.

백엔드 (Spring Boot)에서 해야 할 일:

  1. 프론트엔드로부터 받은 임시 인증 코드를 사용해 액세스 토큰을 요청한다.
  2. 액세스 토큰을 사용해 GitHub API로부터 사용자 정보를 가져온다.
  3. 사용자 정보를 이용해 자체 시스템에서 사용자를 인증하거나 계정을 생성한다.

구현 예제

프론트엔드 (React) - 예시

  1. GitHub 로그인 버튼 구현:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import React from "react";

export const GitHubLogin = () => {
    const CLIENT_ID = "YOUR_GITHUB_CLIENT_ID";
    const REDIRECT_URI = "http://localhost:3000/callback";

    // GitHub 로그인 페이지로 리다이렉트
    const handleLogin = () => {
        window.location.href = `https://github.com/login/oauth/authorize?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&scope=user`;
    };

    return <button onClick={handleLogin}>GitHub로 로그인</button>;
};
  1. 콜백 처리:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React, { useEffect } from "react";
import axios from "axios";

// 콜백 페이지
export const Callback = () => {
    useEffect(() => {
        // 콜백 URL에서 code 값 추출
        const code = new URLSearchParams(window.location.search).get("code");
        // 얻어온 code 값이 있으면 백엔드로 전송
        if (code) {
            axios
                .post("/api/github-callback", { code })
                .then((response) => {
                    // 로그인 성공 처리
                    console.log(response.data);
                })
                .catch((error) => {
                    console.error("Error:", error);
                });
        }
    }, []);

    return <div>Processing login...</div>;
};

백엔드 (Golang-net/http) - 예시

  1. Package상수 정의:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strings"
)

const (
	clientID     = "your-client-id"
	clientSecret = "your-client-secret"
	githubTokenURL = "https://github.com/login/oauth/access_token"
	githubUserURL  = "https://api.github.com/user"
)
  1. main 함수 정의, 라우터를 설정하고 서버를 시작:
1
2
3
4
func main() {
	http.HandleFunc("/callback", handleCallback) // 콜백 핸들러 등록
	http.ListenAndServe(":8080", nil)
}
  1. 콜백 핸들러 구현:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func handleCallback(w http.ResponseWriter, r *http.Request) {
	code := r.URL.Query().Get("code") // 클라이언트가 보낸 code 값 쿼리에서 추출
	if code == "" {
		http.Error(w, "Missing code", http.StatusBadRequest)
		return
	}

	token, err := getAccessToken(code) // 액세스 토큰 요청 함수 호출
	if err != nil {
		http.Error(w, "Failed to get token", http.StatusInternalServerError)
		return
	}

	userInfo, err := getUserInfo(token) // 사용자 정보 요청 함수 호출
	if err != nil {
		http.Error(w, "Failed to get user info", http.StatusInternalServerError)
		return
	}

	// 원래는 받아온 사용자의 정보를 이용해 jwt 토큰을 발급하거나 세션을 생성하는 등의 작업을 수행
	// 이 예제에서는 회원가입, 로그인 작업을 생략하고 사용자 정보를 그대로 보내줌

	fmt.Fprintf(w, "User Info: %s", userInfo)
}
  1. 액세스 토큰 요청 함수 구현:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func getAccessToken(code string) (string, error) {
    // POST 요청을 보내기 위해 URL 값 설정
    // 유저가 보낸 code 값과 클라이언트 ID, 시크릿 값 설정
	data := url.Values{
		"grant_type":    {"authorization_code"},
		"code":          {code},
		"client_id":     {clientID},
		"client_secret": {clientSecret},
	}

    // Authorization Server로 POST 요청 보내기
	resp, err := http.Post(githubTokenURL, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

    // 응답받은 JSON 파싱
	var result map[string]interface{}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return "", err
	}

    // 파싱한 JSON에서 액세스 토큰 추출
	return result["access_token"].(string), nil
}
  1. 사용자 정보 요청 함수 구현:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func getUserInfo(token string) (string, error) {
	req, err := http.NewRequest("GET", githubUserURL, nil)
	if err != nil {
		return "", err
	}

    // 헤더에 받은 액세스 토큰 추가
	req.Header.Set("Authorization", "token "+token)

	// Authorization Server로 GET 요청 보내기
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	var userInfo map[string]interface{}
	if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
		return "", err
	}

    // 사용자 정보 반환
	return fmt.Sprintf("%v", userInfo), nil
}

결론

OAuth 2.0은 사용자 데이터에 대한 안전한 접근을 위임하기 위한 표준 프로토콜이다.