Devise-4.3.0の処理の流れ

ちょっと調べるたびに毎回処理の流れを追っていくのは大変なのでメモっておく。

## 対象

- devise-4.3.0
- warden-1.2.7

## Warden::Managerの初期化

`Devise::Rails`で初期化。`Warden::Manager`の初期化時に`Devise.warden_config`を設定する

```ruby:devise/blob/v4.3.0/lib/devise/rails.rb
module Devise
  class Engine < ::Rails::Engine
    # Initialize Warden and copy its configurations.
    config.app_middleware.use Warden::Manager do |config|
      Devise.warden_config = config
    end
  end
end
```

`Warden::Manager#initialize`にブロックを渡すと`Warden::Config`のインスタンスを引数に渡して実行してくれる。

```ruby:warden/blob/v1.2.7/lib/warden/manager.rb
module Warden
  class Manager
    # Initialize the middleware. If a block is given, a Warden::Config is yielded so you can properly
    # configure the Warden::Manager.
    # :api: public
    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?
      self
    end
  end
end
```

## ORMの設定(`Devise::Models#devise`)

`config/initializers/devise.rb`で各種ORM向けのdeviseの設定を読み込む

```ruby:devise/blob/v4.3.0/lib/generators/templates/devise.rb
Devise.setup do |config|
  # ==> ORM configuration
  # Load and configure the ORM. Supports :active_record (default) and
  # :mongoid (bson_ext recommended) by default. Other ORMs may be
  # available as additional gems.
  require 'devise/orm/<%= options[:orm] %>'
end
```

ActiveRecordであればこんな感じ。

```ruby:devise/blob/v4.3.0/lib/devise/orm/active_record.rb
require 'orm_adapter/adapters/active_record'

ActiveSupport.on_load(:active_record) do
  extend Devise::Models
end
```

`Devise::Models#devise`でDeviseでが利用するためのMappingの設定を作成する

```ruby:devise/blob/v4.3.0/lib/devise/models.rb
module Devise
  module Models
    # Include the chosen devise modules in your model:
    #
    #   devise :database_authenticatable, :confirmable, :recoverable
    #
    # You can also give any of the devise configuration values in form of a hash,
    # with specific values for this model. Please check your Devise initializer
    # for a complete description on those values.
    #
    def devise(*modules)
      options = modules.extract_options!.dup

      selected_modules = modules.map(&:to_sym).uniq.sort_by do |s|
        Devise::ALL.index(s) || -1  # follow Devise::ALL order
      end

      devise_modules_hook! do
        include Devise::Models::Authenticatable

        selected_modules.each do |m|
          mod = Devise::Models.const_get(m.to_s.classify)

          if mod.const_defined?("ClassMethods")
            class_mod = mod.const_get("ClassMethods")
            extend class_mod

            if class_mod.respond_to?(:available_configs)
              available_configs = class_mod.available_configs
              available_configs.each do |config|
                next unless options.key?(config)
                send(:"#{config}=", options.delete(config))
              end
            end
          end

          include mod
        end

        self.devise_modules |= selected_modules
        options.each { |key, value| send(:"#{key}=", value) }
      end
    end
  end
end
```

## Mappingの設定

Deviseは`ActionDispatch::Routing::Mapper`を拡張して`devise_for`を定義している。

```ruby:devise/blob/v4.3.0/lib/devise/rails/routes.rb
module ActionDispatch::Routing
  class Mapper
    def devise_for(*resources)
      @devise_finalized = false
      raise_no_secret_key unless Devise.secret_key
      options = resources.extract_options!

      options[:as]          ||= @scope[:as]     if @scope[:as].present?
      options[:module]      ||= @scope[:module] if @scope[:module].present?
      options[:path_prefix] ||= @scope[:path]   if @scope[:path].present?
      options[:path_names]    = (@scope[:path_names] || {}).merge(options[:path_names] || {})
      options[:constraints]   = (@scope[:constraints] || {}).merge(options[:constraints] || {})
      options[:defaults]      = (@scope[:defaults] || {}).merge(options[:defaults] || {})
      options[:options]       = @scope[:options] || {}
      options[:options][:format] = false if options[:format] == false

      resources.map!(&:to_sym)

      resources.each do |resource|
        mapping = Devise.add_mapping(resource, options)

        begin
          raise_no_devise_method_error!(mapping.class_name) unless mapping.to.respond_to?(:devise)
        rescue NameError => e
          raise unless mapping.class_name == resource.to_s.classify
          warn "[WARNING] You provided devise_for #{resource.inspect} but there is " \
            "no model #{mapping.class_name} defined in your application"
          next
        rescue NoMethodError => e
          raise unless e.message.include?("undefined method `devise'")
          raise_no_devise_method_error!(mapping.class_name)
        end

        if options[:controllers] && options[:controllers][:omniauth_callbacks]
          unless mapping.omniauthable?
            raise ArgumentError, "Mapping omniauth_callbacks on a resource that is not omniauthable\n" \
              "Please add `devise :omniauthable` to the `#{mapping.class_name}` model"
          end
        end

        routes = mapping.used_routes

        devise_scope mapping.name do
          with_devise_exclusive_scope mapping.fullpath, mapping.name, options do
            routes.each { |mod| send("devise_#{mod}", mapping, mapping.controllers) }
          end
        end
      end
    end
  end
