OAuth 적용 시작하기 전
OAuth에 대한 설명은 아래 포스팅을 봐 주시면 감사하겠다!
2022.06.02 - [Study/Server] - [Server] OAuth 알고 쓰기
Google의 OAuth2를 이 포스팅에 작성된 절차를 기준으로 하나씩 살펴볼 것이다.
OAuth 적용 절차
1. Client는 OAuth를 사용하기 위해 사전에 Resource Server에게 OAuth에 대한 사용에 대한 동의를 받아 둔다.
-> 이 과정에서 Client ID, Client Secret, Authorized redirect URIs를 얻게 된다.
(1) console.cloud.google.com에 접근.
(2) google people api 검색 후 접근.
(3) 사용자 인증 정보의 사용자 인증 정보 만들기 에서 OAuth 클라이언트 ID 클릭.
(4) 필요한 값들을 적절히 입력.
(5) 얻은 Client ID, Client Secret를 기록해 두고 작성한 리디렉션 URI를 기록해 둔다.(다시 확인할 수 있음)
2. Client가 Resource Owner에게 Resource Server가 요구하는 URI를 담은 링크를 제공하면서 데이터의 사용에 대한 승인을 요구한다.
(1) main.go
var oauthConf = oauth2.Config{
ClientID: ????,
ClientSecret: ????,
RedirectURL: "http://localhost:4000/callback",
Scopes: []string{
"https://www.googleapis.com/auth/userinfo.email",
},
Endpoint: google.Endpoint,
}
// SSR을 통해 Resource Owner에게 Resource Server가 요구하는 URI를 전달하기 위한 구조체.
type urlData struct {
Url string
}
// 32byte의 무작위 문자열을 생성하여 CSRF attack을 대비.
// 꼭 32byte일 필요 없음.
func getToken() string {
b := make([]byte, 32)
rand.Read(b)
return base64.RawStdEncoding.EncodeToString(b)
}
// Resource Server에게 URI를 전달 받음.
func getLoginURL(state string) string {
return oauthConf.AuthCodeURL(state)
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
token := getToken()
url := urlData{Url: getLoginURL(token)}
templates.ExecuteTemplate(w, "home", url)
}
(2) oauth.html
{{define "home"}}
<!DOCTYPE html>
<html>
<head>
<title>Google OAuth</title>
</head>
<body>
<button class="social__button social__button--Google e-o-auth" type="button" onclick="window.location.href='{{.Url}}'">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" viewBox="0 0 18 18"><path fill="#4285F4" d="M17.785 9.169c0-.738-.06-1.276-.189-1.834h-8.42v3.328h4.942c-.1.828-.638 2.073-1.834 2.91l-.016.112 2.662 2.063.185.018c1.694-1.565 2.67-3.867 2.67-6.597z"></path><path fill="#34A853" d="M9.175 17.938c2.422 0 4.455-.797 5.94-2.172l-2.83-2.193c-.758.528-1.774.897-3.11.897-2.372 0-4.385-1.564-5.102-3.727l-.105.01-2.769 2.142-.036.1c1.475 2.93 4.504 4.943 8.012 4.943z"></path><path fill="#FBBC05" d="M4.073 10.743c-.19-.558-.3-1.156-.3-1.774 0-.618.11-1.216.29-1.774l-.005-.119L1.254 4.9l-.091.044C.555 6.159.206 7.524.206 8.969c0 1.445.349 2.81.957 4.026l2.91-2.252z"></path><path fill="#EB4335" d="M9.175 3.468c1.684 0 2.82.728 3.468 1.335l2.531-2.471C13.62.887 11.598 0 9.175 0 5.667 0 2.638 2.013 1.163 4.943l2.9 2.252c.727-2.162 2.74-3.727 5.112-3.727z"></path></svg>
</button>
</body>
</html>
{{end}}
(3) 실행 결과
3. Resource Server는 Resource Owner에게 로그인을 요청하고 링크의 URI를 통해 Client의 Client ID, Client Secret, redirect URI를 통해 옳은 요청인지 확인한 후 Client에게 제공할 데이터를 허용할 범위를 설정할 수 있는 페이지를 보여준다.
(1) 실행 결과
데이터 허용 범위를 설정할 수 있는 기능을 Resource Owner에게 제공하지 않은 상태이다.
(2) 승인할 수 없는 요청을 보냈을 때
4. Resource Server는 해당 절차가 끝나면 Authorization Code를 포함한 URI로 Resource Owner의 웹 브라우저가 리다이렉트할 수 있도록 요청한다.
5. Resource Owner의 웹 브라우저는 은밀히 Resource Server가 알려준 URI로 이동을 하게 되며 이 과정을 통해 Client는 해당 Resource Owner의 Authorization code의 값을 알게 된다.
6. Client는 Client ID, Client Secret 그리고 grant type에 맞는 데이터를 Resource Server에 제공한다.
7. Resource Server 는 Client의 요청에 대해 본인이 가진 정보와 일치하는지 확인 후 일치한다면 Authorization code에 대한 값은 삭제하고 Token을 만들어 Client에게 제공한다.
(1) main.go
func callbackHandler(w http.ResponseWriter, r *http.Request) {
// code Query에 Authorization code가 담긴 Request를 받는다.
code := r.URL.Query().Get("code")
// oauthConf에 작성한Client의 정보와 code를 통해 Resource Server로 부터 token을 부여받는다.
token, err := oauthConf.Exchange(context.Background(), code)
if err != nil {
handleErr(w, err)
return
}
response, err := http.Get(oauthGoogleUrlAPI + token.AccessToken)
if err != nil {
handleErr(w, err)
return
}
defer response.Body.Close()
contents, err := ioutil.ReadAll(response.Body)
if err != nil {
handleErr(w, err)
return
}
w.Write(contents)
}
(2) 실행 결과
(3) Exchange 함수를 통해 부여받는 Token struct
//oauth2.go
type Token struct {
// AccessToken is the token that authorizes and authenticates
// the requests.
AccessToken string `json:"access_token"`
// TokenType is the type of token.
// The Type method returns either this or "Bearer", the default.
TokenType string `json:"token_type,omitempty"`
// RefreshToken is a token that's used by the application
// (as opposed to the user) to refresh the access token
// if it expires.
RefreshToken string `json:"refresh_token,omitempty"`
// Expiry is the optional expiration time of the access token.
//
// If zero, TokenSource implementations will reuse the same
// token forever and RefreshToken or equivalent
// mechanisms for that TokenSource will not be used.
Expiry time.Time `json:"expiry,omitempty"`
// raw optionally contains extra metadata from the server
// when updating a token.
raw interface{}
}
전체 코드
package main
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"github.com/gorilla/mux"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"html/template"
"io/ioutil"
"net/http"
)
var templates *template.Template
var oauthConf = oauth2.Config{
ClientID: "????",
ClientSecret: "????",
RedirectURL: "http://localhost:4000/login/callback",
Scopes: []string{
"https://www.googleapis.com/auth/userinfo.email",
},
Endpoint: google.Endpoint,
}
const oauthGoogleUrlAPI = "https://www.googleapis.com/oauth2/v2/userinfo?access_token="
type errMsg struct {
errorMsg string
}
type urlData struct {
Url string
}
func handleErr(w http.ResponseWriter, err error) {
msg := errMsg{
errorMsg: err.Error(),
}
json.NewEncoder(w).Encode(msg)
}
func getToken() string {
b := make([]byte, 32)
rand.Read(b)
return base64.RawStdEncoding.EncodeToString(b)
}
func getLoginURL(state string) string {
return oauthConf.AuthCodeURL(state)
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
token := getToken()
url := urlData{Url: getLoginURL(token)}
templates.ExecuteTemplate(w, "home", url)
}
func callbackHandler(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
token, err := oauthConf.Exchange(context.Background(), code)
if err != nil {
handleErr(w, err)
return
}
response, err := http.Get(oauthGoogleUrlAPI + token.AccessToken)
if err != nil {
handleErr(w, err)
return
}
defer response.Body.Close()
contents, err := ioutil.ReadAll(response.Body)
if err != nil {
handleErr(w, err)
return
}
w.Write(contents)
}
func main() {
templates = template.Must(template.ParseGlob("static/*.gohtml"))
r := mux.NewRouter()
r.HandleFunc("/", indexHandler).Methods("GET")
r.HandleFunc("/login/callback", callbackHandler).Methods("GET")
http.ListenAndServe(":4000", r)
}
나름 꼼꼼히 한다고 해보았는데 틀린 부분이 있을지도 모르겠다. 글을 보는 누군가 틀린 부분을 발견한다면 댓글로 알려주시면 감사하겠다!
참조 :
https://www.joinc.co.kr/w/man/12/oAuth2/Google
'Study > Go' 카테고리의 다른 글
[Go] Context 패키지 알고쓰기 (0) | 2022.08.17 |
---|---|
[Go] GC(Garbage Collection) 심화 (0) | 2022.08.05 |
[Go] 고루틴(GoRoutine) 심화(7) - 파이프라인 (0) | 2022.06.24 |
[Go] 고루틴(GoRoutine) 심화(6) - 고루틴 에러 처리 (0) | 2022.06.20 |
[Go] 고루틴(GoRoutine) 심화(5) - 누수 관리 (0) | 2022.05.24 |