2015年10月16日

goji + goth で OAuth ログイン

ただの日記。

何となく Go 言語goji を使って OAuth クライアントとして外部サービスにログインして戻ってくる流れを作ってみようと思ったんだけど、実際書き始めてみると思っていたよりも迷ったので一応書き残しておく。

認証用ライブラリの選択

まず便利そうなパッケージをいくつかピックアップしてきた。
取りあえず自分がよく使う Twitter と Google+ アカウントでのログインを使いたいので、OAuth 1.x に対応していない gomniauth を除外。
次に gologin を見てみると、これは golang.org/x/net/context を活用したつくりになっていて結構良さそうに見える。ただ複数のサービスにログインできるようにするためには何気にコード量が多くなりそうなのが若干気になる。
最後に goth を見てみると、サンプルコードひとつで複数サービスへのログインを想定した作りになっていた。

今回はサンプルコードが何となく想像していた構造に近そうな goth にしてみることにした。
コードが短いのは結局のところ取り捨て選択に寄るもので細かい融通が利かないことも少なくないので、やりたいことや相性によって良くも悪くもなるので判断が難しい。

サンプルコードを元に goji 用に書き直す

これを単純に goji で置き換える形で書いてみたのがこちら。この段階ではまだ動いてない。
動かすと「you must select a provider」というメッセージが表示されるので、URL から「twitter」などのプロバイダ名が取得できていないことが容易に推測できる。
まあ書き直している時点で動かなくなることが想定済みだったけども。

じゃあそもそもプロバイダ名をどうやって取得しているのか? というのをまず確認することにした。

https://github.com/markbates/goth/blob/master/gothic/gothic.go#L161
該当の処理はここで、*http.Request から URL のクエリ部分を抜き出し "provider" あるいは ":provider" という名前のパラメータを取得しようとしていた。
先頭にコロンがついたものは元々のコードで使われていた github.com/gorilla/pat が独自に追加しているパラメータで、"/auth/{provider}" のようなパターンでハンドラを登録しておくと追加されるらしい。

そのすぐ上にある GetProviderName を外部から差し替える事でこの振る舞いを変えることもできるようだ。
ところで goji では "/auth/:provider/callback" のようなパターンでハンドラ登録をしておくと web.C.URLParams から URL のパラメータを取得できる仕組みになっているのだが、GetProviderName には *http.Request しか渡されないためそのままでは取得できず、微妙に意思の疎通ができていない感じになっている。

この時点で比較的アクロバティックでない手段として考えついたのは以下の様なものだった。
  • GetProviderName を差し替えて、自力で URL のパスからプロバイダ名を抽出する
  • pat の仕様に準拠するように、独自にパラメータを追加する
  • URL を /auth?provider=twitter のような形に変更する
候補としては一番最後のが愚直ながらも収まりの良さそうな解決方法ではあるのだが、この場合コールバック先 URL にも同じように GET パラメータを付与させておく必要があり、ユーザーがログイン成功後こちらに戻ってくるためにはログイン先のサービス側で GET パラメータの連結をしてもらわないといけなくなる。
それに失敗してログインできないような大手サービスがあるとはあまり思えないものの、可能なら避けたい気もするので今回は選ばなかった。

次に、pat に合わせる形で済ませると簡単そうかと思ったのでこれを考えてみた。
というのも最初は「もしかして req.URL.Query().Get("provider")Set に変えるだけで簡単に割り当ても出来るのでは?」と思ったのだが、どうやらこれは Query のタイミングで毎回 URL.RawQuery から生成されているようなので、直接 RawQuery を書き換えないといけないようだ。
そうなると "Raw" Query なのに実際には改ざん済みという状況を作らないといけなくて、個人的にこの仕様はあまり好きではないので除外した。
(もちろん、もし Set で割り当てができたとしても、結果としては RawQuery とは異なる状態を持つか RawQuery も伴って変更されるかどちらかだと思うので、どちらにしても受け入れがたいものではあった)

結局最後に残ったのは URL を自分で切り分けて検出するパターン。
しかしこれを自力でやるとせっかく goji がパースしてくれていたのに改めてパースし直さなければいけなくて全体的に見ると二度手間で、goji を使っている意味が薄れている感じがしてこれも気に入らないところではある。
ひとまずこれで動くようにはなった。

セッション周辺の挙動も気になる

