RailsでSNSログインを実装するときに/auth/:providerで認証ページへ飛ぶのはなぜか
さて、前回設定ファイルを追加することで、middlewareとして有効になるというところまで理解できたので、今回は /auth/:provider
で認証ページへ飛ぶ謎を解き明かしていきます。
前回みた通り「 Rack Middleware
に登録される」という時点で、Railsに到達する前にリダイレクトされているということはソースコードを見に行かなくても予想はできることですが、ちゃんと実際に確認してみることはとてもいいことなのでちゃんと見に行く。
まず、 Rack Middleware
の話題が出ているのにRackについての説明が全くないとさすがに意味不明すぎるので、最初に簡単にRackについて調べた。
Rackとは
Rackの実態は、超簡単に言うと
- ステータスコード
- レスポンスヘッダのハッシュ
- レスポンスボディの配列
をまとめた配列を返すようになっているcallメソッドが定義されたクラスのこと(具体的には [ステータスコード, { ヘッダ }, [ ボディ ]]
が返る)
webサーバーとRuby製webフレームワークとやりとりするための統一APIを提供している(規約、取り決めを提供している)
リクエスト時の全体の流れとしては
- リクエストが来たらwebサーバーはenvハッシュと呼ばれるハッシュを1つ用意する(envハッシュにはHTTPリクエストの情報が全て含まれている)
- webサーバーはenvハッシュを引数として、アプリやフレームワークのcallメソッドを呼び出す(これがRack)
- この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なようです。