OAuth 2.0에 대해 상세히 설명하려고 한다. GitHub OAuth를 예로 들어 전체적인 흐름을 자세히 살펴본 후, 프론트엔드와 백엔드에서의 역할을 구분하여 구현 예제를 제시하겠다.
OAuth 2.0이란?#
OAuth 2.0은 사용자 데이터에 대한 제 3자 접근 권한을 안전하게 위임하기 위한 표준 프로토콜이다.
쉽게 말해, 사용자가 다른 애플리케이션에 자신의 데이터에 대한 접근 권한을 부여할 때 사용되는 프로토콜이다.
주로 로그인 시스템을 구현할 때, 사용자가 다른 서비스의 계정으로 로그인할 수 있도록 하는데 사용된다.
OAuth 2.0의 용어 정리#
설명하기에 앞서, OAuth 2.0에서 사용되는 주요 용어를 정리하고 넘어가자.
- Resource Owner: 보호된 자원의 소유자, 즉 사용자이다.
- Client: Resource Owner를 대신하여 보호된 자원에 접근하려는 애플리케이션이다 (우리가 만드는 애플리케이션).
- Resource Server: 보호된 자원을 호스팅하는 서버이다 (예: GitHub의 API 서버).
- Authorization Server: 인증을 처리하고 액세스 토큰을 발급하는 서버이다.
OAuth 2.0 흐름#
GitHub OAuth를 예로 들어 전체 흐름을 단계별로 상세히 설명해 보겠다.
1. 애플리케이션 등록#
OAuth 흐름을 시작하기 전에, 개발자는 GitHub에 애플리케이션을 등록해야 한다.
- GitHub의 Developer Settings에서 새 OAuth App을 생성한다.
- 애플리케이션 이름, 홈페이지 URL, Authorization callback URL을 입력한다.
- GitHub는 Client ID와 Client Secret을 발급한다.
2. 권한 요청 (Authorization Request)#
사용자가 “GitHub로 로그인” 버튼을 클릭하면 다음 과정이 시작된다:
- 프론트 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 IDredirect_uri
: 인증 후 리다이렉트될 URLscope
: 요청하는 권한 범위 (예: user, repo 등)state
: CSRF 공격을 방지하기 위한 랜덤 문자열
사용자는 GitHub 로그인 페이지에서 자신의 credentials를 입력한다.
GitHub는 사용자에게 요청된 권한을 보여주고 승인을 요청한다.
3. 권한 부여 (Authorization Grant)#
- 사용자가 권한을 승인하면, GitHub는 사용자를 권한 요청 시 설정한
redirect_uri
로 code
와 state
쿼리를 추가해서 리다이렉트한다.
- 리다이렉트 URL 예:
1
| https://your-app.com/callback?code=TEMPORARY_CODE&state=RANDOM_STRING
|
code
: 임시 인증 코드state
: 요청 시 전송한 state 값과 동일해야 한다
- 프론트 Client는 이 임시 코드를 받아 백엔드 Client로 전송한다.
4. 액세스 토큰 요청 (Access Token Request)#
- 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
|
- GitHub는 이 정보를 검증한다.
5. 액세스 토큰 발급 (Access Token Grant)#
- 검증이 성공하면, GitHub는 액세스 토큰을 백엔드 Client에게 발급한다.
- 응답 예:
1
2
3
4
5
| {
"access_token": "gho_16C7e42F292c6912E7710c838347Ae178B4a",
"token_type": "bearer",
"scope": "user"
}
|
- 백엔드 Client는 이 액세스 토큰을 안전하게 저장한다.
6. 보호된 리소스 접근 (Protected Resource Access)#
- 백엔드 Client는 발급받은 액세스 토큰을 사용하여 GitHub API에 사용자 정보를 요청한다.
- GET 요청을
https://api.github.com/user
로 보낸다. - 헤더에 액세스 토큰을 포함시킨다:
1
| Authorization: token ACCESS_TOKEN
|
- GitHub는 토큰을 검증하고, 요청된 사용자 정보를 반환한다.
7. 사용자 인증 완료#
백엔드 Client는 받은 사용자 정보를 사용하여 자체 시스템에서 사용자를 인증하거나 계정을 생성한다. (예: 회원가입)
사용자는 이제 Client 애플리케이션에 로그인된 상태가 된다.
이로써 GitHub OAuth 흐름이 완료된다.
프론트엔드와 백엔드의 역할 구분#
프론트엔드 (React)에서 해야 할 일:#
- GitHub로 로그인 버튼을 생성하고, 사용자가 이를 클릭하면 GitHub의 Authorization 페이지로 리다이렉트한다.
- 인증 후, 콜백 URL에서 임시 인증 코드를 받아 백엔드로 전송한다.
백엔드 (Spring Boot)에서 해야 할 일:#
- 프론트엔드로부터 받은 임시 인증 코드를 사용해 액세스 토큰을 요청한다.
- 액세스 토큰을 사용해 GitHub API로부터 사용자 정보를 가져온다.
- 사용자 정보를 이용해 자체 시스템에서 사용자를 인증하거나 계정을 생성한다.
구현 예제#
프론트엔드 (React) - 예시#
- 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
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) - 예시#
- 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"
)
|
- main 함수 정의, 라우터를 설정하고 서버를 시작:
1
2
3
4
| func main() {
http.HandleFunc("/callback", handleCallback) // 콜백 핸들러 등록
http.ListenAndServe(":8080", nil)
}
|
- 콜백 핸들러 구현:
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
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
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은 사용자 데이터에 대한 안전한 접근을 위임하기 위한 표준 프로토콜이다.