【Rails Gem】deviseでやっておくべき設定変更

# はじめに
deviseは、Ruby on Railsアプリケーションにおけるユーザー認証機能を簡単に実装できる有名なGemです。ログイン機能やユーザー登録、パスワードのリセット、アカウントのロック、二段階認証といった認証に関する様々な機能を一括して提供します。
Railsの代表的な認証ツールとして多くのプロジェクトで採用されており、カスタマイズ性とセキュリティの高さが特徴です。
また、Railsの「プラグイン可能なモジュール」の設計思想に基づいて構築されており、必要な機能だけを選んで導入できる柔軟性も持っています。

が、しかし！
便利な反面、使い方を間違えるとセキュリティの問題につながることもあります。
たとえば、設定を初期状態のまま使っていたり、カスタマイズを怠ったりすると、思わぬリスクを招くことがあります。

## ターゲット
- deviseを初めて使う人
- 有名だからよくわからないけどとりあえずで入れてみたって人
- 初期設定のまま使っている人

## 環境
この記事記載時は
ruby 3.2.2
rails 7.1.5
devise 4.9.4
を使って作っています。

## deviseの導入
調べればわかりやすい記事がいっぱい出てくるのでここでは省略します。
devise:install
実行して
User modelとかViewとか作ってログインできたやったー
までは終わっている（自分でできる）前提で進めていきます。

## 脆弱性
さてこの初期状態で脆弱性診断を行うといくつか問題が指摘されると思います。
そこを指摘されないように設定変更していくのが今回の目的です。

指摘が予想される項目としては
- セッションタイムアウトの時間設定がされていない
- アカウントロックの設定がされていない
- ユーザー登録情報が露見する
- 脆弱なパスワードポリシー

などがあります。

これらについて対応方法をまとめていきたいと思います。

## セッションタイムアウトの時間設定
セッションタイムアウトは、ユーザーが一定時間操作を行わなかった場合にセッションを自動的に終了させる仕組みです。
これにより、以下のようなセキュリティ上の利点、欠点があります。

#### 利点：

- 不正アクセスのリスク軽減
公共の端末や共有デバイスで、ログアウトを忘れた場合でも、一定時間後にセッションが終了することで、第三者による不正な操作を防止できます。
- 長時間放置による情報漏洩の防止
重要なデータや機密情報へのアクセスが、長時間放置された状態で維持されることを避けられます。

#### 欠点：
- ユーザーエクスペリエンスの低下
セッションが予期せず切れることで、ユーザーが作業内容を失ったり、再ログインを求められたりすることがあります。
- 過剰な短時間設定による不便さ
セッションの有効期限が短すぎる場合、頻繁にログインを要求され、ユーザーの作業効率を妨げる可能性があります。
これらの利点と欠点を理解した上で、適切なタイムアウト時間を設定することが重要です。

次に、Deviseを使ったセッションタイムアウトの具体的な設定方法を紹介します。
※変更前はdevise:install後デフォルトの状態

```diff:config/initializers/devise.rb
  # ==> Configuration for :timeoutable
  # The time you want to timeout the user session without activity. After this
  # time the user will be asked for credentials again. Default is 30 minutes.
- # config.timeout_in = 30.minutes
+ config.timeout_in = 30.minutes
```

```diff:app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,
+        :timeoutable
end
```

この変更によりセッション更新から指定時間以上空くとセッションの無効化が行われ、`current_user` で取得できる値も `nil` となる。


## アカウントロックの設定
アカウントロック機能は、不正ログインやセキュリティ侵害を防ぐための重要な仕組みです。
これにより、以下のようなセキュリティ上の利点、欠点があります。

#### 利点:
- ブルートフォース攻撃の抑止
複数回のパスワード入力ミス後にアカウントをロックすることで、不正アクセス者が無制限にパスワードを試行するのを防ぐことができます。
- ユーザー保護の強化
不正ログインの試みが発生した際、アカウントを自動でロックすることで、ユーザーの情報を保護する信頼性の高い仕組みを提供します。

#### 欠点:
- 正規ユーザーへの影響
正当なユーザーがログイン情報を複数回間違えた場合でもロックされるため、サポート対応の負担が増加する可能性があります。
- 悪用のリスク
意図的に他人のアカウントにアクセスを試みてロック状態にする「サービス妨害攻撃」の一種として利用される危険性があります。

これらの利点と欠点を理解した上で、適切なアカウントロックまでの回数や実装可否を決定することが重要です。

次に、Deviseを使ったアカウントロックの具体的な設定方法を紹介します。
※変更前はdevise:install後デフォルトの状態


#### userテーブルロック情報のカラム追加
```
rails generate migration AddLockableToUsers failed_attempts:integer unlock_token:string locked_at:datetime
```

