【Go言語】Goで学ぶOAuth認証

## はじめに
今回は、現代のWeb開発で必要不可欠なOAuth認証をGo言語で実装していこうと思います！
初学者の方でもしっかり理解していただくために基本的な概念から実際の実装フローまで解説させていただきます。

まずは、基本概念から見ていきましょう。

## OAuthの基本概念
- **OAuth**
    - 複数のWebサービスを連携して動作させるために使われる仕組み
    - 通常、Webサービスを利用するためは、個別にユーザーIDとパスワードを入力してユーザーを認証する必要がありますが、OAuthを利用することで、IDやパスワードを入力することなく、アプリケーション間の連携が可能



OAuthを理解していく上で重要な用語があるので押さえていきましょう。

:::note
- クライアント
    - OAuthを使用してユーザーの認証を行うアプリケーション(今回だと自分のGoアプリケーション)
- リソースオーナー
    - 保護されたリソース（例：個人情報）へのアクセスを許可するユーザー
- 認可サーバー
    - ユーザーの認証を行い、アクセストークンを発行するサーバー（例：Google、Facebookなど）
- リソースサーバー
    - 保護されたユーザーデータを保持するサーバー
:::

OAuth認証の基本的な流れは以下の通りです。
1\. **クライアントが認可サーバーにアクセス許可を要求する。**
2\. **ユーザー（リソースオーナー）が許可を与える。**
3\. **クライアントが認可コードを受け取る。**
4\. **クライアントがこの認可コードを使ってアクセストークンを要求する。**
5\. **認可サーバーがアクセストークンを発行する。**
6\. **クライアントがこのアクセストークンを使ってリソースサーバーから保護されたデータにアクセスする。**

