雰囲気でOAuth2.0を使っているエンジニアがgolangで学んでみる
はじめに
技術書典で購入していたAuth屋さんの本を読みました。
とてもわかりやすくいい本ですが、チュートリアルを進める時にcurlコマンドとブラウザを行ったり来たりするのがめんどくさくなってきたので、golangで置き換えてみることにしました。
どうしようかと考えてみて🙄、下記のように実装してみます。
- httpサーバを起動させる
- アクセスするとgoogleにリダイレクトさせる
- callbackを受けたら認可コードでトークンリクエストをする
- トークンを取得したらリソースにアクセスする(GooleのAPIを叩く)
最終的なコードは以下にあります。
準備
以下を行います。
Auth屋さんの本でもチュートリアルを行う前に書かれている内容です。
ローカルのhttpサーバでcallbackを受けるためリダイレクトURIはhttp://localhost:8080/callback
をセットしておきます。
- GCPのコンソールでGoole Photo Library APIを有効にする
- 認証画面の作成
- OAuthクライアントIDの作成
- シークレット情報のダウンロード
- ダウンロードしたらjsonファイルを
client_secret.json
とリネームしてmain.goと同じフォルダに配置する。
実装
net/http
でhttpサーバを立てたらその中に処理を書いていきます。
main関数では/start
と/callback
の2つのエンドポイントと関数のセットを登録してhttpサーバを起動します。
setup関数は必要なパラメータをセットしてます。
func main() {
setUp()
http.HandleFunc("/start", start)
http.HandleFunc("/callback", callback)
err := http.ListenAndServe("localhost:8080", nil)
if err != nil {
log.Fatal(err)
}
log.Println("start server localhost:8080...")
}
リダイレクト
ブラウザでhttp://localhost:8080/start
を開くとGoogleの認可エンドポイントにhttpリダイレクトするようにします。
url.Values
に必要な以下のパラメータをセットします。
- response_type
- client_id
- state
- scope
- redirect_uri
PKCEを用いるので以下もセットします。
- code_challenge_method
- code_challenge
code_challengeにセットする文字列の値は、SHA256でハッシュ値を計算してBase64URLエンコードしたものをセットします。
RFC7636に書かれているcode_verifierのサンプル値にSHA256+Base64URLエンコードした値をセットします。
最終的にリダイレクトさせるコードは以下のようになりました。
func start(w http.ResponseWriter, req *http.Request) {
authEndpoint := oauth.authEndpoint
values := url.Values{}
values.Add("response_type", response_type)
values.Add("client_id", oauth.clientId)
values.Add("state", oauth.state)
values.Add("scope", oauth.scope)
values.Add("redirect_uri", redirect_uri)
// PKCE用パラメータ
values.Add("code_challenge_method", oauth.code_challenge_method)
values.Add("code_challenge", oauth.code_challenge)
// 認可エンドポイントにリダイレクト
http.Redirect(w, req, authEndpoint+values.Encode(), 302)
}
コールバック
Googleの認可エンドポイントにリダイレクトさせられた後はGoogleにログインをし、アプリが必要な権限の移譲に同意をします。
同意すると認可コードとともにredirect_uriで設定したlocalhostに戻されるのでその時の処理を書きます。
req.URL.Query()
で返されるurl.Values
に認可コードが入っているので、それをそのままアクセストークンをリクエストする関数に渡します。
func callback(w http.ResponseWriter, req *http.Request) {
query := req.URL.Query()
// トークンをリクエストする
result, err := tokenRequest(query)
if err != nil {
log.Println(err)
}
body, err := apiRequest(req, result["access_token"].(string))
if err != nil {
log.Println(err)
}
w.Write(body)
}
トークンリクエスト
必要なパラメータをセットしてトークンエンドポイントに送ります。
- client_id
- client_secret
- grant_type
- redirect_uri
- code
- code_verifier
codeにはリダイレクトされた時にもらった認可コードを、code_verifierにはSHA256+BASE64urlエンコードする前の文字列をセットします。
トークン取得に成功したら内容をリターンします。
func tokenRequest(query url.Values) (map[string]interface{}, error) {
tokenEndpoint := oauth.tokenEndpoint
values := url.Values{}
values.Add("client_id", oauth.clientId)
values.Add("client_secret", oauth.clientSecret)
values.Add("grant_type", grant_type)
// 取得した認可コードをトークンのリクエストにセット
values.Add("code", query.Get("code"))
values.Add("redirect_uri", redirect_uri)
// PKCE用パラメータ
values.Add("code_verifier", verifier)
req, err := http.NewRequest("POST", tokenEndpoint, strings.NewReader(values.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Printf("request err: %s", err)
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
log.Printf("token response : %s", string(body))
var data map[string]interface{}
json.Unmarshal(body, &data)
return data, nil
}
リソースアクセス
取得したトークンをHTTPリクエストのヘッダーにセットしてGoogleのAPIにアクセスします。
トークンはjsonに入っているのでそれを取り出してリクエスト関数に渡します。
リクエストを送りHTTPレスポンスのbodyを返す。
func apiRequest(req *http.Request, token string) ([]byte, error) {
photoAPI := "https://photoslibrary.googleapis.com/v1/mediaItems"
req, err := http.NewRequest("GET", photoAPI, nil)
if err != nil {
return nil, err
}
// 取得したアクセストークンをHeaderにセットしてリソースサーバにリクエストを送る
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
client := &http.Client{}
resp, err := client.Do(req)
if err != nil || resp.StatusCode != 200 {
log.Printf("http status code is %d, err: %s", resp.StatusCode, err)
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println(err)
return nil, err
}
return body, nil
}
Photo Library APIのレスポンスをw.Write
させているのでブラウザに結果が出力されたらOKです。
Discussion