본문 바로가기
자습

Go OAuth2.0 서버

by litaro 2021. 1. 31.

OAuth2.0

OAuth2.0이란 표준 인증 프로토콜 또는 프레임워크이다.

여기서 명확히 해야할 점은 OAuth2.0이 직접적인 Authentication (인증) 보다는 Authorization (승인)과 관련이 있다는 점이다. 즉, 사용자가 PW나 별도 정보로 해당 ID의 소유주인지를 확인하는 인증의 프로세스에 관한 것이아니라, 이미 인증된 상황에서 다른 곳의 리소스를 접근하기 위해 허락해주는 승인 프로세스에 관한 것이다.

가장 일반적인 예로, 새로운 웹사이트 회원가입시 새로 ID를 발급받는것 대신 기존의 Google 이나 카카오톡 계정과 연계하는것을 선택하게 되는데 이때 웹사이트와 카카오톡 서버와의 승인 절차가 OAuth2.0 으로 이루어지는 것이다.

4가지 방법

itnext.io/an-oauth-2-0-introduction-for-beginners-6e386b19f7a9

 

An OAuth 2.0 introduction for beginners

How OAuth 2.0 works and how to choose the right flow

itnext.io

OAuth2.0 의 인증 과정의 최종 목적은 Server에 접근할 수 있는 Access Token을 받는것이다.

그리고 이 Access Token을 받기 위해서는 4가지 방법이 있다

  • Authorization Code Grant: 사용자가 login 하면 Code 가 발급되고 이 Code로 Access Token을 발급받는다.
  • Implicit Grant : 사용자가 login 하면 바로 Access Token이 발급된다.
  • Client Credentials: User 를 인증하는것이 아니라 Client 를 인증하여 Access Token을 발급한다.
  • Password Grant: 사용자 login 정보 + client secrete 까지 모든 정보로 요청해서 한번에 Access Token을 발급받는다.

Authorization Code Grant

이 중에 보통 사용하는 Authorization Code Grant 방식을 flow 로 알아보고 Go library를 활용해서 이를 구현한 예제를 따라해보자.

아래 Flow는 기본 flow에 아래 나오는 Go Oauth2.0 예제정보를 매핑한 flow이다.

GO Server/Client 

Go OAuth2.0 Server로 검색하면 나오는 아래 라이브러리에서 예제로 Authorization Code Grant 방식을 해볼수 있도록 제공해준다.

github.com/go-oauth2/oauth2

 

go-oauth2/oauth2

OAuth 2.0 server library for the Go programming language. - go-oauth2/oauth2

github.com

예제의 flow는 위의 Flow와 동일하고 코드를 보면서 flow를 이해해보자

client.go

기본 설정 

  • client id : client 식별 ID
  • client secrete : client를 인증할 secrete 값
  • scopes
  • redirect_url : authorization code 를 돌려받을 client url
  • authserver url (auth url, token url) : 인증과정을 진행할 url
import (
...

	"golang.org/x/oauth2"
)

const (
	authServerURL = "http://localhost:9096"
)

var (
	config = oauth2.Config{
		ClientID:     "222222",
		ClientSecret: "22222222",
		Scopes:       []string{"all"},
		RedirectURL:  "http://localhost:9094/oauth2",
		Endpoint: oauth2.Endpoint{
			AuthURL:  authServerURL + "/authorize",
			TokenURL: authServerURL + "/token",
		},
	}
	globalToken *oauth2.Token // Non-concurrent security
)

main 함수

  • 사용자가 Client시작 : :9094/호출 시 auth server (:9096/authorize) 로 redirect
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
	...
	http.Redirect(w, r, u, http.StatusFound)
})
  • auth server에서 authorization code를 redirect로 전달: /auth2 로 들어온 authorization code로 background에서 token을 요청 (:9096/token) 하여 가져옴
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
	...
	http.Redirect(w, r, u, http.StatusFound)
})

