はじめに
この記事は、Go4 アドベントカレンダーの2日目になります。
OAuthの認証ってどうやって実装するのだろう。
golang/oauth2: Go OAuth2があることは知っていましたが、実際に触ったことがなかったので、今回認証用のAPIサーバを作ってみようと思いました。
時間がなくてまだWIPです。。。
hirano00o/github-oauth-sample
golang/oauth2の使い方
oauth2/example_test.go at master · golang/oauth2 を見ると、使い方がわかります。
簡単にまとめるとこんな感じ。
-
conf := &oauth2.Config{}
にGithubのCLIENT IDやSECRET、エンドポイントを詰める。
エンドポイントは、oauth2/github.go at master · golang/oauth2 をimport
して利用する。
このconf
は、トークンを取得するときや、APIにアクセスするときにも利用する。 -
conf.AuthCodeURL()
でCSRF対策用のstate等を入れて、GithubにログインするためのURLを作成する。
このURLにアクセスすると、Githubのログイン画面が出力され、ログインするとコールバック用のURLにリダイレクトされる。
コールバック用のURLは、CLIENT IDやSECRETを作成したときに設定したもの。 - コールバック用URLにリダイレクトされたリクエストには、
code
やstate
がformに設定されている。
このstate
が、AUthCodeURL()
で設定したstate
とイコールかを確認し、イコールであればcode
を基にconf.Exchange()
でトークンを発行する。
Exchange()
で発行したトークンは、構造体であり、access token
やtoken type
,refresh token
,expiry
が入っている。 - このトークンとコンテキストで
conf.Client()
からhttpClientを作り、GetやPost等を行う。
ちなみに、conf.Client()
したとき、TokenSource()
が呼ばれ、トークンのリフレッシュが必要なときは、自動的にリフレッシュされる。
フロー
登場人物は
- ユーザと
- フロント用サーバ
- 認証用APIサーバ(今回作っているもの)
です。
コールバック先は、フロント用サーバとしています。
- ユーザがフロント用サーバにアクセスする。フロント用サーバはセッションIDを払い出しCookieに詰める。
- ユーザがログインボタンを押すと、フロント用サーバは、セッションIDを取得し、認証用APIサーバにセッションIDを渡す。
-
認証用サーバは、ランダム値の
state
を作成、githubログイン用URLを発行する。state
とセッションIDを紐付けてDBに入れる。 - フロント用サーバは、ユーザを返却されたログイン用URLにリダイレクトさせる。
- ユーザがログインに成功するとコールバックされ、フロント用サーバは
code
やstate
を取得する。 - フロント用サーバは、
code
とstate
、セッションIDを認証用APIサーバに渡す。 -
認証用APIサーバは、
state
がURL発行時のものとイコールか確認し、イコールであれば、code
からgithub用のトークンを作成する。
また、ユーザ用のトークンを作成し、これとgithub用のトークンを紐付けてDBに保存し、ユーザ用のトークンのみ返却する。 - フロント用サーバは、トークンをCookieに詰めてユーザに返す。
(例えば)この後に、リポジトリ一覧を取得する場合は、
- フロント用サーバがCookieからトークンを取得し、認証用APIサーバに渡す。
- 認証用APIサーバは、トークンの期限を確認し、トークンと紐づくgithub用のトークンを返す。
- フロント用サーバは、返却されたgithub用のトークンでリポジトリ一覧を取得する
ユーザ用のトークンとか無しで、セッションIDとgithub用のトークンを紐付ければよかったか...
コード(WIP)
Clean Architectureを意識して作ってみています。
hirano00o/github-oauth-sample
.
├── 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
で環境変数を読み込むときに一緒に作り、コンフィグとして読み込んでいます。
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作ったり、トークン発行するためには必要だけども。
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に変更したいし。テスト作らないと。。。