golang/oauth2を使ってgithubのOAuth認証用APIサーバを作ってみる(WIP)

# はじめに
この記事は、[Go4](https://qiita.com/advent-calendar/2019/go4) アドベントカレンダーの2日目になります。
OAuthの認証ってどうやって実装するのだろう。
[golang/oauth2: Go OAuth2](https://github.com/golang/oauth2)があることは知っていましたが、実際に触ったことがなかったので、今回認証用のAPIサーバを作ってみようと思いました。
時間がなくてまだWIPです。。。
[hirano00o/github-oauth-sample](https://github.com/hirano00o/github-oauth-sample)

# golang/oauth2の使い方

[oauth2/example_test.go at master · golang/oauth2](https://github.com/golang/oauth2/blob/master/example_test.go) を見ると、使い方がわかります。

簡単にまとめるとこんな感じ。

1. `conf := &oauth2.Config{}` にGithubのCLIENT IDやSECRET、エンドポイントを詰める。  
  エンドポイントは、[oauth2/github.go at master · golang/oauth2](https://github.com/golang/oauth2/blob/master/github/github.go) を`import`して利用する。  
  **この`conf`は、トークンを取得するときや、APIにアクセスするときにも利用する。**
2. `conf.AuthCodeURL()`でCSRF対策用のstate等を入れて、GithubにログインするためのURLを作成する。  
  このURLにアクセスすると、Githubのログイン画面が出力され、ログインするとコールバック用のURLにリダイレクトされる。  
  コールバック用のURLは、CLIENT IDやSECRETを作成したときに設定したもの。
3. コールバック用URLにリダイレクトされたリクエストには、`code`や`state`がformに設定されている。  
  この`state`が、`AUthCodeURL()`で設定した`state`とイコールかを確認し、イコールであれば`code`を基に`conf.Exchange()`でトークンを発行する。  
  `Exchange()`で発行したトークンは、構造体であり、`access token`や`token type`, `refresh token`, `expiry`が入っている。
4. このトークンとコンテキストで`conf.Client()`からhttpClientを作り、GetやPost等を行う。  
  ちなみに、**`conf.Client()`したとき、`TokenSource()`が呼ばれ、トークンのリフレッシュが必要なときは、自動的にリフレッシュされる。**

# フロー
登場人物は

- ユーザと
- フロント用サーバ
- **認証用APIサーバ(今回作っているもの)**

です。
コールバック先は、フロント用サーバとしています。

1. ユーザがフロント用サーバにアクセスする。フロント用サーバはセッションIDを払い出しCookieに詰める。
2. ユーザがログインボタンを押すと、フロント用サーバは、セッションIDを取得し、**認証用APIサーバ**にセッションIDを渡す。
4. **認証用サーバ**は、ランダム値の`state`を作成、githubログイン用URLを発行する。`state`とセッションIDを紐付けてDBに入れる。
5. フロント用サーバは、ユーザを返却されたログイン用URLにリダイレクトさせる。
6. ユーザがログインに成功するとコールバックされ、フロント用サーバは`code`や`state`を取得する。
7. フロント用サーバは、`code`と`state`、セッションIDを**認証用APIサーバ**に渡す。
8. **認証用APIサーバ**は、`state`がURL発行時のものとイコールか確認し、イコールであれば、`code`からgithub用のトークンを作成する。  
  また、ユーザ用のトークンを作成し、これとgithub用のトークンを紐付けてDBに保存し、ユーザ用のトークンのみ返却する。
10. フロント用サーバは、トークンをCookieに詰めてユーザに返す。

(例えば)この後に、リポジトリ一覧を取得する場合は、

1. フロント用サーバがCookieからトークンを取得し、認証用APIサーバに渡す。
2. 認証用APIサーバは、トークンの期限を確認し、トークンと紐づくgithub用のトークンを返す。
3. フロント用サーバは、返却されたgithub用のトークンでリポジトリ一覧を取得する

ユーザ用のトークンとか無しで、セッションIDとgithub用のトークンを紐付ければよかったか...

# コード(WIP)
Clean Architectureを意識して作ってみています。
[hirano00o/github-oauth-sample](https://github.com/hirano00o/github-oauth-sample)

```bash:ディレクトリ構成
.
├── domain
│   └── domain.go
├── go.mod
├── go.sum
├── infrastructure
│   ├── config.go
│   ├── dbhandler.go
│   └── router.go
├── interfaces
│   ├── controllers
│   │   ├── context.go
│   │   ├── controller.go
│   │   └── error.go
│   ├── database
│   │   ├── database.go
│   │   └── repository.go
│   └── handler.go
├── main.go
├── mysql
│   └── init.sql
├── swagger.yaml
└── usecases
    └── usecase.go
```

よく利用する`oauth2.Config`は、`infrastructure`層の`config.go`で環境変数を読み込むときに一緒に作り、コンフィグとして読み込んでいます。

```golang:infrastructure/config.go
func getGithubConf() (github oauth2.Config) {
        scopes := []string{"repo"}
        github = oauth2.Config{
                ClientID:     os.Getenv("GITHUB_CLIENT_ID"),
                ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"),
                RedirectURL:  os.Getenv("SERVER_HOST"),
                Scopes:       scopes,
                Endpoint:     oauth2github.Endpoint,
        }
        return
}
```

そして読み込んだコンフィグは、`interface層`の`controller`に渡して利用しています。
正直どこで作るべきか悩みましたが、毎回どこかで作るよりかは...ってことでコンフィグ読み込み時に一緒にしました。
`usecase`層まで足を伸ばしているのが、微妙に感じるけど問題ないのか？ state入りのURL作ったり、トークン発行するためには必要だけども。

```golang:infrastructure/router.go
func Router() *gin.Engine {
        conf := NewConf()
        controller := controllers.NewController(NewDB(conf.DBConf.Database, conf.DBConf.DSN))
        router := gin.Default()
        v1 := router.Group("/v1")
        auth := v1.Group("/auth")
        github := auth.Group("/github")
        github.GET("/login", func(c *gin.Context) { controller.Login(c, conf) })
        github.GET("/callback", func(c *gin.Context) { controller.Callback(c, conf) })
        github.GET("/token", func(c *gin.Context) { controller.Auth(c) })
        zap.S().Info("running")
        return router
}
```

# おわりに
なるべく早く完成させます。
DBは、Redisに変更したいし。テスト作らないと。。。