全体の流れを以下の図で理解していきましょう。[(参照動画)](https://www.youtube.com/watch?v=j0pIlZdD7-A)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763692/38511057-13c9-9835-1e26-815fb3168194.png)

それでは、GoでOAuth認証を実装する方法を見ていきましょう。
今回は、Google OAuth 2.0を例として使用します。
また、以下はVScode上で作業を進めております。

## GoでOAuthを実装していく
### プロジェクトの作成
まずは、作業用ディレクトリを作成し、その中にGoのファイルを作成していきます。
```terminal:terminal
# go-oauthというディレクトリ名ですすめていきます
$ mkdir go-oauth && cd go-oauth

# Goファイルの作成
$ touch main.go
```
続いては、初期化をしていきます。
```terminal
# Go Modulesを初期化
$ go mod init go-oauth
```

### 必要なパッケージのインストール
パッケージを`go get`コマンドでインストールします。
```terminal
$ go get golang.org/x/oauth2
$ go get golang.org/x/oauth2/google
```

### Google API Consoleでプロジェクトを設定
以下の手順で設定していきます。
1\. [*Google API Console*](https://console.cloud.google.com/)にアクセスします。

2\. 新しいプロジェクトを作成します。
- 「プロジェクトの選択」→「新しいプロジェクト」

3\.「**認証情報**」タブから「認証情報を作成」→「OAuthクライアントID」を選択します。

4\. アプリケーションの種類として「Webアプリケーション」を選択します。

5\. 承認済みのリダイレクトURIとして http://localhost:8080/callback を追加します。

6\. クライアントIDとクライアントシークレットを取得します。

## 基本的なコード作成
それでは、コードを書いていきましょう。
以下は、OAuthフローを実装する基本的なGoのコードなります。

```golang:main.go
package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "net/http"

    "golang.org/x/oauth2"
    "golang.org/x/oauth2/google"
)

var (
    googleOauthConfig *oauth2.Config
    oauthStateString  = "random"
)

func init() {
    googleOauthConfig = &oauth2.Config{
        RedirectURL:  "http://localhost:8080/callback",
		
		// 先ほど作成した、ご自身の「クライアントID」と「クライアントシークレット」を入力してください
        ClientID:     "YOUR_CLIENT_ID",
        ClientSecret: "YOUR_CLIENT_SECRET",
        Scopes:       []string{"https://www.googleapis.com/auth/userinfo.email"},
        Endpoint:     google.Endpoint,
    }
}

func main() {
    http.HandleFunc("/", handleMain)
    http.HandleFunc("/login", handleGoogleLogin)
    http.HandleFunc("/callback", handleGoogleCallback)

    fmt.Println("Started server on http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func handleMain(w http.ResponseWriter, r *http.Request) {
    var htmlIndex = `<html>
<body>
    <a href="/login">Google Log In</a>
</body>
</html>`

    fmt.Fprintln(w, htmlIndex)
}

func handleGoogleLogin(w http.ResponseWriter, r *http.Request) {
    url := googleOauthConfig.AuthCodeURL(oauthStateString)
    http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}

func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
    content, err := getUserInfo(r.FormValue("state"), r.FormValue("code"))
    if err != nil {
        fmt.Println(err.Error())
        http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
        return
    }

    fmt.Fprintf(w, "Content: %s\n", content)
}

func getUserInfo(state string, code string) ([]byte, error) {
    if state != oauthStateString {
        return nil, fmt.Errorf("invalid oauth state")
    }

    token, err := googleOauthConfig.Exchange(oauth2.NoContext, code)
    if err != nil {
        return nil, fmt.Errorf("code exchange failed: %s", err.Error())
    }

    response, err := http.Get("https://www.googleapis.com/oauth2/v2/userinfo?access_token=" + token.AccessToken)
    if err != nil {
        return nil, fmt.Errorf("failed getting user info: %s", err.Error())
    }

    defer response.Body.Close()
    contents, err := ioutil.ReadAll(response.Body)
    if err != nil {
        return nil, fmt.Errorf("failed reading response body: %s", err.Error())
    }

    return contents, nil
}
```
`go run main.go`を叩いて、実行してみましょう！
```terminal
$ go run main.go

# http://localhost:8080にアクセスしてみましょう
Started server on http://localhost:8080
```

実際に`localhost:8080`にアクセスすると、`Google Log In`と表示されます。
そちらをクリックし、GoogleアカウントでログインしブラウザにJSONが表示されればOKです！！

では、順を追って説明していきます。
### 設定
ここではOAuth設定を初期化しています。
```golang
var (
    googleOauthConfig *oauth2.Config
    oauthStateString  = "random"
)

func init() {
    googleOauthConfig = &oauth2.Config{
        RedirectURL:  "http://localhost:8080/callback",
        ClientID:     "YOUR_CLIENT_ID",
        ClientSecret: "YOUR_CLIENT_SECRET",
        Scopes:       []string{"https://www.googleapis.com/auth/userinfo.email"},
        Endpoint:     google.Endpoint,
    }
}
```
> **変数宣言**

- `googleOauthConfig`: GoogleのOAuth 2.0設定を保持する変数

- `oauthStateString`: CSRF防止用のランダムな文字列

> **init関数**
- `RedirectURL`: 認証後のリダイレクト先

- `ClientID`: Google APIのクライアントID

- `ClientSecret`: Google APIのクライアントシークレット

- `Scopes`: 認証で要求する権限（ここではユーザーのメール情報）

- `Endpoint`: GoogleのOAuth 2.0エンドポイント

### main関数
main関数では、HTTPサーバーを設定し、各ルートに対応するハンドラ関数を定義しています。

```golang
func main() {
    http.HandleFunc("/", handleMain)
    http.HandleFunc("/login", handleGoogleLogin)
    http.HandleFunc("/callback", handleGoogleCallback)

    fmt.Println("Started server on http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}
```

> **ルートハンドラの設定**
- `/`（ルートURL）にアクセスした場合「handleMain関数」が呼ばれます

- `/login`にアクセスした場合「handleGoogleLogin関数」が呼ばれます

- `/callback`にアクセスした場合「handleGoogleCallback関数」が呼ばれます

> **サーバの起動**

- `fmt.Println("Started server on http://localhost:8080")`でサーバの起動メッセージを表示します

- `log.Fatal(http.ListenAndServe(":8080", nil))`でHTTPサーバをポート8080で起動します
    - エラーが発生した場合はログにエラーメッセージが記録されます


### ログインハンドラ
```golang
func handleGoogleLogin(w http.ResponseWriter, r *http.Request) {
    url := googleOauthConfig.AuthCodeURL(oauthStateString)
    http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}
```
> **handleGoogleLogin関数**

- `認証URLの生成`: `googleOauthConfig.AuthCodeURL(oauthStateString)`でGoogleのOAuth認証URLを生成

- `リダイレクト`: `http.Redirect(w, r, url, http.StatusTemporaryRedirect)`でユーザーを認証URLにリダイレクト


### コールバックハンドラ
ここでは、Googleからのコールバックを処理します。
認証コードを受け取り、それを使用してユーザー情報を取得します。
```golang
func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
    content, err := getUserInfo(r.FormValue("state"), r.FormValue("code"))
    if err != nil {
        fmt.Println(err.Error())
        http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
        return
    }

    fmt.Fprintf(w, "Content: %s\n", content)
}
```

### ユーザー情報の取得
```golang
func getUserInfo(state string, code string) ([]byte, error) {
    if state != oauthStateString {
        return nil, fmt.Errorf("invalid oauth state")
    }

    token, err := googleOauthConfig.Exchange(oauth2.NoContext, code)
    if err != nil {
        return nil, fmt.Errorf("code exchange failed: %s", err.Error())
    }

    response, err := http.Get("https://www.googleapis.com/oauth2/v2/userinfo?access_token=" + token.AccessToken)
    if err != nil {
        return nil, fmt.Errorf("failed getting user info: %s", err.Error())
    }

    defer response.Body.Close()
    contents, err := ioutil.ReadAll(response.Body)
    if err != nil {
        return nil, fmt.Errorf("failed reading response body: %s", err.Error())
    }

    return contents, nil
}
```

> **getUserInfo関数**

1\. **Stateの検証**

- `if state != oauthStateString`: CSRF防止のためのステート文字列を検証

2\. **アクセストークンの取得**

- `googleOauthConfig.Exchange(oauth2.NoContext, code)`: 認証コードをアクセストークンに交換

3\. **ユーザー情報の取得**

- `http.Get("https://www.googleapis.com/oauth2/v2/userinfo?access_token=" + token.AccessToken)`: アクセストークンを使用してユーザー情報を取得

4\. **レスポンスの処理**

- `defer response.Body.Close()`: レスポンスボディを閉じる

- `ioutil.ReadAll(response.Body)`: レスポンスボディを読み込む

## セキュリティ上の考慮事項
OAuth実装を行う際は、以下のセキュリティ上の考慮事項に注意を払いましょう。

:::note warn
- **HTTPS の使用**
    - 本番環境では必ずHTTPSを使用してください。これにより、通信が暗号化され、盗聴や改ざんといった中間者攻撃を防ぐことができます
- **状態パラメータの検証**
    - `oauthStateString`は、クロスサイトリクエストフォージェリ（CSRF）攻撃を防ぐために使用されます
    - 本番環境では、これをランダムな文字列に置き換え、セッションごとに一意の値を使用するべきです
- **スコープの最小化**
    - 必要最小限のスコープのみを要求してください。これにより、アプリケーションが必要以上の権限を持つことを防ぎます
- **アクセストークンの安全な保管**
    - アクセストークンは機密情報です。適切に暗号化して保存し、必要なときのみ使用するようにしてください
- **エラーハンドリング**
    - すべての可能性のあるエラーを適切に処理し、ユーザーに適切なフィードバックを提供してください

:::

## さいごに
本記事では、GoでOAuth認証を実装する方法について、ステップバイステップで解説してみました。
Goでは、`golang.org/x/oauth2`パッケージで比較的簡単に実装が可能でしたね。ただし、OAuthを実装する際はセキュリティ上の考慮事項をしっかりと把握した上で実装しましょう。

最後までご覧いただきありがとうございました！