#### migrationファイル編集
```diff:db/migrate/*********_add_lockable_to_users.rb
class AddLockableToUsers < ActiveRecord::Migration[7.1]
  def change
    add_column :users, :failed_attempts, :integer
    add_column :users, :unlock_token, :string
    add_column :users, :locked_at, :datetime
+   add_index :users, :unlock_token, unique: true # index追加
  end
end
```
#### DBに反映
```
rails db:migrate
```

#### 設定ファイル変更
デフォルトのコメントにある通り
ロックされたとき、
メールでロック解除のURLを送る場合 `:email`
時間経過でロック解除する場合 `:time`
メールでも時間経過でも解除できるようにする場合 `:both`
ユーザーでは解除できないシステム側でのみ解除する場合(またはその仕組を別で作る場合) `:none`
を `config.unlock_strategy` に指定
`config.maximum_attempts` は何回間違えたらロックするか
`config.unlock_in` は時間経過でのロック解除までの時間
```diff:config/initializers/devise.rb
  # Defines which strategy will be used to unlock an account.
  # :email = Sends an unlock link to the user email
  # :time  = Re-enables login after a certain amount of time (see :unlock_in below)
  # :both  = Enables both strategies
  # :none  = No unlock strategy. You should handle unlocking by yourself.
- # config.unlock_strategy = :both
+ config.unlock_strategy = :both

  # Number of authentication tries before locking an account if lock_strategy
  # is failed attempts.
- # config.maximum_attempts = 20
+ config.maximum_attempts = 20

  # Time interval to unlock the account if :time is enabled as unlock_strategy.
- # config.unlock_in = 1.hour
+ config.unlock_in = 1.hour
```

```diff:app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,
+        :recoverable, # メールでロック解除する場合
+        :lockable

end
```

## ユーザー登録情報露見の防止
パスワードリマインダーを利用する際、メールアドレスを入力してパスワードリセットを行います。このとき、登録されていないメールアドレスを入力しエラーメッセージが表示される仕様だと、サービスにそのメールアドレスが登録されていないことが第三者に知られてしまう可能性があります。
Paranoidモードを有効にすることで、登録の有無にかかわらず同じ応答を返すようになり、ユーザー情報が漏洩するリスクを防ぐことができます。
これにより、以下のようなセキュリティ上の利点、欠点があります。

#### 利点：
- 登録メールアドレスの特定を防止
存在しないメールアドレスを入力した場合でも、登録されたメールアドレスかどうかを特定できないため、攻撃者が辞書攻撃やユーザー情報収集を行うのを防ぎます。

#### 欠点:
- ユーザーの入力ミスが検出しにくい
正規のユーザーがメールアドレスを誤入力しても、同じメッセージが表示されるため、自分のミスに気づきにくい場合があります。

- 追加のサポート対応が必要になる可能性
「リセットメールが届かない」と問い合わせるユーザーが増える可能性があり、サポート対応が煩雑になることがあります。

次に、Deviseを使ったParanoidモードの具体的な設定方法を紹介します。
※変更前はdevise:install後デフォルトの状態

```diff:config/initializers/devise.rb
  # It will change confirmation, password recovery and other workflows
  # to behave the same regardless if the e-mail provided was right or wrong.
  # Does not affect registerable.
- # config.paranoid = true
+ config.paranoid = true
```
## パスワードポリシーの強化その１
初期状態では6文字以上という制限はかかっていますが、数字だけ、アルファベットだけのように簡単なパスワードを設定できるようになっており、脆弱なパスワードポリシーとなっています。
そこで、英数字大文字・小文字混在のパスワードでないとエラーなどの処理を追加することで、パスワードポリシーの強化を行います。
これにより、以下のようなセキュリティ上の利点、欠点があります

#### 利点:
- セキュリティ向上
短く単純なパスワードは推測や総当たり攻撃に対して非常に弱いですが、最低文字数や記号、大文字、小文字の使用を義務付けることで、攻撃者がパスワードを突破する難易度を大幅に上げることができます。

- セキュリティの標準化
組織のセキュリティポリシーや業界標準（例: OWASPの推奨基準）に準拠しやすくなり、コンプライアンスの観点からもメリットがあります。

#### 欠点:
- ユーザーエクスペリエンスの低下
複雑なパスワードを求められることで、ユーザーが覚えにくいパスワードを設定し、メモに書き留めたり再設定を繰り返したりする可能性があります。

- 実装と管理の手間
バリデーションルールの実装やカスタマイズ、ユーザーからの問い合わせ対応など、開発者や運用者の負担が増える可能性があります。

次に、Deviseを使った複雑化によるパスワードポリシー強化の具体的な設定方法を紹介します。
※変更前はdevise:install後デフォルトの状態

