<この記事は「Money Forward Advent Calendar 2015」の20日目の記事です>
最近 OmniAuth 用の OAuth2 のストラテジーを作る機会が何度かあったので、gem のコードやドキュメントを読んで調べたことをまとめました。独自に OmniAuth のストラテジー、特に OAuth2 のストラテジーを作る場合は是非参考にしてください。
なお、本投稿は OmniAuth の Strategy Contribution Guide および OmniAuth、OmniAuth OAuth2、さらに 有志が開発した OmniAuth 用の各種ストラテジーの実装を参考にしています。
OmniAuth
OmniAuth は、認証プロバイダを利用してユーザ認証する方法を標準化するための gem です。
例えば、Web アプリケーションを作るときに、複数の認証プロバイダを利用してユーザ認証できるようするとします。その時に、こっちのプロバイダは OAuth2 を採用していて、あっちは LDAP で、となったら、色々なプロトコルに対応しないと行けなくて大変ですね。また、同じ OAuth2 でも、プロバイダによってはプロトコルで定められた範囲外での要求事項があるかもしれません。
OmniAuth は、ストラテジーと呼ばれる仕組みを提供し、認証プロバイダを介したユーザ認証方法を標準化しています。どんな認証プロバイダを使う時でも、そのプロバイダに対応する Omniauth のストラテジーが用意されていれば、大体同じようなインターフェースを介してユーザ認証ができるのです。素晴らしいですね。
OmniAuth のストラテジー
OmniAuth のストラテジーは Rack のミドルウェアです。Rails アプリで omniauth のストラテジーを使ったことがある人は、以下の様な記述をしたことがあるかもしれません。
Rails.application.config.middleware.use OmniAuth::Builder do
# omniauth-twitter のストラテジを登録
provider :twitter, ENV['TWITTER_KEY'], ENV['TWITTER_SECRET']
end
Omniauth::Builder#provider
の中身を見ると、実際にミドルウェアを登録しているのがわかります。
また、上記のように omniauth-twitter ストラテジを登録した状態で rake middleware
を実行して見ると、ミドルウェア一覧に omniauth-twitter のストラテジが出てきます。
> rake middleware
...
use OmniAuth::Strategies::Twitter
...
ストラテジーは Rack のミドルウェアなので、rack アプリケーションであれば、rails に限らず、例えば sinatra などでも利用可能です。
ストラテジーを使った認証の流れ
ストラテジーを使った認証は、主に以下の3つのフェーズに分かれています。
セットアップフェーズ
ストラテジーの設定を行うフェーズです。動的なセットアップが必要な場合はここで行います。例えば、事前に必要な外部への http リクエストを行ったり、動的に変えたいオプションの値を作成してセットしたりします。不要な場合は特に何もしなくて構いません。
セットアップフェーズの実装は以下のようになっています。setup
オプションに Rack エンドポイントを渡せばそいつを呼び出してくれます。また truthy な値が渡されていれば、OmniAuth を使っているアプリのセットアップ用のパス(デフォルトでは/auth/:provider/setup
)が呼び出されます。デフォルトでは false なのでセットアップフェーズは呼び出されません。
def setup_phase
if options[:setup].respond_to?(:call)
log :info, 'Setup endpoint detected, running now.'
options[:setup].call(env)
elsif options.setup?
log :info, 'Calling through to underlying application for setup.'
setup_env = env.merge('PATH_INFO' => setup_path, 'REQUEST_METHOD' => 'GET')
call_app!(setup_env)
end
end
また、単純に setup_phase をオーバーライドしても良さそうです。
リクエストフェーズ
リクエストフェーズでは、認証に関する情報の収集を行います。例えば、OAuth2 などでは外部サイトへのリダイレクトを行い、LDAP などではユーザに情報の入力をさせたりするフェーズです。
独自のストラテジーを作成する場合は、request_phase
メソッドを上書きしてリクエストフェーズの実装を行います。
コールバック
コールバックフェーズでは、リクエストフェーズで集めたデータを、OmniAuth が定めたフォーマットに詰め込んで、アプリケーションに渡します。
独自のストラテジーを作成する場合、callback_phase
メソッドを上書きしても良いのですが、OmniAuth が用意している DSL を使えば、適切なフォーマットにデータをセットする作業を OmniAuth に任せる事ができます。例えばこんな感じです。
module OmniAuth
module Strategies
class Developer
include OmniAuth::Strategy
...
uid do
request.params[options.uid_field.to_s]
end
end
end
end
DSL は以下の通りです。
- uid: プロバイダ内でユニークな ID
- info: ユーザに関する様々な情報を持つハッシュ。キーは Auth Hash Schema に記載されているものが利用できます。
- credentials: ストラテジーの実行によって何かしらのクレデンシャルを取得する場合はここにセットします。
- extra: 追加で含めておきたい情報をここに含めます。
ストラテジーの設定
ストラテジーの設定は、OmniAuth が提供する Options オブジェクトにセットすることが強く推奨されています。オプションを宣言してデフォルト値をセットするには、以下の用に option オプション名, デフォルト値
と記述します。
module OmniAuth
module Strategies
class Developer
include OmniAuth::Strategy
option :fields, [:name, :email]
option :uid_field, :email
end
end
end
このように宣言したオプションは、ストラテジーの初期化時に自動でデフォルト値に設定されます。また、初期化時に上書きすることも可能です。
app = lambda{|env| [200, {}, ["Hello World."]]}
OmniAuth::Strategies::Developer.new(app).options.uid_field # => :email
OmniAuth::Strategies::Developer.new(app, :uid_field => :name).options.uid_field # => :name
provider メソッドを使ってストラテジーを登録する場合も、最後の引数にハッシュを渡すことでオプションの上書きが可能です。
Rails.application.config.middleware.use OmniAuth::Builder do
provider :facebook, ENV['FACEBOOK_KEY'], ENV['FACEBOOK_SECRET'], scope: [:email]
end
Omniauth OAuth2
Omniauth OAuth2 は、Omniauth 用の OAuth2 ストラテジーの雛形を提供する gem です。この gem が定義する OmniAuth::Strategies::OAuth2
を継承して独自のストラテジーを作れば、簡単に OAuth2 に対応することができます。
OmniAuth OAuth2 ストラテジーの各フェーズ
リクエストフェーズ
リクエストフェーズでは外部サービスにリダイレクトするだけです。
def request_phase
redirect client.auth_code.authorize_url({:redirect_uri => callback_url}.merge(authorize_params))
end
ここで使われている client は、OAuth2 gem が提供する OAuth2::Client
クラスのインスタンスです。client_id
と client_secret
以外に追加でオプションを渡す場合は、client_options
オプションにハッシュを渡します。
def client
::OAuth2::Client.new(options.client_id, options.client_secret, deep_symbolize(options.client_options))
end
redirect_uri
redirect_uri は外部サービスがユーザを認証&認可を確認した後に、ユーザエージェントをリダイレクトしてくる先です。
callback_url は OmniAuth に以下のように定義されています。
def callback_url
full_host + script_name + callback_path + query_string
end
実は、callback_url は OmniAuth OAuth2 で以下のようにオーバーライドされていましたが、つい最近の PR で消されています。これによって redirect_uri
に渡す URL にクエリ文字列まで含まれるようになったのですが、これによって登録済みの redirect_uri とストラテジーが渡す redirect_uri が一致しなくなるケースがあり、結構色んなストラテジーが動かなくなったようです。背景はこの辺のコメントに書いてありますが、新しくストラテジーを作る場合は注意したいところです。
def callback_url
full_host + script_name + callback_path
end
authorize_params
#authorize_params
が返すハッシュには任意のパラメータを追加することができます。そのようなパラメータをオプションとして渡す場合は、authorize_options
オプションの配列に、渡したいオプションのキーを含めます。デフォルトでは、authorize_options
は :scope
のみを含む配列です(option :authorize_options, [:scope]
)。つまり、scope
オプションに値を渡しておけば、#authorize_params
はデフォルトで { scope: 'xxx' }
のようなハッシュを返します。
また、#authorize_params
内では state
パラメータもセットしています。この state
パラメータは RFC にも定められている通り、クライアントが渡した値がそのまま外部サービス(認可サーバ)から返ってきます。OmniAuth OAuth2 ストラテジーでは、state の値をセッションにも保存しておいて、コールバックフェーズで外部サービスから返ってきた値と比較することで CSRF 対策を行っています。
コールバックフェーズ
コールバックフェーズでは、前述の state のチェックや各種エラー処理などを行った後、アクセストークンを取得します。メインの処理は以下のところです。ここでも OAuth2::Client
のインスタンスを使ってトークンを取得しています。
def build_access_token
verifier = request.params["code"]
client.auth_code.get_token(verifier, {:redirect_uri => callback_url}.merge(token_params.to_hash(:symbolize_keys => true)), deep_symbolize(options.auth_token_params))
end
#token_params
が返すハッシュにパラメータを追加する場合は、authorize_params の時と同様に、含めたいパラメータをセットしたオプションのキーを token_options
の配列に追加しておきます。
アプリケーションに返す情報の作成
アクセストークンやリフレッシュトークンなどはデフォルトで以下の通りセットしてくれているのであまり気にする必要はありません。
credentials do
hash = {"token" => access_token.token}
hash.merge!("refresh_token" => access_token.refresh_token) if access_token.expires? && access_token.refresh_token
hash.merge!("expires_at" => access_token.expires_at) if access_token.expires?
hash.merge!("expires" => access_token.expires?)
hash
end
info
を設定するには追加の API 呼び出しが必要なことが多いと思われますが、ちょうど取得してきたアクセストークンがあるので、OAuth2::Client
を使えば簡単に取得できそうですね。ちなみに info
の取得は skip_info
オプションによって無効にできます。
uid
については、可能な限り追加の API 呼び出し無しでセットすることが推奨されています。外部サービスからのコールバック時のパラメータなどに uid
が含まれている場合はそちらを使いましょう。
TIPS など
HTTP 通信したい/JSON パースしたい/XML パースしたい
上記のことをやる場合は、以下の gem を使うことが強く推奨されています。特別な理由がない限りは、上記の目的でこれら以外の gem は使わないようにしましょう。
特殊なストラテジー名にしたい
例えば oauth
を単純にクラス名にしようとすると Oauth
になります。これを OAuth
にしたい、という時は、OmniAuth.config.add_camelization('oauth', 'OAuth')
のように add_camelization
を使います。
また、google_oauth2
のように gem 名(ファイル名)の途中にアンダースコアを含んだりする場合は、name
オプションによりプロバイダー名と手動で対応しておかないと上手く動かなかったりします。 こんな感じです。 option :name, 'google_oauth2'
同じストラテジーを複数回登録したい
name
オプションを使うと同じストラテジーを複数回登録できます。
provider :twitter, CLIENT_ID1, CLIENT_SECRET1
provider :twitter, CLIENT_ID2, CLIENT_SECRET2, name: 'twitter2'
ssl の認証を無効にしたい
OAuth2::Client を使って外部と通信するときに、どうしても開発環境で SSL の認証を無効にしたい時があったとします。そんな場合は client_options: { ssl: { verify: false } }
のように、client_options を渡してあげると良いです。ただし本番では絶対にやらないように。
テスト用に通信をモックしたい
この辺を参考にしてください。
なんか取り留めのない感じになってしまいましたが、参考になれば幸いです。なにか間違いやわかりにくい点があればご連絡ください。
実際にストラテジーを作る場合は、パッケージの構成なども指定されています。その他参考になる情報が色々と書いてあるので、[Strategy Contribution Guide]https://github.com/intridea/omniauth/wiki/Strategy-Contribution-Guide) は必読です。ここに書いたことも、このガイドを参考にして、更にコードを読みつつ試しつつ調べたものです。
それではみなさん、良い OmniAuth ライフを。