go言語でoauth2パッケージを使う

oauth2パッケージ

go言語でoauth2を扱うのに便利なoauth2パッケージがあります。
今回は、これを使ってみたいと思います。
例として、Googleフォト(Picasa)のAPIを扱います。
ソースは下記を参照。


import (
  "bufio"
  "fmt"
  "io/ioutil"
  "log"
  "os"

  "golang.org/x/oauth2"
  "golang.org/x/oauth2/google"
)

func main() {
  //ClientIDやClientSecretはGoogle API Console からコピーしてきます。
  //Endpointはgoogle、github、facebookなどがoauth2配下のパッケージに用意されています。
  config := oauth2.Config{
    ClientID:     "xxxxxxxxxxx",
    ClientSecret: "yyyyyyyyyyyy",
    Endpoint:     google.Endpoint,
    RedirectURL:  "urn:ietf:wg:oauth:2.0:oob", //今回はリダイレクトしないためこれ
    Scopes:       []string{"https://picasaweb.google.com/data/"}, //必要なスコープを追加
  }
  
  //認証のURLを取得。AuthCodeURLには文字列を渡す。CSRF攻撃の回避のため、本当はリダイレクトのコールバックで検証する。
  url := config.AuthCodeURL("test")
  fmt.Println(url) //認証のURLを表示。ブラウザにコピペする

  //リダイレクト先がないため、ブラウザで認証後に表示されるコードを入力
  var s string
  var sc = bufio.NewScanner(os.Stdin)
  if sc.Scan() {
    s = sc.Text()
  }

  //アクセストークンを取得
  token, err := config.Exchange(oauth2.NoContext, s)
  if err != nil {
    log.Fatalf("exchange error")
  }

  client := config.Client(oauth2.NoContext, token) //httpクライアントを取得
  
  //取得したclientでGetする
  resp, err := client.Get("https://picasaweb.google.com/data/feed/api/user/default?start-index=1")
  if err != nil {
    log.Fatalf("client get error")
  }
  
  //レスポンスを表示
  defer resp.Body.Close()
  byteArray, _ := ioutil.ReadAll(resp.Body)
  fmt.Println(string(byteArray))

}

どこでoauth2のアクセストークンを扱っているか

上記のソースではトークンを取得した後、config.Client()でhttp Clientを取得してそれを使うだけで、oauth2の通信が行えています。
どこで、アクセストークンをセットし、トークンの期限が切れていたらリフレッシュトークンを使うなどの処理をしているか気になります。
ソースコードを見ていくと、config.Client()の先、NewClient()でhttp.Clientを作成しているのが見えます。
ここで、http.ClientのTransportに独自のTransportを設定しているのがそれっぽいです。

func NewClient(ctx context.Context, src TokenSource) *http.Client {
  //省略
  
  return &http.Client{
    Transport: &Transport{
      Base:   internal.ContextTransport(ctx),
      Source: ReuseTokenSource(nil, src),
    },
  }
}

このTransportですが、RoundTripというメソッドを持っています。この辺について詳しくは以下のページを参照。
Go http.RoundTripper 実装ガイド
RoundTrip内を見ると、トークンが切れていたらリフレッシュしたり、Authorizationヘッダにアクセストークンをつけているのが追えます。


func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
  if t.Source == nil {
    return nil, errors.New("oauth2: Transport's Source is nil")
  }
  token, err := t.Source.Token()  //アクセストークンが切れていたらリフレッシュする
  if err != nil {
    return nil, err
  }

  req2 := cloneRequest(req) 
  token.SetAuthHeader(req2)      //アクセストークンをAuthorizationヘッダに設定
  t.setModReq(req, req2)
  res, err := t.base().RoundTrip(req2)
  if err != nil {
    t.setModReq(req, nil)
    return nil, err
  }
  res.Body = &onEOFReader{
    rc: res.Body,
    fn: func() { t.setModReq(req, nil) },
  }
  return res, nil
}