OAuth 2.0 클라이언트 만들기(feat. 깃허브)

OAuth 2.0 클라이언트 만들기(feat. 깃허브)

OAuth 2.0

OAuth 는 기본적으로 서비스 제공자(깃허브, 트위터, 페이스북)가 신뢰할 수 없는 타 어플리케이션에게 사용자의 아이디와 패스워드를 제공하지 않더라도 사용자의 특정 정보에 접근하거나 작업을 처리할 수 있도록 하는 방법이자 표준이다. 최근 일반적으로 웹사이트에 흔히 볼 수 있는 SNS 계정으로 로그인하는 것이 가장 대표적인 사용처라고 볼 수 있다. 내가 만든 서비스에 수동으로 회원가입 기능을 넣지 않더라도 서비스 제공자가 가지고 있는 개인정보를 기반으로 로그인할 수 있도록 하는 것이며 이를 사용하면 작은 서비스에서 직접 비밀번호를 관리해야 하거나 하는 번거로운 부분을 떠안고 가지 않고 부담을 대기업에게 떠넘기게 된다.

 

JWT(Json Web Token)와는 다르게 토큰 자체는 큰 의미를 가지고 있지 않다. 다만, 인증을 통해 발급된 토큰으로 데이터를 조회하거나 여러 작업을 할 수 있는데, OAuth 를 통해 발급된 토큰으로 무언가 작업을 하기 위해서는 해당 토큰이 특정 작업을 할 수 있는 권한이 있는지 먼저 검사를 거쳐야 하며, 권한이 있다고 판단되면 해당 작업을 수행하게 된다. 즉, 토큰 자체가 데이터를 포함하고 있는 JWT 와는 달리 OAuth AccessToken 은 권한을 확인하고 서비스 제공자에게 데이터를 요청하기 위해 사용한다.

구성요소

OAuth 2.0 의 구성요소는 대체로 Client, Resource Owner, Resource Server, Authorization Server 로 구성되며 각각 다음을 의미한다.

Client 서비스 제공자에게 서비스를 제공받은 서버 또는 서비스를 말한다. 내가 만든 앱이라고 생각하자.
Resource Owner 서비스 제공자의 서비스(Github, Facebook, Google 등)에 가입되어 있어서 개인정보를 소유 중인 사용자, 내가 만든 앱을 사용할 사용자.
Resource Server Resource Owner 의 개인정보를 가지고 있는 서비스 제공자의 서버
Authorization Server OAuth 2.0 엑세스 토큰을 발급받기 위한 서비스 제공자의 인증 서버

OAuth AccessToken 의 발급을 간단히 살펴보자, OAuth 의 방식은 4가지가 있지만, 여기서는 가장 흔하게 쓰이는 방식인 Code Grant 방식으로 설명해보려고 한다. Code Grant 는 인증을 위해 Code 를 발급받고, 그 이후 발급받은 Code 값을 사용하여 AccessToken 발급을 위해 다시 한 번 인증 서버에 요청하는 것을 말한다. Code 는 단순한 문자열이다. 이 과정은 이후 구현 과정에서 살펴볼 예정이다.

 

먼저 내가 만든 앱(Client)을 사용하려는 사용자(Resource Owner)가 'Github 로그인하기' 버튼을 클릭하게 되면 어떤 일이 발생하게 될까?

  1. 사용자가 Github 에 로그인이 되어 있지 않은 경우 로그인 요청.
  2. Client 는 AccessToken 발급을 위한 Code 값을 넣어오기 위해 https://github.com/login/oauth/authorize 로 요청.
  3. AccessToken 발급을 위해 Code 를 파라매터로 넘겨  Authorization Server 인 https://github.com/login/oauth/access_token 에 요청한다.
  4. Client 에 Github 를 통해 로그인한 사용자의 정보를 얻기 위해 AccessToken 을 Authorization HTTP Header 에 첨부하여 https://api.github.com/user 에 요청.

* 아래 섹션부터는 구현 영역이다. 구현에는 관심없다면 마치며로 넘어가자.

App 등록하기

OAuth 를 사용하여 로그인을 구현하기 전에 해야 할 일이 있다면, 깃허브에 우리가 만든 앱을 알려주어야 하고 Client ID, Client Secret 를 먼저 발급받는 과정이 필수적으로 필요하다는 것이다. 깃허브에서 Settings - Developer Settings 로 진입하게 되면 New Github App 이 있는데, 이를 클릭하면 어플리케이션을 등록할 수 있다. 예를 들면 다음과 같다.

살펴보아야 할 점이 있다면, Client ID 부분과 Client Secret, 그리고 Homepage URL, Callback URL 부분이다. 이 부분은 설정되어 있어야 하며 어플리케이션 내부에서 값으로 사용하게 될 예정이다. 만약 Client Secret 이 비어 있다면 New Client Secret 으로 먼저 생성하자.

요청과 라우트 구성하기

