Plan 9とGo言語のブログ

主にPlan 9やGo言語の日々気づいたことを書きます。

golang.org/x/oauth2で色々な認可フローや方言に対応する

この記事は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_typeauthorization_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_typeclient_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_typepasswordです。

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()を呼び出さないため、EndpointAuthURLは不要です。

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の一部だけをそのまま扱いたい場合にとても便利です。