warden のコードリーディング
warden gem のコードリーディングしたのでメモ
warden とは
rack ベースの認証フレームワーク。 devise でお馴染みの gem ですね。
rack middleware の登録
warden は rack middleware として動作します。 rails とかだと config/application.rb
あたりで以下のように rack middleware に登録することで warden を利用することができます。
# https://github.com/wardencommunity/warden/wiki/Setup module Hoge class Application < Rails::Application config.use Warden::Manager do |manager| manager.default_strategies :password manager.failure_app = BadAuthenticationEndsUpHere end end end
また、以下のように認証で利用する strategy の実態(認証処理)を登録してあげる必要があります。
Warden::Strategies.add
で実態を登録した場合、Warden::Strategies::Base
のサブクラスとして実態が定義されます。
# https://github.com/wardencommunity/warden/wiki/Strategies Warden::Strategies.add(:password) do def valid? params['username'] || params['password'] end def authenticate! u = User.authenticate(params['username'], params['password']) u.nil? ? fail!("Could not log in") : success!(u) end end
ブロック内で行なっている処理を理解するためにまずは Warden::Manager を読んでいきます
Warden::Manager の initializer は以下のようになっています。
# https://github.com/wardencommunity/warden/blob/master/lib/warden/manager.rb#L19-L25 def initialize(app, options={}) default_strategies = options.delete(:default_strategies) @app, @config = app, Warden::Config.new(options) @config.default_strategies(*default_strategies) if default_strategies yield @config if block_given? end
Warden::Config
のインスタンスを生成し @config
に代入。
ブロックが与えられていれば @config
を引数にブロックを実行しています。
middleware 登録の時のブロック引数 manager
の正体は Warden::Config
のインスタンスでした。
続いて ブロック内でやっている manager.default_strategies
について理解するために Warden::Config を読みます。
# https://github.com/wardencommunity/warden/blob/master/lib/warden/config.rb#L63-L70 def default_strategies(*strategies) opts = Hash === strategies.last ? strategies.pop : {} hash = self[:default_strategies] scope = opts[:scope] || :_all hash[scope] = strategies.flatten unless strategies.empty? hash[scope] || hash[:_all] || [] end
引数 strategies を配列で受け取り、最後の要素が hash の場合は それを opts に代入しています。
続いて hash = self[:default_strategies]
でデフォルトのstrategyを取得しています。
hash = self[:default_strategies]
という記法はすこし奇妙に感じますが、Warden::Config が Hash Class のサブクラスだからこのように書けるのですね。
最後に opts
から scope を取得し、 hash[scope]
に引数 strategies を代入しています。
続いて manager.failure_app = BadAuthenticationEndsUpHere
の処理を見ていきます。
# https://github.com/wardencommunity/warden/blob/master/lib/warden/config.rb#L21-L35 def self.hash_accessor(*names) #:nodoc: names.each do |name| class_eval <<-METHOD, __FILE__, __LINE__ + 1 def #{name} self[:#{name}] end def #{name}=(value) self[:#{name}] = value end METHOD end end hash_accessor :failure_app, :default_scope, :intercept_401
hash_accessor により Warden::Config#failure_app=
が動的に定義されていて、self[:failure_app] に代入をしています。
ここまでで、 冒頭のようにmiddleware を登録すると Warden::Manager
の @config
は以下の内容になっていることがわかります
# 簡易化のためhash 形式かつ default_strategiesとfailure_app 以外のキーを除外しています { default_strategies: { _all: [:password] }, failure_app: BadAuthenticationEndsUpHere }
middleware の呼び出し
rack middleware は callメソッドによって呼び出されます。早速 Warden::Manager
の call メソッドを読んでみます。
# https://github.com/wardencommunity/warden/blob/master/lib/warden/manager.rb#L30-L48 def call(env) # :nodoc: return @app.call(env) if env['warden'] && env['warden'].manager != self env['warden'] = Proxy.new(env, self) result = catch(:warden) do env['warden'].on_request @app.call(env) end result ||= {} case result when Array handle_chain_result(result.first, result, env) when Hash process_unauthenticated(env, result) when Rack::Response handle_chain_result(result.status, result, env) end end
env['warden']
に Warden::Proxy
のインスタンスを代入しています。
なぜ env にいれているかというと、この rack middleware の外からenv['warden'] を参照したいからだと思われます。
catch(:warden)
で後続のブロック内の処理から大域脱出をできるようにしています。
ブロックの中を見ていきます。
env['warden'].on_request
では詳細省略しますが、warden では以下のようにon_requestコールバックを登録することができ、on_requestコールバックが登録されていればここで実行されます。
Warden::Manager.on_request do |proxy| ... end
コールバックを実行したら後続のmiddlewareおよび rack アプリケーションを実行しています。
認証処理
ここまでで、 Warden::Manager middlewareでは 認証方法(strategy)の指定や env['warden'] に Warden::Proxy のインスタンスを代入するなどの初期化処理をしていることがわかりました。
続いてrack アプリケーション内で実際に認証をする方法を見ていきます。
warden ではアプリケーションの任意の場所で以下のようにすることで認証を行うことができます。
env['warden'].authenticate!
env['warden'] は Warden::Proxy のインスタンスでしたね。では Warden::Proxy#authenticate!
を見ていきましょう
# https://github.com/wardencommunity/warden/blob/master/lib/warden/proxy.rb#L132-L136 def authenticate!(*args) user, opts = _perform_authentication(*args) throw(:warden, opts) unless user user end
_perform_authentication
を実行して戻り値 user が存在しなければ throw(:warden, opts)
で大域脱出。user が存在すれば そのまま user を返しています。
_perform_authentication
を見ましょう
# https://github.com/wardencommunity/warden/blob/master/lib/warden/proxy.rb#L328-L343 def _perform_authentication(*args) scope, opts = _retrieve_scope_and_opts(args) user = nil # Look for an existing user in the session for this scope. # If there was no user in the session, see if we can get one from the request. return user, opts if user = user(opts.merge(:scope => scope)) _run_strategies_for(scope, args) if winning_strategy && winning_strategy.successful? opts[:store] = opts.fetch(:store, winning_strategy.store?) set_user(winning_strategy.user, opts.merge!(:event => :authentication)) end [@users[scope], opts] end
_retrieve_scope_and_opts
は @config
から scope と そのscope に対応する optionを取り出しているようです。
middleware 登録時に何も指定していない場合は scope
は :default
それに対応するオプションは 空のhash になります。
続いて user メソッドを opts を引数に呼び出しています。
冒頭のようにmiddleware を登録した場合だと opts の中身は {scope: :default}
になります。
user メソッドも割愛しますが、@users に該当 scope のキャッシュがいればそれを返し、なければ session の取り出しを試みます。複数 authenticate!
が呼ばれてもパフォーマンスが劣化しないようにキャッシュしたり、一度認証が通ってsession が作成されればsessionが残っていれば再認証する必要がないのでこの処理を挟んでいるようです。
user メソッド を呼び出しても user が存在しなければ後続の _run_strategies_for(scope, args)
に続きます。
# https://github.com/wardencommunity/warden/blob/master/lib/warden/proxy.rb#L353-L376 def _run_strategies_for(scope, args) #:nodoc: self.winning_strategy = @winning_strategies[scope] return if winning_strategy && winning_strategy.halted? # Do not run any strategy if locked return if @locked if args.empty? defaults = @config[:default_strategies] strategies = defaults[scope] || defaults[:_all] end (strategies || args).each do |name| strategy = _fetch_strategy(name, scope) next unless strategy && !strategy.performed? && strategy.valid? catch(:warden) do _update_winning_strategy(strategy, scope) end strategy._run! _update_winning_strategy(strategy, scope) break if strategy.halted? end end
winning_strategy
は恐らく最後に呼び出したstrategyです。初回は nil にっているはずです。
args は env['warden'].authenticate!
の引数でしたね。
今回の場合 authenticate! には何も渡してないので args は空です。
argsが空の場合、@config からstrategies を取り出します
「 rack middleware の登録」の箇所に書いた通り@configは以下のようになっているので、今回の場合 strategies は [:password, :basic]
になります
{ default_strategies: { _all: [:password] }, failure_app: BadAuthenticationEndsUpHere }
続いて取得した strategies をループ回して _fetch_strategy(name, scope)
で strategyの実態の取り出し、
strategy._run!
でstrategyの実行を行なっています。
# https://github.com/wardencommunity/warden/blob/master/lib/warden/proxy.rb#L379-L387 _fetch_strategy(name, scope) def _fetch_strategy(name, scope) @strategies[scope][name] ||= if klass = Warden::Strategies[name] klass.new(@env, scope) elsif @config.silence_missing_strategies? nil else raise "Invalid strategy #{name}" end end
冒頭で登録した strategy の実態を取り出し、そのインスタンスが返されています。
strategy の実態が定義されていない場合は 例外が返されます。
strategy を取得できたら strategy._run!
を呼び出します。
strategy は Warden::Strategies::Base
のサブクラスとして定義されているので Warden::Strategies::Base#_run!
のを見ます。
# https://github.com/wardencommunity/warden/blob/master/lib/warden/strategies/base.rb#L52-L56 def _run! # :nodoc: @performed = true authenticate! self end
@performed
にして authenticate! を呼び出していますね。strategy の authenticate!
メソッドは 冒頭で strategy の実態を登録する際に定義した認証処理でしたね。
authenticate! では任意の認証処理を実行して 失敗した場合は fail!
成功した場合は success!(user)
を呼び出しています。
# https://github.com/wardencommunity/warden/blob/master/lib/warden/strategies/base.rb#L144-L147 def fail!(message = "Failed to Login") halt! @message = message @result = :failure end # https://github.com/wardencommunity/warden/blob/master/lib/warden/strategies/base.rb#L125-L130 def success!(user, message = nil) halt! @user = user @message = message @result = :success end
fail!
の場合は helt!
で @helted
を true にして @result
に :failure
を代入しています。
@helted
が true の場合だと後続のstrategy が実行されないので、後続のstrategy もトライする場合は fail!
ではなくfail
で呼び出す必要があります。
success!
は helt!
を実行し、 @user
に認証によって取得することができた user を代入しています。
strategy._run!
で認証処理を実行した後は_update_winning_strategy(strategy, scope)
でwinning_strategyを直前に実行したstrategyに更新します。その後strategy.helted?
を確認して true だと 認証が終了したとみなしループを抜けます。
_run_strategies_for
の処理を見たので _perform_authentication
に戻ります。
_run_strategies_for
が終了すると winning_strategy.successful? で直前に実行された認証が成功しているか確認します。
成功していれば set_user(winning_strategy.user, opts.merge!(:event => :authentication))
を実行します。
# https://github.com/wardencommunity/warden/blob/master/lib/warden/proxy.rb#L170-L194 def set_user(user, opts = {}) scope = (opts[:scope] ||= @config.default_scope) # Get the default options from the master configuration for the given scope opts = (@config[:scope_defaults][scope] || {}).merge(opts) opts[:event] ||= :set_user @users[scope] = user if opts[:store] != false && opts[:event] != :fetch options = env[ENV_SESSION_OPTIONS] if options if options.frozen? env[ENV_SESSION_OPTIONS] = options.merge(:renew => true).freeze else options[:renew] = true end end session_serializer.store(user, scope) end run_callbacks = opts.fetch(:run_callbacks, true) manager._run_callbacks(:after_set_user, user, self, opts) if run_callbacks @users[scope] end
set_user では @usersに認証で取得できたuser のデータをキャッシュし、sessionに書き込んでいます。
set_user
の処理が終わると [@users[scope], opts]
を返して _perform_authentication
の処理は終わりです。
_perform_authentication
が終わったので authenticate!
に戻ります。
だいぶ離れたのでコードをいかに再掲
def authenticate!(*args) user, opts = _perform_authentication(*args) throw(:warden, opts) unless user user end
_perform_authentication は strategy で定義した認証が成功したら user は認証によって取得されたデータ。失敗したら nilが返ってきました。
認証が成功した場合は そのまま user を返して終わりです。
認証が失敗した場合は、認証が失敗した場合は throw(:warden, opts)
で大域脱出をはかり、
Warden::Manager#call まで処理が戻ります。以下再掲
def call(env) # :nodoc: return @app.call(env) if env['warden'] && env['warden'].manager != self env['warden'] = Proxy.new(env, self) result = catch(:warden) do env['warden'].on_request @app.call(env) end result ||= {} case result when Array handle_chain_result(result.first, result, env) when Hash process_unauthenticated(env, result) when Rack::Response handle_chain_result(result.status, result, env) end end
認証に成功した場合は result は@app.call(env)
の戻り値になります。rack middleware の場合は基本的に Array になります。
認証に失敗した場合は throw(:warden, opts)
の opts がresult に入ります。opts は Hash です。
認証が正常に終了した場合は result は Array のはずなので handle_chain_result を実行します。 handle_chain_result は基本的に result をそのまま返しています。
認証が失敗した場合は result は Hash なので process_unauthenticated(env, result)
を実行します。
process_unauthenticated
では 基本的に 冒頭で登録した failure_app の call メソッドを呼び出します。
failure_app をカスタマイズすることで、認証失敗時の処理を細かくカスタマイズすることができます。
終わりに
長くなったかつ最後の方はちょっと力尽きた感ありましたが、warden のコードを読む機会があったのでメモしておきます