RailsでSNSログインを実装するときに作成する設定ファイルは一体何をやっているのか
RailsでSNSログインを実装する場合、 config/initializers
あたりに以下のような設定ファイルを追加すると思います。
Rails.application.config.middleware.use OmniAuth::Builder do
provider :azure_activedirectory_v2,
client_id: ENV["AZURE_AD_CLIENT_ID"],
client_secret: ENV["AZURE_AD_CLIENT_SECRET"],
tenant_id: ENV["AZURE_AD_TENANT_ID"]
end
しかし正直なところ、このコードを見ただけでは「おまじない」感が拭えないばかりか、
例えばネットで rails twitter ログイン
とか検索しても、だいたい
- gem入れる
- ↑の設定ファイルを追加する
/auth/twitter
にリンクを貼る
くらいしか書いていなくて、小手先だけの対応をしている感じがしてとても気持ち悪いです。
しかしお恥ずかしい話ですが、正直僕は今まで調べるの面倒で小手先対応していました。ダメなエンジニアですね。とても反省しています。
そんなこんなで小手先の対応を続けていたところ、最近Azure Active Directoryの認証をRailsアプリに組み込んだ時に、どうにもうまく動作させることができずバチが当たってしまったので
- ↑の設定ファイルは何をやっているのか?
- なぜroutesに定義していないにもかかわらず
/auth/:provider
にリンク貼ったりするだけで認証画面へ飛ばすことができるのか?
あたりをちゃんと調べたので記事に残しておこうと思います。
設定ファイルは何をやっているのか?
「なぜ設定ファイルを追加することでmiddlewareとして登録され、リクエスト時にSNS認証が有効になるのか?」
ということが知りたいので、どうやってそれが実現されていくのかを、起点となる設定ファイルから順番にRails内部へ侵入し見ていく。
まず、そもそも Rails.application.config.middleware.use
こいつがなんなのかだが、ここのメソッドを呼んでいる。
def use(klass, *args, &block)
middlewares.push(build_middleware(klass, args, block))
end
やっていることはとても簡単で、 Middleware
クラスのインスタンスを作成して、アクセサを使って middlewares
にpushしているだけ。
Railsコンソールなどで、 Rails.application.config.middleware
をのぞいて見ると ActionDispatch::MiddlewareStack
のインスタンスが取得でき、 middlewares
にはRailsガイドでもおなじみのRack Middlewareがずらずらと並んでいることが確認できます。
今回の設定ファイルでは
- klassは
OmniAuth::Builder
- argsは空の配列
block_given?
はtrueを返す
という感じになっている
登録されたmiddlewareはその後、Rails engineのappの中のbuildが呼ばれ
def app
@app || @app_build_lock.synchronize {
@app ||= begin
stack = default_middleware_stack
config.middleware = build_middleware.merge_into(stack)
config.middleware.build(endpoint) # <- ここ
end
}
end
そのbuildはActionDispatch::MiddlewareStackのbuildなのでここが呼ばれ、ようやく先ほど登録した Middleware
のインスタンスの build
が呼ばれる
def build(app = nil, &block)
instrumenting = ActiveSupport::Notifications.notifier.listening?(InstrumentationProxy::EVENT_NAME)
middlewares.freeze.reverse.inject(app || block) do |a, e|
if instrumenting
e.build_instrumented(a)
else
e.build(a) # <- ここ
end
end
end
Middleware
クラスのインスタンスの buildは klass
のインスタンス作成を行っているので、いよいよ OmniAuth::Builder
のインスタンスの作成まで辿り着く
def build(app)
klass.new(app, *args, &block) # <- klassはOmniAuth::Builderのこと
end
OmniAuth::Builder
は Rack::Builder
を継承しているので、 Rack::Builder
の initialize が最初に実行される
Rack::Builder
の initialize
では instance_eval
を使って、受け取ったブロックを実行している。
このブロックこそ、我々が config/initializers
に追加した設定ファイルの provider
メソッドを含む do〜end
の部分である。
def initialize(default_app = nil, &block)
@use, @map, @run, @warmup, @freeze_app = [], nil, default_app, nil, false
instance_eval(&block) if block_given?
end
さて、上述したとおり instance_eval
を呼び出されているレシーバは、ここでは OmniAuth::Builder
のインスタンスなので、
ブロックにある provider
メソッドはここに定義されているメソッドが呼び出されているということになる。
klass
は :azure_activedirectory_v2
、 args
は環境変数のハッシュ、 block
は nil
で呼び出されている。
def provider(klass, *args, &block)
if klass.is_a?(Class)
middleware = klass
else
begin
middleware = OmniAuth::Strategies.const_get("#{OmniAuth::Utils.camelize(klass.to_s)}")
rescue NameError
raise(LoadError.new("Could not find matching strategy for #{klass.inspect}. You may need to install an additional gem (such as omniauth-#{klass})."))
end
end
args.last.is_a?(Hash) ? args.push(options.merge(args.pop)) : args.push(options)
use middleware, *args, &block
end
klass
は Class
ではないので、ここ↓で Omniauth
のストラテジークラスが取得される
middleware = OmniAuth::Strategies.const_get("#{OmniAuth::Utils.camelize(klass.to_s)}")
ここで取得されるストラテジークラスのためにgemを入れているのである!
(ここではこのgemのクラスが取得される)
そして最後にここ↓ でRackの use
メソッドにより Rack Middleware
として登録されるので、この時点からリクエスト時にSNS認証が有効になる
use middleware, *args, &block
なるほど〜
次回へ続く
ちょっと長くなりそうなので、
なぜroutesに定義していないにもかかわらず
/auth/:provider にリンク貼ったりするだけで認証画面へ飛ばすことができるのか?
は次回に続きます