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
}