http.HandleFunc("/oauth2", func(w http.ResponseWriter, r *http.Request) {
	r.ParseForm()
	state := r.Form.Get("state")
	...
	code := r.Form.Get("code")
	...
	token, err := config.Exchange(context.Background(), code, oauth2.SetAuthURLParam("code_verifier", "s256example"))
	...
	globalToken = token

	e := json.NewEncoder(w)
	e.SetIndent("", "  ")
	e.Encode(token)
})
  • 사용자가 client를 통해 resource server 요청 (:9094/try): resource server (:9096/test)에 access_token으로 요청
http.HandleFunc("/try", func(w http.ResponseWriter, r *http.Request) {
	...
	resp, err := http.Get(fmt.Sprintf("%s/test?access_token=%s", authServerURL, globalToken.AccessToken))
	defer resp.Body.Close()

	io.Copy(w, resp.Body)
	...
})

server.go

main 함수

  • token 관리 저장소 설정 : default로 메모리에 key/value 형태로 저장
  • token 생성 : jwt token 
  • client 정보 저장: client_id, client_secrete, client_domain
manager := manage.NewDefaultManager()
manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg)

// token store
manager.MustTokenStorage(store.NewMemoryTokenStore())

// generate jwt access token
manager.MapAccessGenerate(generates.NewJWTAccessGenerate("", []byte("00000000"), jwt.SigningMethodHS512))

clientStore := store.NewClientStore()
clientStore.Set("222222", &models.Client{
	ID:     "222222",
	Secret: "22222222",
	Domain: "http://localhost:9094",
})
manager.MapClientStorage(clientStore)
  • Server object 생성
  • 승인 절차를위한 UserAuthoizationHandler 설정 : user 정보가 있는지 확인해서 없으면 내부적으로 /login 호출
srv := server.NewServer(server.NewConfig(), manager)

srv.SetUserAuthorizationHandler(userAuthorizeHandler)
func userAuthorizeHandler(w http.ResponseWriter, r *http.Request) (userID string, err error) {
	store, err := session.Start(r.Context(), w, r)
	...
	uid, ok := store.Get("LoggedInUserID")
	if !ok {
	...
		store.Set("ReturnUri", r.Form)
		store.Save()

		w.Header().Set("Location", "/login")
		w.WriteHeader(http.StatusFound)
		return
	}
	userID = uid.(string)
	store.Delete("LoggedInUserID")
	store.Save()
	return
}
  • login handler 설정 : login form 화면 (login.html) 을 보여주고 form 에 입력된 user ID/PW를 받아서 저장 후 /auth 호출
  • auth handler 설정 :  사용자에게 Authorization Grant 하는 화면 (auth.html) 을 띄움. 사용자가 Grant 하면 /authorize 호출
  • authorize handler 설정: Client 가 처음 요청하는 통로로 HandleAuthorizationRequest 에서 내부적으로 userAuthorizeHandler를 호출해서 /login 과 /auth 를 통해 들어온 정보로 authorization code를 redirect_url (:9094/oauth2) 로 전달
http.HandleFunc("/login", loginHandler)
http.HandleFunc("/auth", authHandler)

http.HandleFunc("/authorize", func(w http.ResponseWriter, r *http.Request) {
	store, err := session.Start(r.Context(), w, r)
	...
	var form url.Values

	if v, ok := store.Get("ReturnUri"); ok {
		form = v.(url.Values)
	}
	r.Form = form

	store.Delete("ReturnUri")
	store.Save()

	err = srv.HandleAuthorizeRequest(w, r)
	...
})
func loginHandler(w http.ResponseWriter, r *http.Request) {
	store, err := session.Start(r.Context(), w, r)
	...

	if r.Method == "POST" {
		...
		store.Set("LoggedInUserID", r.Form.Get("username"))
		store.Save()
		...
		w.Header().Set("Location", "/auth")
		w.WriteHeader(http.StatusFound)
		return
	}
	outputHTML(w, r, "static/login.html")
}

func authHandler(w http.ResponseWriter, r *http.Request) {
	store, err := session.Start(nil, w, r)
	...
	outputHTML(w, r, "static/auth.html")
}
  • token handler 설정: client 가 authorization code로 access token을 요청하면 발급하여 전달
