devise ストロングパラメータの仕組みを調べてみた。

##はじめに
サインアップページにdeviseのデフォルトで許可されている要素ではない、"username"を追加することになり、deviseでのストロングパラメータの設定方法を調べた時のことを備忘録として投稿します。

##ストロングパラメータとは
**特定のパラメーターのみを許可することで、モデルへの不正なパラメータの流入を防ぐ仕組みのこと。**MassAssingment脆弱性というセキュリティ上の問題に対処するための仕組みとしてRails4系から導入されましたものです。

deviseにおいては、モデルにパラメータを送ることのできるアクションは、３つに限定されており、パラーメータも以下のように限定されています。

|action| allowed params|
|:-:|:-:|
|sign_in  | authentication keys |
|sign_up  | authentication keys, password,password_confirmation  |
|account_update| authentication keys,password, password_confirmation,current_password  | 

参考：[Strong Parameters](https://github.com/heartcombo/devise#strong-parameters)
##サインアップ時のストロングパラメータの設定
サインアップ時にデフォルトで許可されているパラメータは、以下の３つです。

1. authentication keys(認証キー)
1. password(パスワード)
1. password_confirmation(パスワード確認用)

※authentication keys(認証キー)のデフォルト値は、emailです。

~~~ruby:config/initializers/divise.rb
# ==> Configuration for any authentication mechanism
  # Configure which keys are used when authenticating a user. The default is
  # just :email. 
  ....中略.....
  # config.authentication_keys = [:email]

~~~
これら以外を許可したい場合は、ApplicationControllerに設定する必要があります。以下は、サインアップアクションでusernameの値をパラメータとして許可する設定です。

~~~ruby:application_controller.rb
class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:username])
  end
end
~~~
これで一見落着！

と思ったのですが、一つ気になることがありました。

##ログイン時のストロングパラメータの設定
何が気になったのかというと、ログイン（sign_in
)アクションの許可されているパラメータについてです。もう一度見てみます。

|action| allowed params|
|:-:|:-:|
|sign_in  | authentication keys |
参考：[Strong Parameters](https://github.com/heartcombo/devise#strong-parameters)
あれ？？authentication keysだけなの？？？
先述した通り、authentication keysのデフォルト値は、:emailです。つまり、許可されたパラメータは、:emailのみということでしょうか。
しかし、ログイン認証時には、サインアップの際に登録したパスワードと、ログインフォームから送られてきたパスワードを照合して、ログイン認証を行います。とすると、passwordも許可されているはずです。
このモヤモヤを解消するべく、deviseのストロングパラメータもう少し詳しく確認しました。

##1.configure_permitted_parametersメソッドの確認

~~~ruby:application_controller.rb
def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:username])
  end
~~~

###1. devise_parameter_sanitizer

~~~ruby:devise/lib/devise/controllers/helpers.rb 
# Set up a param sanitizer to filter parameters using strong_parameters. See
      # lib/devise/parameter_sanitizer.rb for more info. Override this
      # method in your application controller to use your own parameter sanitizer.
      def devise_parameter_sanitizer
        @devise_parameter_sanitizer ||= Devise::ParameterSanitizer.new(resource_class, resource_name, params)
      end
~~~

> Set up a param sanitizer to filter parameters using strong_parameters. See lib/devise/parameter_sanitizer.rb for more info. Override this method in your application controller to use your own parameter sanitizer.
（ストロングパラメータを使用してパラメータをフィルタリングするための、パラメータのサニタイザーを設定します。詳細については、lib/devise/parameter_sanitizer.rbをご覧ください。ご自身で設定したパラメータのサニタイザーを使いたい場合は、このメソッドをapplication controllerでオーバーライドしてください。）

メソッドの処理は、
Devise::ParameterSanitizerクラスをインスタンス化しています。
詳細については、lib/devise/parameter_sanitizer.rbをご覧ください。とのことでしたので、確認してみます。