環境変数に必要なデータを加えて PORT=3000 SESSION_SECRET=hoge TWITTER_KEY=fuga TWITTER_SECRET=moge go run main.go と実行させることで、取りあえず Twitter でのログインに成功するようになった。
しかしログイン後 Cookie を覗いてみると 500 バイトぐらいのデータが入りっぱなしだ。そのうち消えるとはいえ不始末感が拭えない。
また、セッションにはどうやら github.com/gorilla/sessions を使用していて、これを使っている場合は後始末が必要なのに元のサンプルコードでは特にやっていない。
「なんでやっていないんだろう」と一瞬考えたものの、確か github.com/gorilla/mux を使った時は自動でやってくれているはずなので、例で使われていた pat でも内部的にやっているんだろうと思った

内部でセッションに何を使っているのかは説明などで触れられていないので pat 以外で使おうとした際に後始末が忘れられそうだし、先述のプロバイダ名取得も含めつくりが pat に合わせてありそうな部分が見受けられるので、pat に依存気味でサンプルコードとしてはあまり良くないような気がしてきた。

とはいえ気付いてしまえばこれは単純に context.ClearHandler を goji の Middleware として追加するだけでいけるのでいいのだが、クッキーにデータが残りっぱなしになるのはあまりエレガントな対策方法が思いつかない。
sessions にも丸ごと削除する機能はなく、試しに中身のデータを消してみてもクッキー自体は残っていた。
もちろん http.SetCookie で直接消しに行く手はあるものの、外部から無理矢理叩いてる感じはとても美しくない。
取りあえず丸ごと消すのは諦めて、後始末をやるようにだけしたのがこちら

思っていたよりも綺麗に収まらなかった

pat を goji に変えた結果思っていたよりも修正項目が多かったのだが、これは恐らく goth での処理を更に単純化するために用意された gothic の柔軟性が今ひとつだった(≒実質的に pat を前提としたコードになっていた)ことに起因するものだろうと感じた。
セッションの後始末の話も単純にクッキーの読み書きとかなら必要なさそうだし、URL からのパラメータ抽出ももっと柔軟にできた方がいい。
例えば goji 以外でも github.com/julienschmidt/httprouter とか github.com/dimfeld/httptreemux とかのように、*http.Request 以外に URL からのデータ格納するケースは他にもあるわけだし。

その辺を踏まえて改めて仕切りなおしてみることにした。

自家製 gothic

ということで、もう少し柔軟に使えそうな gothic の代わりになるものを試しに作ってみた。
goth の構造が綺麗に整っていたので gothic の差し替えはとても綺麗にできた。

https://github.com/oov/gothic

主な変更点は以下のとおり。
  • GetProviderName を廃止し、プロバイダ名は直接渡す形に変更
  • セッションの代わりに github.com/gorilla/securecookie を使用し、後始末を不要に
  • 使い終わったクッキーはその場で破棄
  • GetState を廃止し OAuth 2.0 の state パラメータを内部で検証
これによって自分が使おうとした際に気になった部分は一掃できた。
改めて goji と組み合わせて使った場合のサンプルはこちら

わかりやすい変化としては引数が増えたことによって gothic.BeginAuthHandlerhttp.HandlerFunc ではなくなったので名前を gothic.BeginAuth に変更して、ついでに error を返すように変えたのでエラー処理が増えてサンプルコードではこの辺が少し冗長に見えるようになった。
ただ元々の gothic でも gothic.CompleteUserAuth に関しては戻り値を返していてハンドラにできていなかったし、個人的にはこの変更で両方の足並みが揃った感じがしていい。

gothic.BeginAutherror を返すようにしたのはハンドラではなく普通の関数ならエラーは処理せず返すのが自然だと思ったからで、内部で勝手に処理されると自前のエラーページなども表示できないし、エラーが起きたこと自体に気付けない(無視できることと気付けないことには大きな隔たりがある)し、結構微妙な問題があると思っている。
この変更をせずとも gothic.GetAuthURL で取得した URL にリダイレクトするコードを各自で書けば同じようにエラー処理はできたけど、パッケージを使う人のほとんどはリダイレクト先 URL が取得できたらその後はリダイレクトしたいわけで、エラー処理が必要になっても gothic.BeginAuth の存在意義は十分にあるように思う。

ところで実は元々は web.C にガッツリ依存させて gojic って名前にする冗談で作り始めたんだけど、作業している途中でプロバイダ名を単に引数で渡すようにするだけで素晴らしい柔軟性があることに気付いて最終的にこのような形に落ち着いた。

その他

Twitter でのログイン時に authorize じゃなくて authenticate を使いたかったものの対応していなかったのだが、これは仕様を変えなくても追加だけでスムーズにいけそうだったので PR を投げてマージしてもらった。

あと、ついここ最近の流れで http.Requestgolang.org/x/net/context.Context を持たせてはどうか、という話も持ち上がっているので、これの流れ次第でまたちょっとルータやライブラリに動きがあるかも知れない。
Clip to Evernote