動機 : GAE / Go でもTwitterのユーザーログインがしたい! but 認証ライブラリがそのままでは使えない!
React + API (GAE/Go) でシステムを作っていて、ログイン機能としてTwitter連携したい。
GoogleとTwitter Botの記事が多いがなんとかユーザー認証の仕方を学ぶ。
フロー自体については割愛。
公式ページの概要
以下、長いので結論を先に
goのバージョンは1.0 Standardな環境
ユーザー認証のための dghubble/oauth1 では http.DefaultClient.Transportを上書きで対応でき流が、
ctx := appengine.NewContext(r)
http.DefaultClient.Transport = &urlfetch.Transport{Context: ctx}
requestToken, requestSecret, err := config.RequestToken()
go-twitterでoauth1からhttp.Clientを生成するところはコンテキスト経由でurlfetchのトランスポートを食わせてやると良い
ctx := appengine.NewContext(r)
// コンテキストのValue経由でクライアントを渡す
newCtx := context.WithValue(ctx, oauth1.HTTPClient, urlfetch.Client(ctx))
token := oauth1.NewToken(accessToken, secret)
// ここで単に config.Client(ctx, token) としても処理本体であるurlfetch.Transport.RoundTripが渡らないためエラーとなる
httpClient := config.Client(newCtx, token)
client := lib.NewClient(httpClient)
user, _, err := client.Accounts.VerifyCredentials(nil)
// userには問題なくログインしたユーザーのtwitterのアカウント情報が入っている
Go言語用のTwitterクライアントは二つ
ざっと探すとanacondaが定番のようだが、ユーザー認証によるアクセストークンの取得方法がよくわからなかったのでパス
さらにVerifyCredentialsでboolしか取れないようなので、ユーザー認証は想定していないのかも?
そしてもう一つがgo-twitter
同じ開発者のライブラリに oauth1 があり、いい感じにtwitterログインによるアクセストークン取得のサンプルが書いてある。
つまり、oauth1 でアクセストークンを取得して、そのあとgo-twiiterでAPIを叩けば良い。
ところが、GAE/Goでは外部へのhttpアクセスは対応しないとエラーになる
http.DefaultClientなどの通常のアクセス方法では以下のようなエラーが出る。
http.DefaultTransport and http.DefaultClient are not available in App Engine. See https://developers.google.com/appengine/docs/go/urlfetch/overview
これについては公式のどこかのページにurlfetchを使えと書いてあり、ふた通りの対応がある。
1. http.Clientを切り替える方法
ライブラリが対応している必要がある。
import "google.golang.org/appengine/urlfetch"
func handler(r *http.Request, ...) {
ctx := appengine.NewContext(r)
newClient := urlfetch.Client(ctx)
// anacondaなどのhttp.Client切り替えに対応している場合は渡すことができる
api.Client = newClient
}
2. http.DefaultClientのTransportを上書きする
ライブラリがhttp.DefaultClient.Do(req)とかしていてClientの上書きができない場合はこの方法が使える。
oauth1でtwitterのアクセストークンを取得するところまではこの方法で対応できる。
func GetCallbackURL(r *http.Request) (string, error) {
ctx := appengine.NewContext(r)
http.DefaultClient.Transport = &urlfetch.Transport{Context: ctx}
// 問題なくアクセスできる
requestToken, requestSecret, err := config.RequestToken()
}
以上の二つの方法はググればすぐ出てくるし、公式ページからも見て取ることができる。
ここで問題。oauth1のconfig.Client()にどう対応する?
client.Transport = &urlfetch.Transport({Context: ctx}) とやると、oauth1のための認証ヘッダーの作成などが行われなくなる。
ソースを見るとTransportを使って処理しているため。urlfetch.Transportとバッティングしてしまう。
ソースコードを追っかけているとTransport.Base という感じに継承できるようになっていている。
func (t *Transport) RoundTrip (req ...) {
// 認証ヘッダーとかトークンの処理とか。。。
return t.base().RoundTrip(req2)
}
func (t *Transport) base() http.RoundTripper {
// ↓このBaseにurlfetch.Transportが入っていれば行けそう。
if t.Base != nil {
return t.Base
}
// ↓こいつのせいでエラーになる
return http.DefaultTransport
}
で、このBaseを設定しているのはconfig.go のNewClient()のとこ
config.Client(ctx, token) が NewClient() に渡って、contextTransport()に渡されている。
context.ContextのValueに入れておく、ということらしい。
直接渡せるようにしてもよかったのでは?
type contextKey struct{}
var HTTPClient contextKey
func contextTransport(ctx context.Context) http.RoundTripper {
if client, ok := ctx.Value(HTTPClient).(*http.Client); ok {
return client.Transport
}
return nil
}
というわけで、oauth1のconfig.Client()から帰ってきたhttp.Clientにurlfetchの処理を入れるためには、Valueにcurlfetch.Client()を入れたcontextをたすせば良いと。
というわけで上記の結論になるのでした。
システムの奥の部分を上書きできるになってる設計ってよくできてるなと勉強になりました。
欲を言えばコンパイル時にチェックができるとより良いんだろうけど。
それにしてもGoでtwitterでユーザ認証の情報がほとんどなかった。
GAE/Goはbot用途に使う人がほとんどだから需要はないんだろうなぁ。