2021-01-24

RailsでSNSログインを実装するときに/auth/:providerで認証ページへ飛ぶのはなぜか

さて、前回設定ファイルを追加することで、middlewareとして有効になるというところまで理解できたので、今回は /auth/:provider で認証ページへ飛ぶ謎を解き明かしていきます。
前回みた通り「 Rack Middleware に登録される」という時点で、Railsに到達する前にリダイレクトされているということはソースコードを見に行かなくても予想はできることですが、ちゃんと実際に確認してみることはとてもいいことなのでちゃんと見に行く。

まず、 Rack Middleware の話題が出ているのにRackについての説明が全くないとさすがに意味不明すぎるので、最初に簡単にRackについて調べた。

Rackとは

Rackの実態は、超簡単に言うと

  1. ステータスコード
  2. レスポンスヘッダのハッシュ
  3. レスポンスボディの配列

をまとめた配列を返すようになっているcallメソッドが定義されたクラスのこと(具体的には [ステータスコード, { ヘッダ }, [ ボディ ]] が返る)
webサーバーとRuby製webフレームワークとやりとりするための統一APIを提供している(規約、取り決めを提供している)

リクエスト時の全体の流れとしては

  1. リクエストが来たらwebサーバーはenvハッシュと呼ばれるハッシュを1つ用意する(envハッシュにはHTTPリクエストの情報が全て含まれている)
  2. webサーバーはenvハッシュを引数として、アプリやフレームワークのcallメソッドを呼び出す(これがRack)
  3. このcallメソッドが行う仕事は、envハッシュ内の情報に基づいてリクエストを処理することと、3つの要素を含む配列を1つ返すこと(この配列が↑に書いた通りステータスコードなどを含んでいる)

Rack Middleware は以下のようにして連結して使えるようになっている(HogeやFugaはRack App)

use Hoge
use Fuga
run App

なのでたまねぎのような構造になっていて

requestが来る => 一番外側のrack appが処理する => 二番目に外側のrack appが処理する => 一番内側のrack appが処理する => Railsとかに渡す => 一番内側のrack appがresponsを受け取る => 一番外側のrack appまでバケツリレーする

のような感じで処理が進む
まとめるとWebアプリケーションは、リクエストが来たときに一番外側のミドルウェアから順番にcallメソッドが呼ばれていき、一番内側ののRackアプリケーションまで一気に到達してレスポンスを返すようになっているということ

以上を踏まえて認証画面へ飛ばされるコードを追っていく

なぜroutesに定義していないにもかかわらず /auth/:provider にリンク貼ったりするだけで認証画面へ飛ばすことができるのか?

前回の記事で、 config/initializers で書いた設定の通り Rack Middleware に登録が行われます。
上述した通り Rack Middleware は数珠繋ぎにcallメソッドが呼ばれていく構造なので、登録しようとしている認証のgemのcallが呼ばれる。

前回に引き続きAzureのgemを例に見ていくと、Azure gemそのものにはcallメソッドは実装されていないので、継承先であるStrategyのcallが呼ばれ、同call!が呼ばれる。という流れになる。
(厳密には継承先の OmniAuth::Strategies::OAuth2 でincludeされている OmniAuth::Strategy に定義されているcallが呼ばれている。)

def call!(env) # rubocop:disable CyclomaticComplexity, PerceivedComplexity
  unless env['rack.session']
    error = OmniAuth::NoSessionError.new('You must provide a session to use OmniAuth.')
    fail(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

今回の目当てである認証画面へリダイレクトされる箇所は、ここにある。

return request_call if on_request_path? && OmniAuth.config.allowed_request_methods.include?(request.request_method.downcase.to_sym)

on_request_path? メソッドでは、オプションとして渡された request_path がcallに応答できればcallを呼び、そうでなければ現在のパスがリクエストパスと一致しているかを確認している。
リクエストパスはここに定義されている通り、オプションとして渡したものか、(何も指定していなければ) /auth/:provider のパスになる。

def request_path
  @request_path ||= options[:request_path].is_a?(String) ? options[:request_path] : "#{path_prefix}/#{name}"
end

これが /auth/:provider でリダイレクトされる処理の正体。

また、OmniAuth.config.allowed_request_methods.include?(request.request_method.downcase.to_sym) ではリクエストのHTTPメソッドが、許可されたHTTPメソッドに含まれているのかを確認している。

omniauth2.0 からは基本的にPOSTだけを許可しているので、 link_to 〜, method: :post とするか、 form_with でpostするしかない。
ただし、postするということはRailsのCSRFtokenを付与しなければ OmniAuth::AuthenticityError となってしまうので、↑のリリースノートでも言及されているomniauth-rails_csrf_protection gemを入れればOKなようです。