はじめに
Go で Google OAuth 2.0 のクライアントアプリケーションを実装します。
今回のシナリオは、Google アカウントでログインし、Google リソースサーバーからユーザー情報を取得します。
シーケンスは以下の通りです。
※1: 今回は導線無しで、直リンクでアクセスします
手順
1. Google Cloud の設定
詳細な認証情報の作成は省略します。Google Cloud Console のドキュメントをご参照ください。
主要な設定は以下の通りです。
- アプリケーションの種類: ウェブアプリケーション
- 承認済みのリダイレクト URI:
http://your_server_url/callback
認証情報作成後、クライアント ID とクライアントシークレットを取得しておきます。
2. セットアップ
2-1. Go 関連(ライブラリは OAuth2.0 関連のみ記載)
go 1.24.0
require (
golang.org/x/oauth2 v0.19.0
)
2-2. 環境変数
1 で取得したクライアント ID とクライアントシークレットを環境変数に設定します。
REDIRECT_URL
は、1 で設定した「承認済みのリダイレクト URI」と同じ値を設定します。
また、今回はユーザー情報を取得するためのスコープのみ設定します。
GOOGLE_CLIENT_ID=your_client_id
GOOGLE_CLIENT_SECRET=your_client_secret
REDIRECT_URL=http://your_server_url/callback
SCOPES="openid,email,profile"
3. 実装
3-1. OAuth2.0 クライアントの資格情報
import (
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
)
// 環境変数の値が入る
oauth2Config := &oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURL: redirectURL,
Scopes: scopes,
Endpoint: google.Endpoint,
}
3-2. State
CSRF 攻撃を防ぐために使用するランダム(他者が推測困難)な文字列です。
具体的な使用方法は、3-4 と、3-5 で説明します。
state := uuid.New().String()
3-3. PKCE
認可コード傍受攻撃を防ぐために使用します。仕様は RFC7636 に準拠しています。
具体的な使用方法は、3-4 と、3-5 で説明します。
func randomAlphaNumericString(length int) (string, error) {
chars := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~"
bytes := make([]byte, length)
if _, err := rand.Read(bytes); err != nil {
return "", fmt.Errorf("failed to generate random bytes: %w", err)
}
result := ""
for _, b := range bytes {
result += string(chars[b%byte(len(chars))])
}
return result, nil
}
codeVerifier, err := randomAlphaNumericString(43)
if err != nil {
return nil, fmt.Errorf("failed to generate code verifier: %w", err)
}
hash := sha256.Sum256([]byte(codeVerifier))
codeChallenge := base64.RawURLEncoding.EncodeToString(hash[:])
3-4. /authorize エンドポイント
認可リクエストを作成します。
3-2 で生成した state
と、3-3 で生成した codeChallenge
をクエリパラメータとして送信します。
当該エンドポイントにアクセスすると、Google 認可サーバーにリダイレクトされます。認可サーバーは認可画面を表示し、リソースオーナーに認可を要求します。
http.HandleFunc("/authorize", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, oauth2Config.AuthCodeURL(
state,
oauth2.SetAuthURLParam("code_challenge", codeChallenge),
oauth2.SetAuthURLParam("code_challenge_method", "S256"),
), http.StatusFound)
})
3-5. /callback エンドポイント
3-4 でリソースオーナーが認可を行った後、認可サーバーから当該エンドポイントにリダイレクトされます。
ここでは state の検証とトークンの取得を行います。
http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
// State の検証とトークンの取得
})
3-5-1. State の検証
3-4 で送信した state
と、認可サーバーから返却される state
を比較することで、CSRF 攻撃を検出します。値が一致しない場合は、処理の過程で攻撃者に介入された可能性があるため、エラーとして処理します。
receivedState := r.URL.Query().Get("state")
if state != receivedState {
http.Error(w, "invalid state", http.StatusBadRequest)
return
}
3-5-2. トークンの取得
認可サーバーから返却された認可コード(code
)を使用して、トークンを取得します。
また、この時、3-3 で生成した codeVerifier
も送信します。
これにより、認可サーバー側でトークン要求の正当性を確認してくれます。
具体的には、認可サーバー側で codeVerifier
をハッシュ化した値と、3-4 で事前に送信しておいた codeChallenge
を比較し、認可要求とトークン要求が同一の要求元であることを確認します。
code := r.URL.Query().Get("code")
if code == "" {
http.Error(w, "code not found", http.StatusBadRequest)
return
}
token, err := oauth2Config.Exchange(
ctx,
code,
oauth2.SetAuthURLParam("code_verifier", codeVerifier),
)
if err != nil {
http.Error(w, fmt.Sprintf("failed to exchange token: %v", err), http.StatusInternalServerError)
return
}
成功すれば、トークンを使用して Google API を呼び出すことができます。
3-6. ユーザー情報の取得
トークンを使用してユーザー情報を取得します。
client := oauth2Config.Client(ctx, token)
res, err := client.Get("https://www.googleapis.com/oauth2/v3/userinfo")
if err != nil {
return nil, fmt.Errorf("failed to get user info: %v", err)
}
ユーザー情報が取得できれば完了です。