###2. lib/devise/parameter_sanitizer.rb

すると、デフォルトの設定が見つかりました！！

~~~ruby:devise/lib/devise/parameter_sanitizer.rb
  DEFAULT_PERMITTED_ATTRIBUTES = {
      sign_in: [:password, :remember_me],
      sign_up: [:password, :password_confirmation],
      account_update: [:password, :password_confirmation, :current_password]
    }
~~~
ちゃんとpasswordが許可リストに入っています。それとremember_meもデフォルトで許可されているようです。
ちなみに、認証キーの情報については、以下の @auth_keys にありました。

~~~ruby:devise/lib/devise/parameter_sanitizer.rb
 def initialize(resource_class, resource_name, params)
      @auth_keys      = extract_auth_keys(resource_class)
      @params         = params
      @resource_name  = resource_name
      @permitted      = {}

      DEFAULT_PERMITTED_ATTRIBUTES.each_pair do |action, keys|
        permit(action, keys: keys)
      end
    end
~~~
ここまでで、deviseのデフォルトのストロングパラメータがわかりました。

ストロングパラメータがどのように追加されるのかを確認してみましたので、以下に記載します。

## devise ストロングパラメータの仕組み

###1.Devise::ParameterSanitizerクラスからインスタンス作成
~~~ruby:devise/lib/devise/controllers/helpers.rb 
      def devise_parameter_sanitizer
        @devise_parameter_sanitizer ||= Devise::ParameterSanitizer.new(resource_class, resource_name, params)
      end
~~~
####引数について
引数を理解するために、まずdevise_mappingについて確認します。

~~~ruby:devise/app/controllers/devise_controller.rb 
# Attempt to find the mapped route for devise based on request path
  def devise_mapping
    @devise_mapping ||= request.env["devise.mapping"]
  end
~~~
リクエストパスに基づいたマッピングルートを探してくれるメソッドです。
request.env["devise.mapping"]には、deviseのマッピング情報が格納されています。具体的には、routes.rbの"devise_for"で作成された、マッピングオブジェクトの情報が格納されています。

今回は、**devise_for** **:users**としています。

:::note info
map.devise_for :users
mapping = Devise.mappings[:user]

mapping.name #=> :user
# is the scope used in controllers and warden, given in the route as :singular.

mapping.as   #=> "users"
# how the mapping should be search in the path, given in the route as :as.

mapping.to   #=> User
# is the class to be loaded from routes, given in the route as :class_name.