#### カスタムバリデーションを作成
文字種（記号、大文字、小文字、数字）を含むルールを作るには、カスタムバリデータを使用します。

```app/validators/complex_password_validator.rb
class ComplexPasswordValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    return if value.blank?

    # 必須条件: 数字, 大文字, 小文字, 記号
    unless value =~ /[0-9]/ && value =~ /[A-Z]/ && value =~ /[a-z]/ && value =~ /[^A-Za-z0-9]/
      record.errors.add(attribute, :complexity, message: "パスワードには、大文字、小文字、数字、記号をそれぞれ1つ以上含めてください。")
    end
  end
end
```

#### Userモデルにバリデーションを追加
Deviseを使用しているUserモデルに、パスワードの辞書チェックを追加します。
```diff:app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,

+ # パスワードの複雑性バリデーションを追加
+ validates :password, complex_password: true
end
```

パスワード文字数制限変更はこちら
```config/initializers/devise.rb
  # ==> Configuration for :validatable
  # Range for password length.
  config.password_length = 6..128
```

## パスワードポリシーの強化その２
初期状態では6文字以上という制限はかかっていますが、`p@ssw0rd` など簡単に予測できたり、世間でよく使われているパスワードを設定し、リスト攻撃で簡単に突破されてしまうおそれがあります。
そこで辞書ブロック制限の追加をすることで、パスワードポリシーの強化を行います。
これにより、以下のようなセキュリティ上の利点、欠点があります

#### 利点:
- セキュリティ向上
簡単に推測できるパスワード（例: "password", "123456"など）の使用を防ぎ、総当たり攻撃や辞書攻撃による不正アクセスのリスクを大幅に低減できます。

- セキュリティ基準の遵守
多くのセキュリティ診断基準では、弱いパスワードの利用を禁止することが推奨されています。辞書ブロックを導入することで、これらの基準を満たしやすくなります。

- ユーザー教育
ユーザーが「安全でないパスワード」を認識するきっかけとなり、より強力なパスワードの使用を促進します。

#### 欠点:
- ユーザーエクスペリエンスの低下
辞書に登録された単語が拒否されることで、ユーザーがパスワードを設定する際にフラストレーションを感じる可能性があります。

- 運用負荷の増加
辞書データを適切にメンテナンスし続ける必要があり、例えば新たに一般的なパスワードリストを定期的に追加するなどの運用が必要です。

- 辞書の限界
辞書に登録されていない弱いパスワード（例: "mypassword123"）を完全に排除することはできないため、他のパスワードポリシー（文字数や複雑性のチェック）と組み合わせる必要があります。

次に、Deviseを使った辞書ブロックの具体的な設定方法を紹介します。
※変更前はdevise:install後デフォルトの状態

#### 辞書ファイルの準備
辞書に登録したい禁止パスワードのリストを作成し任意の場所に保存します。
ググればよく使われるパスワードリストなど数百行〜数十万行のファイルが様々用意してくれているところもあるのでそちらを拝借しても良いかと思います。

例) config/weak_passwords.txt
```
password
p@ssw0rd
123456
qwerty
testtest
```

#### カスタムバリデーションを作成
禁止パスワードのチェックを行うカスタムバリデータを作成します。
辞書データが大きい場合、Setを使うことで検索速度を向上できます。

```app/validators/not_in_dictionary_validator.rb
require 'set'

class NotInDictionaryValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    return if value.blank?

    # 辞書ファイルの読み込み
    dictionary = Set.new(File.readlines(Rails.root.join('config', 'weak_passwords.txt')).map(&:strip))

    # 辞書に登録されているパスワードと一致するかを確認
    if dictionary.include?(value.downcase)
      record.errors.add(attribute, :weak_password, message: "is too common and not allowed.")
    end
  end
end
```

#### Userモデルにバリデーションを追加
Deviseを使用しているUserモデルに、パスワードの辞書チェックを追加します。
```diff:app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,

+ # パスワードの辞書バリデーションを追加
+ validates :password, not_in_dictionary: true
end
```

## まとめ

セキュリティ強化は、アプリケーションの信頼性を高める上で非常に重要です。
今回紹介したセッションタイムアウト、アカウントロック、パスワードポリシー（辞書ブロックや複雑性の強制）などの設定は、不正アクセスのリスクを大幅に低減します。
しかし、これらの対策にはユーザーエクスペリエンスの低下や運用負荷の増加といった課題も伴います。
そのため、「すべて絶対設定する必要がある！」というわけではなく、セキュリティと利便性のバランスを考慮し、アプリケーションの特性や利用者に適した形で実装することが重要です。
また、これらを実装しただけで安心するのではなく、他のセキュリティ対策や運用体制も併せて検討する必要があります。