Ruby
Rails
rack
OmniAuth

OmniAuth OAuth2 を使って OAuth2 のストラテジーを作るときに知っていると幸せになれるかもしれないこと

More than 1 year has passed since last update.

<この記事は「Money Forward Advent Calendar 2015」の20日目の記事です>

最近 OmniAuth 用の OAuth2 のストラテジーを作る機会が何度かあったので、gem のコードやドキュメントを読んで調べたことをまとめました。独自に OmniAuth のストラテジー、特に OAuth2 のストラテジーを作る場合は是非参考にしてください。

なお、本投稿は OmniAuth の Strategy Contribution Guide および OmniAuthOmniAuth OAuth2、さらに 有志が開発した OmniAuth 用の各種ストラテジーの実装を参考にしています。

OmniAuth

OmniAuth は、認証プロバイダを利用してユーザ認証する方法を標準化するための gem です。

例えば、Web アプリケーションを作るときに、複数の認証プロバイダを利用してユーザ認証できるようするとします。その時に、こっちのプロバイダは OAuth2 を採用していて、あっちは LDAP で、となったら、色々なプロトコルに対応しないと行けなくて大変ですね。また、同じ OAuth2 でも、プロバイダによってはプロトコルで定められた範囲外での要求事項があるかもしれません。

OmniAuth は、ストラテジーと呼ばれる仕組みを提供し、認証プロバイダを介したユーザ認証方法を標準化しています。どんな認証プロバイダを使う時でも、そのプロバイダに対応する Omniauth のストラテジーが用意されていれば、大体同じようなインターフェースを介してユーザ認証ができるのです。素晴らしいですね。

OmniAuth のストラテジー

OmniAuth のストラテジーは Rack のミドルウェアです。Rails アプリで omniauth のストラテジーを使ったことがある人は、以下の様な記述をしたことがあるかもしれません。

config/initializers/omniauth.rb
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 なのでセットアップフェーズは呼び出されません。

lib/omniauth/strategy.rb
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 メソッドを使ってストラテジーを登録する場合も、最後の引数にハッシュを渡すことでオプションの上書きが可能です。

config/initializers/omniauth.rb
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 ストラテジーの各フェーズ

リクエストフェーズ

リクエストフェーズでは外部サービスにリダイレクトするだけです。

lib/omniauth/strategies/oauth2.rb
def request_phase
  redirect client.auth_code.authorize_url({:redirect_uri => callback_url}.merge(authorize_params))
end

ここで使われている client は、OAuth2 gem が提供する OAuth2::Client クラスのインスタンスです。client_idclient_secret 以外に追加でオプションを渡す場合は、client_options オプションにハッシュを渡します。

lib/omniauth/strategies/oauth2.rb
def client
  ::OAuth2::Client.new(options.client_id, options.client_secret, deep_symbolize(options.client_options))
end

redirect_uri

redirect_uri は外部サービスがユーザを認証&認可を確認した後に、ユーザエージェントをリダイレクトしてくる先です。

callback_url は OmniAuth に以下のように定義されています。

lib/omniauth/strategy.rb
def callback_url
  full_host + script_name + callback_path + query_string
end

実は、callback_url は OmniAuth OAuth2 で以下のようにオーバーライドされていましたが、つい最近の PR で消されています。これによって redirect_uri に渡す URL にクエリ文字列まで含まれるようになったのですが、これによって登録済みの redirect_uri とストラテジーが渡す redirect_uri が一致しなくなるケースがあり、結構色んなストラテジーが動かなくなったようです。背景はこの辺のコメントに書いてありますが、新しくストラテジーを作る場合は注意したいところです。

lib/omniauth/strategies/oauth2.rb
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 のインスタンスを使ってトークンを取得しています。

lib/omniauth/strategies/oauth2.rb
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 の配列に追加しておきます。

アプリケーションに返す情報の作成

アクセストークンやリフレッシュトークンなどはデフォルトで以下の通りセットしてくれているのであまり気にする必要はありません。

lib/omniauth/strategies/oauth2.rb
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 は使わないようにしましょう。
- [faraday](https://github.com/lostisland/faraday_
- multi_json
- multi_xml

特殊なストラテジー名にしたい

例えば 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 ライフを。