일반적으로 OAuth 를 사용하여 로그인을 구현하면 2가지의 라우트를 가지게되는데, 하나는 파라매터를 조합하여 서비스 제공자의 페이지로 리다이렉트하기 위한 라우트, 그리고 하나는 인증완료 이후 엑세스 토큰과 함께 리다이렉트 될 페이지다. 사용자는 Github 아이디로 로그인 하기 위해서 아래와 같은 과정을 거칠 것이다.

  1. 사용자(Resource Onwer)가 서비스(Client) 에서 'Github 로그인하기' 클릭시 /login/github 로 이동.
  2. /login/github 에서 파라매터를 조합하여 https://github.com/login/oauth/authorize 로 리다이렉트하여 사용자 인증.
  3. 사용자가 서비스 제공자(Resource Server)에서 인증을 마치고 Code 값을 포함하여 /login/github/callback 로 리다이렉트.
  4. /login/github/callback 에서 Code 값으로 https://github.com/login/oauth/access_token 요청하고 AccessToken 을 구하여 유저 정보를 얻고 로그인, 이후 서비스 페이지 또는 이전 페이지로 리다이렉트.

/login/github

/login/github 에는 살펴본대로 파라매터를 조합하여 https://github.com/login/oauth/authorize 로 리다이렉트한다. 파라매터를 조합하여 URL 을 반환하는 메서드인 Config.GetPermissionUrl() 은 이후 알아보자. 여기서 configClient ID, Client Secret, Callback URL 을 가지고 있는 Config 구조체를 의미한다.

http.HandleFunc("/login/github", func(w http.ResponseWriter, r *http.Request) {
    // https://github.com/login/oauth/authorize
    http.Redirect(w, r, config.GetPermissionUrl(""), 301)
})

위와 같은 코드를 사용하여 /login/github 에 접속하면 아래와 서비스 제공자의 페이지로 리다이렉트 되어 아래와 같은 화면이 나타난다. 우리가 익히보던 인증화면이다.

/login/github/callback

App 등록하기에서 Callback URL 을 /login/github/callback 으로 설정했던 것을 기억하자. 인증이 끝난 뒤에 리다이렉트 되는 곳이 바로 이 라우트다. 여기에서 하기 적절한 것은 서비스 제공자로부터 인증된 유저의 데이터를 얻어낸 뒤, 로그인 세션을 시작하는 일이다. 여기서 유저의 데이터를 얻어내는 일까지 하면 다음과 같다.

http.HandleFunc("/login/github/callback", func(w http.ResponseWriter, r *http.Request) {
    c := NewClient(config, r.FormValue("code"))
    user := c.User()

    data, err := json.Marshal(user)
    if err != nil {
        panic(err)
    }
    if _, err = w.Write(data); err != nil {
        panic(err)
    }
})

인증이후 Callback URL 로 리다이렉트 되면 code 가 첨부된다. 이를 사용하여 클라이언트 내부적으로 AccessToken 을 얻어와 Client.User() 에서 https://api.github.com/user 에 요청하여 유저 정보를 얻어온다. 편의상 에러는 패닉으로 처리한다.

구현

우리는 'Github 로그인하기' 를 구현하기 위한 간단한 OAuth 클라이언트를 만들어본다. 사실 OAuth 는 클라이언트, 그리고 https://github.com/go-oauth2/oauth2 같은 것을 사용해서 서비스 제공자(Authorization Server)를 직접 구축할 수도 있는데, OAuth 인증 서버를 만드는 일은 라이브러리를 사용하지 않고는 다소 분량이 제법 많은 듯하니 추후 포스팅할 가능이 있다. 설령 라이브러리를 쓰더라도 JWT 를 했던 것과 마찬가지로 뜯어보면서 할 가능성이 크다. 사실 실무에서는 직접 OAuth 서버를 구축할 일은 그다지 많지 않다.

 

대부분 OAuth 클라이언트를 위한 튜토리얼은 서비스 제공자에서 문서로 제공한다. 깃허브의 경우 https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps 를 참고해볼 수 있다.

struct Config

이 구조체는 깃허브에 요청하기 위한 설정을 담고있다. 각 필드가 무엇을 의미하는지는 App 등록하기에서 살펴본 바와 같다.

type Config struct {
	ClientId     string
	ClientSecret string
	Callback     string
}

Config.GetPermissionUrl(string) string

AccessToken 을 얻어오기 위해 Code 를 얻어와야 하는데, 이 메서드는 Code 값을 얻어오기 위한 Url 을 반환한다. https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps 에 따르면 이러한 Code 값을 얻어오기 위해서는 https://github.com/login/oauth/authorize 에 요청해야 할 필요가 있으며 그에 필요한 파라매터는 client_id, redirect_uri, scope, state, allow_signup 이 있다. 여기서 중요한 것은 우리가 App 을 등록할 때 보았던 Client ID(client_id), Callback URL(redirect_uri)이다. 그에 따라 Url 을 구성하는 메서드를 구현해보면 아래와 같다.

