oauth2 packageは何回も読んで毎回忘れてしまうので自分のメモ代わりにまとめます。
oauth2自体の話というよりは oauth2 packageの構成的な話と、中心になっているTokenSource interfaceの話になります。
golang.org/x/oauth2.TokenSource
type TokenSource interface {
Token() (*Token, error)
}
oauth2.TokenSource は Token() とするだけでアクセストークンを取得できる共通の仕組みです。
単体でアクセストークンを取得するためにも使いますが、 oauth2.NewClient() とセットで使うことが多いです。
oauth2.NewClient()の目的はoauth2.TokenSourceからアクセストークンを取得してAuthorizationヘッダーにセットする oauth2.Transport を http.RoundTripperて使ってくれる http.Client を生成してくれる程度なので、 oauth2.TokenSourceだけを使ってあとは自分でhttp.Clientを作ることも可能です。
このoauth2.TokenSourceを生成するためにいくつか方法があり、典型的なのは oauth2.Config と golang.org/x/oauth2/jwt.Config を使うことです。
oauth2.Configはgodocの説明をそのまま引用すると
Config describes a typical 3-legged OAuth2 flow, with both the client application information and the server's endpoint URLs.
jwt.Configは同様に
Config is the configuration for using JWT to fetch tokens, commonly known as "two-legged OAuth 2.0".
どちらもoauth2の共通のフローを実装していて設定を変えるだけで Provider が変わっても共通の仕組みで認証認可が可能になります。
oauth2.Configとjwt.Configはどちらも TokenSource() と Client() をもっていて、直接 oauth2.TokenSource と oauth2.Transportをもった http.Client を返してくれます。
Googleの実装
Googleの実装は golang.org/x/oauth2/google にある。
Google(GCP)の場合はトークンの取得方法は大きく3種類に分かれていて
- ユーザークレデンシャルによる認証
- サービスアカウントによる認証
- GCE/GAEのデフォルトサービスアカウントによる認証
ユーザークレデンシャルによる認証
Googleのconsoleから取得したclient credential使って認証する。
実態はclient_idとclient_secretによる個人の認証になっているため、実装としてはoauth2.Configを利用する。
そのためのインターフェイスとしてgolang.org/x/oauth2/google.ConfigFromJSON() がある。
サービスアカウントによる認証
プロジェクトのconsoleから取得したサービスアカウントを使って認証する。
実態としてはRSAの秘密鍵でJWTを署名するために利用し、実装としては jwt.Configを利用する。
そのためのインターフェイスとして golang.org/x/oauth2/google.JWTConfigFromJSON() がある。
GCE/GAEのデフォルトサービスアカウントによる認証
GCE/GAEの場合は特殊で、特に認証用のクレデンシャルがなくてもその環境専用のアクセストークンを取得できる。
GCEの場合はmetadataサーバーにアクセスするだけでトークンが返ってくる。(これがセキュリティの問題になることもある)
ということで、 oauth2.Configや jwt.Config のフローにのせられないため、 oauth2.TokenSource interfaceを直接実装している。
golang.org/x/oauth2/google.ComputeTokenSource() でoauth2.TokenSourceが返ってくる。
まとめたもの
大抵の場合 oauth2.Config とか jwt.Config とかどうでもいい、俺はトークンがほしいだけなんだという感じだと思うのでまとめたものも用意されている。
FindDefaultCredentials(), DefaultTokenSource(), DefaultClient() の3つ。
後者2つは FindDefaultCredentials() のラッパーである。
FindDefaultCredentials() は現在の状況からGCEなのかGAEなのかユーザー認証なのかサービスアカウント認証なのかを区別して良い感じにクレデンシャルを見つけてTokenSource を作ってくれる。
そこから DefaultTokenSource() は TokenSource を返すか、 DefaultClient() は oauth2互換の http.Clientを返すかの違いだけである。
トークンのキャッシュ
TokenSource()のToken() は愚直にトークンの新規発行を行う実装になっていることが多い。
発行されたアクセストークンは有効期限があり、その間では何度も使用可能なことが多いことと、トークンの発行にはコストがかかることが多いのでキャッシュする仕組みも入っている。
oauth2.ReuseTokenSource() はTokenSource をラップしてキャッシュの機能をいれた TokenSource を返してくれる。
oauth2 package内のものは最初からReuseTokenSourceを使ったものを返してくれている。
TokenSourceの罠
トークン発行のタイムアウト
auth2.TokenSource.Token() は見たらわかるとおり引数にcontext.Contextを受け取らない。
つまりトークンを取得してくるときのタイムアウトなどはTokenSourceの実装に依存する。
oauth2.Configとjwt.Configによって生成されるTokenSourceはトークンの取得自体に使う http.Cientは oauth2.NewClient() を使っていて、
これは http.DefaultClient か TokenSourceを作るときに渡す ctx に明示的に設定されていたらそれを使うかになる。参考
これも http.Clientを差し替えられるだけでタイムアウトはそのhttp.Clientを通してどうにかする必要がある。
ReuseTokenSourceはブロックする
TokenSource.Token() 自体は単に新しいをトークンを生成するだけなのでgoroutine-safeになっていることが多い。実際godocにもmustと書かれている。
ReuseTokenSource は トークンの有効期限内ではキャッシュして切れた後に初めて新しいトークンを生成しようとするというフローになっている。
そのため期限切れになったときにトークン発行リクエストが同時に行われる可能性(Thundering Herd)があり、その問題を回避するためか、トークン発行処理にはロックがかかっている。
全リクエストが独立して発行処理を行っても特に良いことはないので、このロックがあること自体は正しいけど、もし発行処理を行っているところが遅くなった場合は全リクエストに謎の(ロックまちによる)遅延がかかってしまう。
そもそもリクエスト時にトークンの発行処理を行うというのをやめたい。
期限切れたら処理するよりも定期的に発行し直してそれをキャッシュしておくという仕組みを間にはさみたい。
ちなみに google-cloud-go には クライアントを生成時に TokenSource を指定するオプションもあるのでなんとかできないこともない。