http.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) {
	err := srv.HandleTokenRequest(w, r)
	...
})
  • test를 위한 handler 설정: clinet 가 access token을 전달하면 valid 한지 확인하여 해당 token의 client id, user id 정보를 반환
http.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
	token, err := srv.ValidationBearerToken(r)
	...
	data := map[string]interface{}{
		"expires_in": int64(token.GetAccessCreateAt().Add(token.GetAccessExpiresIn()).Sub(time.Now()).Seconds()),
		"client_id":  token.GetClientID(),
		"user_id":    token.GetUserID(),
	}
	e := json.NewEncoder(w)
	e.SetIndent("", "  ")
	e.Encode(data)
})

예제 실행

1. [User]-> [Client] : localhost:9094/로 요청

2. [Client] -> [Server]: localhost:9096/authorize 호출

3. [Server] /authorize -> /login :user 정보 없으니 /login 호출하여 login 정보 획득

4. [Server] /login -> /auth : 사용자 승인 요청

5. [Server] -> [Client] : /authorize 에서 user 정보와 승인 정보로 autorization code 발급하여 redirect url: localhost:9094/oauth2 로 전달

6. [Client] -> [Server] : /oauth2 로 들어온 authorization code로 acess token을 server (localhost:9094/token) 에 요청

7. [Server] -> [Client] : /token 에서 authorization code로 access token 발행

8. [User] -> [Client] : localhost:9094/try 로 내 정보 요청

9. [Client] -> [Server] : localhost:9096/test 에 access token 과 함께 요청

10. [Server] -> [Client] : /test 로 들어온 access token 이 유효한지 확인 후 정보 반환

Password Grant

Password Grant 방식도 내부 테스트 용으로 유용하게 쓰이니 이 방법도 한번 확인하자

server.go

기본 설정은 동일하고 Password Grant 처리를 위한 Handler를 아래와 같이 추가한다.

main.go

srv.SetPasswordAuthorizationHandler(func(username, password string) (userID string, err error) {
	userID = username
	return
})

Token 요청은 아래와 같이 하면 간단하게 access_token 을 받을 수 있다. 

  • Basic Auth Header : base64 encoded string "<client_id>:<client_secret>"
$ curl --request POST \
  --url http://localhost:9096/token \
  --header 'Authorization: Basic MjIyMjIyOjIyMjIyMjIy' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data grant_type=password \
  --data username=test \
  --data password=test \
  --data scope=user
  
{"access_token":"...","expires_in":7200,"refresh_token":"...","scope":"user","token_type":"Bearer"}

 

Summary

  • OAuth2 는 인증 프로토콜이라기 보다는 승인 프로토콜이다. 즉 A 서비스 서버에 이미 인증된 사용자가 B 서비스를 사용하려고 할때 A 서비스 인증정보를 활용해서 B 서비스로 인증받을 수 있도록 하는 프로토콜이다.
  • OAuth2 과정을 통해 최종적으로 얻고자 하는것은 Resource Server에 접근할 수 있는 Access Token 이다.
  • OAuth2 구현하는 방법에는 4가지가 있다. Authorization Code Grant, Implicit Grant, Client Credentials, Password Grant
  • Authorization Code Grant 를 구현하기 위해서는 Client 가 backend (server) 를 가지고 있어야 한다.
  • Go Auth2.0 library가 제공하는 예제를 통해 실제로 Authorization Code Grant 방식과 Password Grant 방식이 어떤 과정을 통해 이루어지는지 알수 있었다.
  • 예제에서는 간단하게 resource server와 auth server가 하나의 server이고, memory로 user/token 정보를 관리했는데 보통 분리된 서버로 구현하고 db로 user/token 을 관리하는 것으로 알고 있어 어떤 추가 작업이 필요할지는 좀더 공부가 필요하다.

 

 

'자습' 카테고리의 다른 글

Localstack 살펴보기  (0) 2021.07.16
React + Gin + Emqx Chat App  (0) 2021.02.27
node-forge를 이용한 RSA 암호화, CSR 생성  (0) 2021.01.16
Electron + Bootstrap 초보의 간단 앱 만들기  (0) 2021.01.09
Electron Quick Start  (0) 2021.01.06