mapping.modules  #=> [:authenticatable]
# is the modules included in the class
:::
参考：[Class: Devise::Mapping](https://www.rubydoc.info/github/plataformatec/devise/Devise/Mapping)

それでは、引数を確認していきます。
####1.resource_class

~~~ruby:devise/app/controllers/devise_controller.rb 
  # Proxy to devise map class
  def resource_class
    devise_mapping.to
  end
~~~
このメソッドでは、クラスネームが返されます。
**User**が返されます。

####2.resource_name

~~~ruby:devise/app/controllers/devise_controller.rb
# Proxy to devise map name
  def resource_name
    devise_mapping.name
  end
  alias :scope_name :resource_name
~~~
このメソッドでは、コントローラーで使用されているスコープ名が「：単数形」で返されます。
**:user**が返されます。

####3.params
リクエストからのパラメータ情報

###2.ストロングパラメータの初期化処理

クラスからインスタンスが作成(new)されたことがトリガとなって、initializeメソッドが呼び出されます。newメソッドの引数をそのまま受け取ります。
まず、インスタンス変数の値をそれぞれ確認していきましょう。

~~~ruby:devise/lib/devise/parameter_sanitizer.rb
 def initialize(resource_class, resource_name, params)
      @auth_keys      = extract_auth_keys(resource_class)
      @params         = params
      @resource_name  = resource_name
      @permitted      = {}

      DEFAULT_PERMITTED_ATTRIBUTES.each_pair do |action, keys|
        permit(action, keys: keys)
      end
    end
~~~
####1. @auth_keys
extract_auth_keys(resource_class)の値が入っています。

~~~ruby:devise/lib/devise/parameter_sanitizer.rb
   def extract_auth_keys(klass)
      auth_keys = klass.authentication_keys

      auth_keys.respond_to?(:keys) ? auth_keys.keys : auth_keys
    end
~~~
auth_keysに引数のauthentication_keysの値を代入しています。
引数(klass)の値は、resource_classの値（User）です。

~~~ruby:devise/lib/devise/strategies/authenticatable.rb
      def authentication_keys
        @authentication_keys ||= mapping.to.authentication_keys
      end
~~~
@authentication_keysにマッピングオブジェクトの認証キー情報を代入しています。

つまり、Userクラス（モデル）の認証キー情報を取得しているということですね。
※認証キーのデフォルト値は、:emailです。
> The default is
  # just :email.

####2.@params
引数のparamsの情報（リクエストパラメータ）が入っています。

####3.@resource_name
引数のresource_nameの値(:user)が入っています。

####4.@permitted
空の配列が入ります。この配列に、この後アクション名をキーにした、ストロングパラメータの値がこの配列の中に格納されます。

###3.パラメータのホワイトリスト（ストロングパラメータ）の作成

initializeメソッドの以下の部分がストロングパラメータの設定の部分にあたります。

~~~ruby:devise/lib/devise/parameter_sanitizer.rb
 DEFAULT_PERMITTED_ATTRIBUTES.each_pair do |action, keys|
        permit(action, keys: keys)
      end
~~~
####1.each_pairメソッドについて
each_pairメソッド は eachメソッドの別名（エイリアス）です。
ハッシュのキーと値を引数としてブロックを評価します。
DEFAULT_PERMITTED_ATTRIBUTESはハッシュ形式でデータが格納されていますので、ハッシュ用のeachメソッドを用います。

参考:[instance method Hash#each](https://docs.ruby-lang.org/ja/latest/method/Hash/i/each.html)

####2.permitメソッドについて

~~~ruby:devise/lib/devise/parameter_sanitizer.rb
    # Add or remove new parameters to the permitted list of an +action+.
    #
    # === Arguments
    #
    # * +action+ - A +Symbol+ with the action that the controller is
    #   performing, like +sign_up+, +sign_in+, etc.
    # * +keys:+     - An +Array+ of keys that also should be permitted.
    # * +except:+   - An +Array+ of keys that shouldn't be permitted.
    # * +block+     - A block that should be used to permit the action
    #   parameters instead of the +Array+ based approach. The block will be
    #   called with an +ActionController::Parameters+ instance.
    #
    # === Examples
    #
    #   # Adding new parameters to be permitted in the `sign_up` action.
    #   devise_parameter_sanitizer.permit(:sign_up, keys: [:subscribe_newsletter])
    #
    #   # Removing the `password` parameter from the `account_update` action.
    #   devise_parameter_sanitizer.permit(:account_update, except: [:password])
    #
    #   # Using the block form to completely override how we permit the
    #   # parameters for the `sign_up` action.
    #   devise_parameter_sanitizer.permit(:sign_up) do |user|
    #     user.permit(:email, :password, :password_confirmation)
    #   end
    #
    #
    # Returns nothing.
    def permit(action, keys: nil, except: nil, &block)
      if block_given?
        @permitted[action] = block
      end

      if keys.present?
        @permitted[action] ||= @auth_keys.dup
        @permitted[action].concat(keys)
      end

      if except.present?
        @permitted[action] ||= @auth_keys.dup
        @permitted[action] = @permitted[action] - except
      end
    end
~~~
説明書きによると、
アクションごとに、パラメータの許可リストに新たなパラメータを追加または、削除するメソッドです。
引数については、以下の４つをとります。

1. action（シンボル形式のアクション名）
1. keys（ 許可するパラメータの配列）
1. except（許可しないパラメータの配列）
1. block（アクション自体の許可パラメータをオーバーライドする用）

戻り値は、何もありません。内部的に許可パラメータを設定する処理を行うものということですね。

処理としては、
引数の条件によって、initializeメソッドで定義した、@permittedの空配列にアクション名をキーに、許可するパラメータを値としてデータをハッシュ形式で格納します。
#####1.blockの場合
ブロックの値をそのまま配列に格納

#####2.keysが存在する場合（許可パラメータを追加）
@permitted[action]の"action"にDEFAULT_PERMITTED_ATTRIBUTESのアクション名を代入。
@permitted[action]に、[dup](https://docs.ruby-lang.org/ja/latest/method/Object/i/clone.html)メソッドで@auth_keys（認証キー）から複製したオブジェクトを代入。
@permitted[action]に、[concat](https://docs.ruby-lang.org/ja/latest/method/Array/i/concat.html)メソッドで配列を追加

**例(sign_inアクション)**
@permitted[:sign_in] = [:email,:password,:remember_me]
hashにすると以下のようになる。
{:sign_in=>[:email,:password,:remember_me]}

#####3.exceptが存在する場合（許可パラメータを削除）
@permitted[action]の"action"にDEFAULT_PERMITTED_ATTRIBUTESのアクション名を代入。
@permitted[action]に、[dup](https://docs.ruby-lang.org/ja/latest/method/Object/i/clone.html)メソッドで@auth_keys（認証キー）から複製したオブジェクトを代入。
※認証キーは必須のため。
@permitted[action]から引数exceptの値を控除する。

##まとめ

~~~ruby:devise/lib/devise/parameter_sanitizer.rb
  DEFAULT_PERMITTED_ATTRIBUTES = {
      sign_in: [:password, :remember_me],
      sign_up: [:password, :password_confirmation],
      account_update: [:password, :password_confirmation, :current_password]
    }
~~~
~~~ruby:devise/lib/devise/parameter_sanitizer.rb
 def initialize(resource_class, resource_name, params)
      @auth_keys      = extract_auth_keys(resource_class)
      @params         = params
      @resource_name  = resource_name
      @permitted      = {}

      DEFAULT_PERMITTED_ATTRIBUTES.each_pair do |action, keys|
        permit(action, keys: keys)
      end
    end
~~~

deviseのデフォルトのストロングパラメータの値は、ハッシュ形式でDEFAULT_PERMITTED_ATTRIBUTESに格納されている。
deviseインストールをすると、Devise::ParameterSanitizerクラスからインスタンスが作成され、それがトリガとなり、initializeメソッドが実行される。
initializeメソッドでは、スコープの認証キーの情報を取得し、permitメソッドで、取得した認証キーの情報と、DEFAULT_PERMITTED_ATTRIBUTESのデータからパラメータの許可リストを作成する。ここで作成される許可リストは、アクション名をキーに持ったハッシュ形式のリストである。

**例(sign_inアクション)**
@permitted[:sign_in] = [:email,:password,:remember_me]
hashにすると以下のようになる。
{:sign_in=>[:email,:password,:remember_me]}

##参考
[heartcombo/devise](https://github.com/heartcombo/devise)
[Strong Parameters](https://github.com/heartcombo/devise#strong-parameters)
[Class: Devise::Mapping](https://www.rubydoc.info/github/plataformatec/devise/Devise/Mapping)
[Class: Devise::ParameterSanitizer](https://www.rubydoc.info/github/plataformatec/devise/Devise/ParameterSanitizer)
[instance method Hash#each](https://docs.ruby-lang.org/ja/latest/method/Hash/i/each.html)
[dup](https://docs.ruby-lang.org/ja/latest/method/Object/i/clone.html)
[concat](https://docs.ruby-lang.org/ja/latest/method/Array/i/concat.html)