[Rails6.0]has_secure_password の password_confirmation の値を nil にすると、validation をパスする

モデルにパスワードを実装するとき、`has_securepassword`を使っています。

これを使えば`password`, `password_confirmation`の属性を作って、`password_digest`属性に BCrypt でハッシュした password の値を保存してくれます。

この`password_confirmation`という属性、実は値を`nil`にすると **validation を通ってしまいます。**

今回は、`has_secure_password`の挙動について記事にします。

# has_secure_password とは

`has_secure_password`とは、Rails が標準に提供するメソッドです。
これを使うことで、簡単にモデルにパスワードを暗号化して保存、認証する機能を追加できます。
[Railsチュートリアル](https://railstutorial.jp/)で使ったことがある方も多いのではないでしょうか。

# validationをコンソールで確認する

実際に`password_confirmation`の値に`nil`を入れるとどうなるのか、コンソールで確認します。
終了時にデータが元に戻るように`--sandbox`オプションをつけています。

```bash
$ bin/rails c --sandbox
Loading development environment in sandbox (Rails 6.0.0)
Any modifications you make will be rolled back on exit
```

モデルを`new`で作成し、`valid?`メソッドで validation を確認していきます。

```irb
irb(main):001:0> user = User.new(name: 'test', password: 'password', password_confirmation: 'password')
   (0.2ms)  BEGIN
=> #<User id: nil, name: "test", password_digest: [FILTERED], created_at: nil, updated_at: nil>
irb(main):002:0> user.valid?
=> true # passwordとpassword_confirmationの値が一致しているためtrue
irb(main):003:0> user.password_confirmation = 'badpassword'
=> "badpassword" # password_confirmationの値を変更する
irb(main):004:0> user.valid?
=> false # passwordとpassword_confirmationの値が一致していないためfalse
irb(main):005:0> user.password_confirmation = nil
=> nil # password_confirmationの値をnilに変更する
irb(main):006:0> user.valid?
=> true # validationが通る！
```

このように`password_confirmation`が`nil`の時は validation が通ってしまいます。

# 値が nil の時の挙動

[ActiveModel::SecurePassword::ClassMethods](https://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html)によると、

```
If confirmation validation is not needed, 
simply leave out the value for XXX_confirmation (i.e. don't provide a form field for it). 
When this attribute has a nil value, the validation will not be triggered.
```

つまり、**`password_confirmationの値がnilのとき、password_confirmationは使われない`**ようです。
先ほどの例だと、`password_confirmation`が`nil`の時は`password_confirmation`を無視していたため、 validation が通ったわけです。

# password_confirmation 部分の実装

`has_secure_password` の実装も確認してみましょう。

https://github.com/rails/rails/blob/master/activemodel/lib/active_model/secure_password.rb

上記のコードの全体像から、`has_secure_password`メソッドの実装部分を切り取りました。

```secure_password.rb
def has_secure_password(attribute = :password, validations: true)
  # Load bcrypt gem only when has_secure_password is used.
  # This is to avoid ActiveModel (and by extension the entire framework)
  # being dependent on a binary library.
  begin
    require "bcrypt"
  rescue LoadError
    $stderr.puts "You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install"
    raise
  end

  include InstanceMethodsOnActivation.new(attribute)

  if validations
    include ActiveModel::Validations

    # This ensures the model has a password by checking whether the password_digest
    # is present, so that this works with both new and existing records. However,
    # when there is an error, the message is added to the password attribute instead
    # so that the error message will make sense to the end-user.
    validate do |record|
      record.errors.add(attribute, :blank) unless record.send("#{attribute}_digest").present?
    end

    validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
    validates_confirmation_of attribute, allow_blank: true
  end
end
```

このコード内の

```rb
validates_confirmation_of attribute, allow_blank: true
```

というコードよって`password_confirmation`属性が追加されています。
また、このコードには`allow_blank`オプションが付いています。
[Rails ガイド](https://railsguides.jp/active_record_validations.html)によると、

> :allow_blank オプションは:allow_nil オプションと似ています。
> このオプションを指定すると、属性の値が blank?に該当する場合（nil や空文字など）にバリデーションがパスします。

つまり、
`password_confirmation.blank? == true`
だと validation をスキップするように実装されています。

# まとめ

確認用の属性を使わないように設定することが容易に実装できるようです。
パスワードの validation 周りが柔軟に実装できそうでとても良いですね。

何か直すべき点、間違った記述があればコメント等で指摘していただければと思います。

# 以下Tips
##has_secure_password の新機能

Rails6.0 から`has_secure_password`にカラムを指定できるようになりました。
先ほどの実装のメソッドの宣言部分を見てみると、

```secure_password.rb
def has_secure_password(attribute = :password, validations: true)
```

と、`attribute`にシンボルで引数を渡すことで様々なカラムで`has_secure_password`を使用出来るようになりました。
これで`activate_digest`, `reset_digest`なんかも実装しやすくなったのではないでしょうか。
詳しくは@suketa 様の
[Rails6 のちょい足しな新機能を試す 11（has_secure_password 編）](https://qiita.com/suketa/items/e0eea59af9843f2a4f4c)
という記事が非常に分かりやすいです。是非ご覧ください。

# 参考文献

https://naokirin.hatenablog.com/entry/2019/03/29/032801
https://railsguides.jp/6_0_release_notes.html
https://railsguides.jp/active_record_validations.html