func (c Config) GetPermissionUrl(scope string) string {
	// State 에 첨부할 랜덤 바이트 배열
	randomBytes := make([]byte, 32)
	if _, err := rand.Read(randomBytes); err != nil {
		panic(err)
	}

	options := struct {
		ClientId    string `url:"client_id"`
		RedirectUri string `url:"redirect_uri"`
		Scope       string `url:"scope"`
		State       string `url:"state"`
		AllowSignup string `url:"allow_signup"`
	}{
		c.ClientId, c.Callback, scope, hex.EncodeToString(randomBytes), "true",
	}

	v, err := query.Values(options)
	if err != nil {
		panic(err)
	}

	return "https://github.com/login/oauth/authorize?" + v.Encode()
}

코드를 보면 먼저 state 를 위한 랜덤 문자열을 만들고, 그를 포함한 파라매터는 모두 URL QueryString 으로 Enocde 처리하여 더해주는 것을 볼 수 있다. 여기서는 부가적으로 외부 라이브러리를 사용했으며 https://github.com/google/go-querystring 를 사용했다.

struct Client

Clienthttp.Client 로 사용하며 내부적으로 Token을 가지고 있다.

type Client struct {
	*http.Client
	Token *Token
}

Client.NewClient(Config, string) *Client

이 메서드는 Code 값으로 https://github.com/login/oauth/access_token 에 요청하여 AccessToken 을 설정하고 반환한다. Github 에서 반환을 해줄 때 access_token, scope, token_type 키를 가진 JSON 을 반환하는데, 이를 표현한 것이 Token 구조체다.

func NewClient(config Config, code string) *Client {
	client := Client{http.DefaultClient, nil}

	options := struct {
		ClientId     string `url:"client_id"`
		ClientSecret string `url:"client_secret"`
		Code         string `url:"code"`
		RedirectUri  string `url:"redirect_uri"`
	}{
		config.ClientId, config.ClientSecret, code, config.Callback,
	}

	v, err := query.Values(options)
	if err != nil {
		panic(err)
	}

	// 엑세스 토큰 얻어오기
	data := client.request("POST", "https://github.com/login/oauth/access_token", bytes.NewBufferString(v.Encode()))

	client.Token = &Token{
		AccessToken: data["access_token"].(string),
		Scope:       data["scope"].(string),
		TokenType:   data["token_type"].(string),
	}

	return &client
}

Client.request() 메서드는 내부적으로 요청을 보낼 때 사용한다. Go 언어에서 HTTP Request 를 보내는 것은 일정한 코드 패턴이 있기 때문에 이를 메서드화 한 것이다.

struct Token

Github 에서 반환하는 AccessToken 을 표현하기 위한 구조체다.

type Token struct {
	AccessToken string `json:"access_token"`
	Scope       string `json:"scope"`
	TokenType   string `json:"token_type"`
}

마치며

여기까지 간단한 OAuth 클라이언트를 Go 언어로 만들어보았다. 정리하자면, OAuth 토큰은 자체적으로 정보를 가진 JWT 와는 다르게 무작위 문자열이며 이를 통해 서비스 제공자의 Authorization Server 에서 부여한 권한으로 사용자의 데이터를 요청할 수 있다. 서비스 제공자의 서버에 토큰을 가지고 요청할 때 서버는 토큰에 대응하는 권한을 확인하고 데이터를 요청한 Client 에 응답을 줄 것이다.

 

우리가 사용한 방식은 Code Grant 방식이며 이외에도 사용자가 직접 서비스에 비밀번호를 제공하여 얻어오는 Password Credentials, 서비스 제공자가 신뢰할 수 있는 클라이언트에게만 사용하는 Client Credentials, Code 값을 생략하고 프론트엔드에게 임시적으로 발급하는 Impicit 방식이 존재한다. 실무에서 가장 많이 사용되는 것은 Code Grant 이다. 여담으로 OAuth 는 블록체인과는 반대의 가치를 추구한다. 탈중앙화를 추구하는 블록체인과는 다르게 OAuth 는 사용자의 개인정보를 전적으로 서비스 제공자 의존하고 있으므로 더욱 집중화된다.

 

https://github.com/pronist/StudyBook

 

GitHub - pronist/StudyBook: 📕 공부의 기록, 알고리즘, 책 및 블로그(https://pronist.tistory.com) 예제코드 모

📕 공부의 기록, 알고리즘, 책 및 블로그(https://pronist.tistory.com) 예제코드 모음 - GitHub - pronist/StudyBook: 📕 공부의 기록, 알고리즘, 책 및 블로그(https://pronist.tistory.com) 예제코드 모음

github.com

더 읽을거리

쿠키(Cookie), 그리고 세션(Sessions)에 대해 알아보자

JWT(JSON Web Token)의 개념부터 구현까지 알아보기