Go でサクッと Google OAuth 2.0


## はじめに

Go で Google OAuth 2.0 のクライアントアプリケーションを実装します。

今回のシナリオは、Google アカウントでログインし、Google リソースサーバーからユーザー情報を取得します。  
シーケンスは以下の通りです。

```mermaid
sequenceDiagram
    participant RO as リソースオーナー
    participant B as ブラウザ
    participant C as OAuth 2.0 クライアント
    participant AS as Google 認可サーバー
    participant RS as Google リソースサーバー

    RO->>B: Google でログイン（※1）
    B->>C: /authorize
    C->>C: 認可リクエスト作成
    C->>B: リダイレクト to 認可サーバー
    B->>AS: リダイレクト
    AS->>RO: 認可画面を表示
    RO->>AS: 認可
    AS->>B: リダイレクト to /callback
    B->>C: リダイレクト
    C->>C: State 検証
    C->>AS: トークン要求
    AS->>C: トークン
    C->>RS: リソース要求
    RS->>C: リソース
    C->>B: リソース
    B->>RO: リソース
```

※1: 今回は導線無しで、直リンクでアクセスします

## 手順

### 1. Google Cloud の設定

詳細な認証情報の作成は省略します。[Google Cloud Console のドキュメント](https://developers.google.com/identity/protocols/oauth2?hl=ja#1.-obtain-oauth-2.0-credentials-from-the-dynamic_data.setvar.console_name.)をご参照ください。

主要な設定は以下の通りです。

- アプリケーションの種類: ウェブアプリケーション
- 承認済みのリダイレクト 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」と同じ値を設定します。
また、今回はユーザー情報を取得するためのスコープのみ設定します。

```bash
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 クライアントの資格情報

```go
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 で説明します。

```go
state := uuid.New().String()
```

#### 3-3. PKCE

認可コード傍受攻撃を防ぐために使用します。仕様は [RFC7636](https://datatracker.ietf.org/doc/html/rfc7636) に準拠しています。  
具体的な使用方法は、3-4 と、3-5 で説明します。

```go

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 認可サーバーにリダイレクトされます。認可サーバーは認可画面を表示し、リソースオーナーに認可を要求します。

```go
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 の検証とトークンの取得を行います。

```go
http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
  // State の検証とトークンの取得
})
```

##### 3-5-1. State の検証

3-4 で送信した `state` と、認可サーバーから返却される `state` を比較することで、CSRF 攻撃を検出します。値が一致しない場合は、処理の過程で攻撃者に介入された可能性があるため、エラーとして処理します。

```go
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` を比較し、認可要求とトークン要求が同一の要求元であることを確認します。

```go
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. ユーザー情報の取得

トークンを使用してユーザー情報を取得します。

```go
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)
}
```

ユーザー情報が取得できれば完了です。

## リファレンス

- [Google OAuth 2.0 のドキュメント](https://developers.google.com/identity/protocols/oauth2?hl=ja)
- [PKCE のドキュメント](https://datatracker.ietf.org/doc/html/rfc7636)