LoginSignup
11
7

More than 5 years have passed since last update.

Goのoauth2 packageに使われているTokenSourceについて

Posted at

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種類に分かれていて

  1. ユーザークレデンシャルによる認証
  2. サービスアカウントによる認証
  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 を指定するオプションもあるのでなんとかできないこともない。

11
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
7