end
```

`mapping = Devise.add_mapping(resource, options)`のresourceは`:user`みたいな値。

```ruby:devise/blob/v4.3.0/lib/devise.rb
module Devise
  # Small method that adds a mapping to Devise.
  def self.add_mapping(resource, options)
    mapping = Devise::Mapping.new(resource, options)
    @@mappings[mapping.name] = mapping
    @@default_scope ||= mapping.name
    @@helpers.each { |h| h.define_helpers(mapping) }
    mapping
  end
end
```

## DeviseのMappingの設定をWarden::Configに設定

Railsの`ActionDispatch::Routing::RouteSet#finalize!`のタイミングで設定する。

```ruby:devise/blob/v4.3.0/lib/devise/rails/routes.rb
module Devise
  module RouteSet
    def finalize!
      result = super
      @devise_finalized ||= begin
        # 省略

        Devise.configure_warden!
        Devise.regenerate_helpers!
        true
      end
      result
    end
  end
end

module ActionDispatch::Routing
  class RouteSet #:nodoc:
    # Ensure Devise modules are included only after loading routes, because we
    # need devise_for mappings already declared to create filters and helpers.
    prepend Devise::RouteSet
  end
end
```

`Devise.mappings`の内容を元に`Warden::Config`の`scope_defaults`, `serialize_into_session`, `serialize_from_session`の設定を行う。

```ruby:devise/blob/v4.3.0/lib/devise.rb
module Devise
  # A method used internally to complete the setup of warden manager after routes are loaded.
  # See lib/devise/rails/routes.rb - ActionDispatch::Routing::RouteSet#finalize_with_devise!
  def self.configure_warden! #:nodoc:
    @@warden_configured ||= begin
      warden_config.failure_app   = Devise::Delegator.new
      warden_config.default_scope = Devise.default_scope
      warden_config.intercept_401 = false

      Devise.mappings.each_value do |mapping|
        warden_config.scope_defaults mapping.name, strategies: mapping.strategies

        warden_config.serialize_into_session(mapping.name) do |record|
          mapping.to.serialize_into_session(record)
        end

        warden_config.serialize_from_session(mapping.name) do |args|
          mapping.to.serialize_from_session(*args)
        end
      end

      @@warden_config_blocks.map { |block| block.call Devise.warden_config }
      true
    end
  end
end
```

## `Devise::Mapping#strategies`(=`Warden::Strategies`に登録されているstrategies)

`Devise::Mapping#strategies`の定義

```ruby:devise/blob/v4.3.0/lib/devise/mapping.rb
module Devise
  class Mapping #:nodoc:
    def strategies
      @strategies ||= STRATEGIES.values_at(*self.modules).compact.uniq.reverse
    end
  end
end
```

`self.modules`は`Devise::Models#devise`に渡した引数のこと(`:user`から`User`にして有効にしたモジュールを探し出している)。`Devise::STRATEGIES`は`Devise.add_modules`で更新される値。

```ruby:devise/blob/v4.3.0/lib/devise.rb
module Devise
  def self.add_module(module_name, options = {})
    options.assert_valid_keys(:strategy, :model, :controller, :route, :no_input, :insert_at)

    ALL.insert (options[:insert_at] || -1), module_name

    if strategy = options[:strategy]
      strategy = (strategy == true ? module_name : strategy)
      STRATEGIES[module_name] = strategy
    end

    if controller = options[:controller]
      controller = (controller == true ? module_name : controller)
      CONTROLLERS[module_name] = controller
    end

    NO_INPUT << strategy if options[:no_input]

    if route = options[:route]
      case route
      when TrueClass
        key, value = module_name, []
      when Symbol
        key, value = route, []
      when Hash
        key, value = route.keys.first, route.values.flatten
      else
        raise ArgumentError, ":route should be true, a Symbol or a Hash"
      end

      URL_HELPERS[key] ||= []
      URL_HELPERS[key].concat(value)
      URL_HELPERS[key].uniq!

      ROUTES[module_name] = key
    end

    if options[:model]
      path = (options[:model] == true ? "devise/models/#{module_name}" : options[:model])
      camelized = ActiveSupport::Inflector.camelize(module_name.to_s)
      Devise::Models.send(:autoload, camelized.to_sym, path)
    end

    Devise::Mapping.add_module module_name
  end
end
```

`devise/modules.rb`(これは`devise.rb`のなかでrequireされる)で組み立てている。

