この記事はQiitaで公開されていました
OAuth 2.0クライアントの実装
golang.org/x/oauth2を使ってトークンを発行する際に、複数の認可フローや方言があるけれど、どうやって実装するんだろう?と思ったことを調べました。OAuth 2.0自体については、OAuth 2.0の仕組みと認証方法が、どのようなレスポンスが返却されるのか記載されているので理解しやすいと思います。
アクセストークンの発行方法
OAuth 2.0には、リダイレクトを伴うものだけではなく、いくつかの認可フローが存在します。各フローの説明は、色々な OAuth のフローと doorkeeper gem での実装がわかりやすいと思います。
- Authorization Code
- Client Credentials
- Resource Owner Password Credentials
- Implicit Grant
以下で、golang.org/x/oauth2を使うと、それぞれの認可フローではどのようにhttp.Client
を取得するのか、をサンプルとして紹介します。
Authorization Code
ブラウザでリダイレクトを行うタイプのトークン取得方法です。3-legged OAuth 2.0と呼ばれます。この認可フローでは、grant_type
はauthorization_code
です。この場合、さらにオンラインとオフラインアクセスで微妙に異なります。以下はオフラインアクセスの例です。
conf := oauth2.Config{ ClientID: "YOUR_CLIENT_ID", ClientSecret: "YOUR_CLIENT_SECRET", Scopes: []string{"SCOPE1", "SCOPE2"}, Endpoint: oauth2.Endpoint{ AuthURL: "https://provider.com/o/oauth2/auth", TokenURL: "https://provider.com/o/oauth2/token", }, } url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline) // ...urlをブラウザで開いて認可コードを取得... authCode := "AUTH_CODE" token, err := conf.Exchange(context.Background(), authCode) if err != nil { log.Fatal(err) } client := conf.Client(context.Background(), token)
余談ですが、ユーザにコピペさせずにoauth2.Config.RedirectURL
で工夫してアクセストークンを取得する方法もあるそうです。
Client Credentials
少し簡略化したトークン取得方法です。2-legged OAuth 2.0と呼ばれます。このためには、golang.org/x/oauth2/clientcredentialsという別のパッケージが必要です。この認可フローでは、grant_type
はclient_credentials
です。
conf := clientcredentials.Config{ ClientID: "YOUR_CLIENT_ID", ClientSecret: "YOUR_CLIENT_SECRET", TokenURL: "https://provider.com/o/oauth2/token", Scopes: []string{"SCOPE1", "SCOPE2"}, } client := conf.Client(context.Background())
上記の例では直接http.Client
を取得していますが、clientcredentials.Config.Token(ctx)
を使うとトークンも取得できます。
Resource Owner Password Credentials
ユーザ名とパスワードを使ったトークンの取得方法です。この認可フローでは、grant_type
はpassword
です。
conf := oauth2.Config{ ClientID: "YOUR_CLIENT_ID", ClientSecret: "YOUR_CLIENT_SECRET", Scopes: []string{"SCOPE1", "SCOPE2"}, Endpoint: oauth2.Endpoint{ TokenURL: "https://provider.com/o/oauth2/token", }, } token, err := conf.PasswordCredentialsToken(ctx, "USER", "PASSWORD") if err != nil { log.Fatal(err) } client := conf.Client(ctx, token)
Authorization Codeと異なりoauth2.AuthCodeURL()
を呼び出さないため、Endpoint
のAuthURL
は不要です。
Implicit Grant
これはよくわかりません。
任意のヘッダを追加したい
認可サーバの仕様によっては、HTTPヘッダが必須になる場合があります。golang.org/x/oauth2にはカスタムしたHTTPリクエストを認証フローで使わせる方法があるので、これを使って行います。具体的には、context.Context
の値に、oauth2.HTTPClient
というキーでカスタムしたhttp.Client
をセットしておくという形になります。
type oauthTransport struct{} func (t *oauthTransport) RoundTrip(r *http.Request) (*http.Response, error) { r.Header.Set("User-Agent", "MyApp/1.0.0") return http.DefaultTransport.RoundTrip(r) } func main() { c := &http.Client{Transport: &oauthTransport{}} ctx := context.WithValue(context.Background(), oauth2.HTTPClient, c) conf := &oauth2.Config{...} token, err := config.PasswordCredentialsToken(ctx, "USER", "PASSWORD")
任意のパラメータを追加したい
試していませんが、認可コード取得時ならoauth2.SetAuthURLParam()
が使えると思います。
Authorizationヘッダではなくペイロードを使う
OAuth 2.0でクライアントシークレットを認可サーバへ渡す方法は2通り存在します。一つはBasic認証のヘッダで渡す方法。もう一つはリクエストのペイロードで渡す方法です。golang.org/x/oauth2は、通常はBasic認証の方法を使いますが、一部のペイロードを要求する既知のサービスはペイロードとして渡すようになっています。
独自または無名の認可サーバは、Basic認証で動作してしまうため、これが不都合な場合はoauth2.RegisterBrokenAuthHeaderProvider(tokenURL)
でペイロードとして渡されるようにURLを登録する必要があります。これは前方一致でマッチします。
func init() { oauth2.RegisterBrokenAuthHeaderProvider("https://provider.com/") }
JSONの形式が微妙に異なる場合に対応する
golang.org/x/oauth2パッケージは、トークン取得時のレスポンスに含まれるペイロードがフォームエンコードまたは以下のような形式のJSONであることを要求します。
{ "access_token": "xxx", "token_type": "bearer", "refresh_token": "xxx", "expires_in": 3600 }
そのため、以下のようなJSONは、そのままでは扱うことができません。
{ "auth_param": { "access_token": "xxx", "token_type": "bearer", "refresh_token": "xxx", "expires_in": 3600 } }
この場合、上で書いたヘッダを追加する方法と同じように、http.RoundTripper
で加工したものを返してあげる必要があります。
type oauthTransport struct{} type tokenJSON struct { Auth json.RawMessage `json:"auth_param"` } func (t *oauthTransport) RoundTrip(r *http.Request) (*http.Response, error) { resp, err := http.DefaultTransport.RoundTrip(r) if err != nil { return resp, err } var v tokenJSON decoder := json.NewDecoder(resp.Body) if err := decoder.Decode(&v); err != nil { return resp, err } resp.Body.Close() resp.Body = ioutil.NopCloser(bytes.NewReader(v.Auth)) resp.ContentLength = int64(len(v.Auth)) return resp, err }
json.RawMessage
は値を[]byte
のまま扱う型です。JSONの一部だけをそのまま扱いたい場合にとても便利です。