2017-10-10

omniauthコードリーディング

からメインのアプリケーションをデコレートしたインスタンスを生成します。

たとえばtwitterとfacebookのproviderを以下のように設定したとします。 Rails.application.config.middleware.use OmniAuth::Builder do provider :twitter, ENV['TWITTER_API_KEY'], ENV['TWITTER_API_SECRET'] provider :facebook, ENV['FACEBOOK_KEY'], ENV['FACEBOOK_SECRET'], scope: 'email' end そうすると、OmuniAuth::Builder#to_appで作成されるRackアプリケーションは、以下のようにミドルウェアで順にラップ(ネスト)されたものになります。 OmniAuth::Strategies::Twitter( app: OmniAuth::Strategies::Facebook( app: Application ) ) 全体のuse呼び出しの順番が Middleware1 → Middleware2 → OmniAuth → Middleware3 の場合はRack::Builder#to_appは以下のようなネストしたRackアプリケーションになります。 Middleware1( app: Middleware2( app: OmniAuth::Strategies::Twitter( app: OmniAuth::Strategies::Facebook( app: Middleware3( app: Application ) ) ) ) ) OmniAuth::Strategy OmniAuthのストラテジクラスは OmniAuth::Strategyをインクルードする必要があります。

OmniAuth::Strategyのcallメソッドは以下のようにパスやHTTPメソッドを見て各処理にdispatchされます。 module OmniAuth module Strategy # snip ... def call(env) dup.call!(env) end

def call!(env) # rubocop:disable CyclomaticComplexity, PerceivedComplexity
  unless env['rack.session']
    error = OmniAuth::NoSessionError.new('You must provide a session to use OmniAuth.')
    raise(error)
  end

  @env = env
  @env['omniauth.strategy'] = self if on_auth_path?

  return mock_call!(env) if OmniAuth.config.test_mode
  return options_call if on_auth_path?  options_request?
  return request_call if on_request_path?  OmniAuth.config.allowed_request_methods.include?(request.request_method.downcase.to_sym)
  return callback_call if on_callback_path?
  return other_phase if respond_to?(:other_phase)
  @app.call(env)
end
# snip...

OAuth2.0やOpenID Connectによる認証の場合は、Authorization Requestのrequest_callメソッドとコールバックURLでToken Requestをするためのcallback_callメソッドが呼び出されます。

request_pathとcallback_pathは以下のようなコードで生成されます。options[:request_path]やoptions[:callback_path]に値が入っていなければ "#{path_prefix}/#{name}" がrequest_path、"#{path_prefix}/#{name}/callback" が callback_pathになります。path_prefixはデフォで/authになります。これがomniauthの /auth/:provider や /auth/:provider/callbackのルーティングの正体です。 module OmniAuth module Strategy # snip... def request_path @request_path ||= options[:request_path].is_a?(String) ? options[:request_path] : "#{path_prefix}/#{name}" end

def callback_path
  @callback_path ||= begin
    path = options[:callback_path] if options[:callback_path].is_a?(String)
    path ||= current_path if options[:callback_path].respond_to?(:call)  options[:callback_path].call(env)
    path ||= custom_path(:request_path)
    path ||= "#{path_prefix}/#{name}/callback"
    path
  end
end
# snip...

request_callやcallback_callではそれぞれrequest_phase、callback_phaseメソッドを呼び出します。この{request,callback}_phaseメソッドに各プロバイダ固有の処理を入れます。 # Performs the steps necessary to run the request phase of a strategy. def request_call # rubocop:disable CyclomaticComplexity, MethodLength, PerceivedComplexity setup_phase log :info, 'Request phase initiated.' # store query params from the request url, extracted in the callback_phase session['omniauth.params'] = request.GET OmniAuth.config.before_request_phase.call(env) if OmniAuth.config.before_request_phase if options.form.respond_to?(:call) log :info, 'Rendering form from supplied Rack endpoint.' options.form.call(env) elsif options.form log :info, 'Rendering form from underlying application.' call_app! else if request.params['origin'] env['rack.session']['omniauth.origin'] = request.params['origin'] elsif env['HTTP_REFERER'] !env['HTTP_REFERER'].match(/#{request_path}$/) env['rack.session']['omniauth.origin'] = env['HTTP_REFERER'] end request_phase end end

# Performs the steps necessary to run the callback phase of a strategy.
def callback_call
  setup_phase
  log :info, 'Callback phase initiated.'
  @env['omniauth.origin'] = session.delete('omniauth.origin')
  @env['omniauth.origin'] = nil if env['omniauth.origin'] == ''
  @env['omniauth.params'] = session.delete('omniauth.params') || {}
  OmniAuth.config.before_callback_phase.call(@env) if OmniAuth.config.before_callback_phase
  callback_phase
end

例えば、omniauth-oauth2ではrequest_phaseでAuthorization Request用のURLを発行してリダイレクト処理をしています。 def request_phase redirect client.auth_code.authorize_url({:redirect_uri = callback_url}.merge(authorize_params)) end

callback_phaseはStrategyに既にメソッドが定義されているので、callback側で特別な処理を入れる場合はsuperを呼び出します。

omniauth-oauth2だと以下のようなコードになっており、エラーハンドリングやstateのチェックをしつつ、Authorization CodeからToken Requestによってアクセストークンを取得しています。 def callback_phase # rubocop:disable AbcSize, CyclomaticComplexity, MethodLength, PerceivedComplexity error = request.params["error_reason"] || request.params["error"] if error fail!(error, CallbackError.new(request.params["error"], request.params["error_description"] || request.params["error_reason"], request.params["error_uri"])) elsif !options.provider_ignores_state (request.params["state"].to_s.empty? || request.params["state"] != session.delete("omniauth.state")) fail!(:csrf_detected, CallbackError.new(:csrf_detected, "CSRF detected")) else self.access_token = build_access_token self.access_token = access_token.refresh! if access_token.expired? super end rescue ::OAuth2::Error, CallbackError = e fail!(:invalid_credentials, e) rescue ::Timeout::Error, ::Errno::ETIMEDOUT = e fail!(:timeout, e) rescue ::SocketError = e fail!(:failed_to_connect, e) end このようにRackミドルウェアを使って、特定のパスへのリクエストをフックしてリダイレクトを噛ませたり、あるいは外部サービスとHTTPリクエストを送ってトークンやユーザ情報をenvに入れてコントローラ側でrequest.envで各データを取得できるようにしています。]]>

このエントリーをはてなブックマークに追加