```ruby:devise/blob/v4.3.0/lib/devise/modules.rb
require 'active_support/core_ext/object/with_options'

Devise.with_options model: true do |d|
  # Strategies first
  d.with_options strategy: true do |s|
    routes = [nil, :new, :destroy]
    s.add_module :database_authenticatable, controller: :sessions, route: { session: routes }
    s.add_module :rememberable, no_input: true
  end

  # Other authentications
  d.add_module :omniauthable, controller: :omniauth_callbacks,  route: :omniauth_callback

  # 省略
end
```

もう一度`Devise::Mapping#strategies`の定義。

```ruby:devise/blob/v4.3.0/lib/devise/mapping.rb
module Devise
  class Mapping #:nodoc:
    def strategies
      @strategies ||= STRATEGIES.values_at(*self.modules).compact.uniq.reverse
    end
  end
end
```

`STRATEGIES=[:database_authenticatable, :remenberable]`だが`revierse`してるので必ず`[:remenberable, :database_authenticatable]`の順になる。
`Warden::Proxy`で認証で`:rememberable`より前に`:database_authenticatable`が実行されることはない。

## Deviseでの認証とログイン

`warden`は`Warden::Proxy`のインスタンス。

```ruby:devise/blob/v4.3.0/lib/devise/controllers/helpers.rb
module Devise
  module Controllers
    # Those helpers are convenience methods added to ApplicationController.
    module Helpers
      # The main accessor for the warden proxy instance
      def warden
        request.env['warden'] or raise MissingWarden
      end
    end
  end
end
```

認証には`warden.authenticate!`を利用する

```ruby:devise/blob/v4.3.0/lib/devise/controllers/helpers.rb
module Devise
  module Controllers
    # Those helpers are convenience methods added to ApplicationController.
    module Helpers
      module ClassMethods
        def devise_group(group_name, opts={})
          mappings = "[#{ opts[:contains].map { |m| ":#{m}" }.join(',') }]"

          class_eval <<-METHODS, __FILE__, __LINE__ + 1
            def authenticate_#{group_name}!(favourite=nil, opts={})
              unless #{group_name}_signed_in?
                mappings = #{mappings}
                mappings.unshift mappings.delete(favourite.to_sym) if favourite
                mappings.each do |mapping|
                  opts[:scope] = mapping
                  warden.authenticate!(opts) if !devise_controller? || opts.delete(:force)
                end
              end
            end

            # 省略
          METHODS
        end
      end
    end
  end
end
```

`@config`は`Warden::Config`。`@config[:default_strategies][scope]`でDeviseが設定したStrategiesが取得できる。

```ruby:warden/blob/v1.2.7/lib/warden/proxy.rb
module Warden
  class Proxy
    def authenticate(*args)
      user, _opts = _perform_authentication(*args)
      user
    end

    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

    # Run the strategies for a given scope
    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?

        self.winning_strategy = @winning_strategies[scope] = strategy
        strategy._run!
        break if strategy.halted?
      end
    end
  end
end
```

ログインには`warden.authenticate`と`warden.set_user`を利用する。

```ruby:devise/blob/v4.3.0/app/controllers/devise/sessions_controller.rb
class Devise::SessionsController < DeviseController
  # POST /resource/sign_in
  def create
    self.resource = warden.authenticate!(auth_options)
    set_flash_message!(:notice, :signed_in)
    sign_in(resource_name, resource)
    yield resource if block_given?
    respond_with resource, location: after_sign_in_path_for(resource)
  end
end
```

```ruby:devise/blob/v4.3.0/lib/devise/controllers/sign_in_out.rb
module Devise
  module Controllers
    # Provide sign in and sign out functionality.
    # Included by default in all controllers.
    module SignInOut
      def sign_in(resource_or_scope, *args)
        options  = args.extract_options!
        scope    = Devise::Mapping.find_scope!(resource_or_scope)
        resource = args.last || resource_or_scope

        expire_data_after_sign_in!

        if options[:bypass]
          ActiveSupport::Deprecation.warn(<<-DEPRECATION.strip_heredoc, caller)
          [Devise] bypass option is deprecated and it will be removed in future version of Devise.
          Please use bypass_sign_in method instead.
          Example:

            bypass_sign_in(user)
          DEPRECATION
          warden.session_serializer.store(resource, scope)
        elsif warden.user(scope) == resource && !options.delete(:force)
          # Do nothing. User already signed in and we are not forcing it.
          true
        else
          warden.set_user(resource, options.merge!(scope: scope))
        end
      end
    end
  end
end
```

```ruby:warden/blob/v1.2.7/lib/warden/proxy.rb
module Warden
  class Proxy
    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]
        options[:renew] = true if options
        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
  end
end
```

## `lib/devise/hooks`以下のファイル

`Wardan::Hooks`に定義してある拡張ポイントに対してメソッドを登録している。コールバックの呼び出しは`Warden::Proxy`の各種操作（`authenticate!`とか`set_user`とか）の実行のタイミングで呼び出されている。