devise-securityのparanoid_verificationのソースコードを追ってみた

こんにちは!kossyです!




今回はdevise-securityのparanoid_verificationのソースコードを追ってみたので、備忘録としてブログに残してみたいと思います。



環境

Ruby 3.0.3
Rails 6.0.4
devise-security 0.16.0



paranoid_verificationってなに?

paranoid_verification モジュールは、「アプリケーションの管理者がいつでも、ユーザーが自分自身を確認するように強制できる」機能です。

以下のPRが paranoid_verification モジュールが実装された時のものです。

github.com

devise_security gem は メンテの止まった devise_security_extension Gemから公式にforkされたGemのため、blameしてもPRまで遡るのができないコードがあります。

paranoid_verification はform前の実装だったので、devise-security-extension gemの過去のPRを見たところ、以下の記載がありました。

Basically I got requirement for one application that "reset password" links should be additional verified after user set his Password.
He should call application support team and they will give him verification code. (hardcore security)

But another usage of this feature is that at any point admin of application can enforce that user should verify himself.

so the feature: Generate (paranoid) verification code and enforce user to fill in verification code.
Until then user wont be able to use the application (similar functionality of expired password)


基本的に、ユーザーがパスワードを設定した後、「パスワードのリセット」リンクを追加で確認する必要があるという1つのアプリケーションの要件がありました。
ユーザーはアプリケーションサポートチームに電話する必要があり、サポートチームはユーザーに確認コードを与えます。 (ハードコアセキュリティ)

ただし、この機能のもう1つの使用法は、アプリケーションの管理者がいつでも、ユーザーが自分自身を確認するように強制できることです。

そのため、機能: paranoid verification code を生成し、ユーザーに検証コードの入力を強制します。
それまでは、ユーザーはアプリケーションを使用できません(期限切れのパスワードと同様の機能)

出典: https://github.com/phatworx/devise_security_extension/pull/117

「アプリケーションの管理者がいつでも、ユーザーが自分自身を確認するように強制したい」という要件がある場合は、このモジュールを有効活用できそうですね。



コードリーディング

上記の機能をどのように実現しているのでしょうか。実際にコードを読んでコンソールで実行しつつ仕様の理解を進めてみます。


need_paranoid_verification?

github.com

def need_paranoid_verification?
  !!paranoid_verification_code
end

paranoid_verificationを使う際にテーブルに追加する必要のあるカラムである paranoid_verification_code の値の有無をBooleanで返却するメソッドでした。

用途としてはメソッド命名的に検証コードを実行する必要があるかどうか?を判定するために用いるためかと。


generate_paranoid_code

github.com

def generate_paranoid_code
  update_without_password paranoid_verification_code: Devise.verification_code_generator.call(),
                          paranoid_verification_attempt: 0
end

名前の通りparanoid_codeをgenerateし、レシーバーに保存するメソッドのようです。(updateするなら!をメソッド名につけるのが慣習的にいいと思うが)

Devise.verification_code_generator.call()の返り値は以下のように、5文字のランダムな文字列が返るようです。

$ Devise.verification_code_generator.call()
=> "c8e93"
$ Devise.verification_code_generator.call()
=> "98102"

定義元は以下でした。

github.com

# captcha integration for confirmation form
mattr_accessor :verification_code_generator
@@verification_code_generator = -> { SecureRandom.hex[0..4] }

captcha向けに5文字の文字列を返しているんですね。なぜ5文字なんだろう、普通一時的な認証コードって6文字が一般的では?と思ったんですが、captcha向けなら納得です。


paranoid_attempts_remaining

github.com

def paranoid_attempts_remaining
  Devise.paranoid_code_regenerate_after_attempt - paranoid_verification_attempt
end

あと何回検証コードの実行ができるかを返却するメソッドのように見えますが、コンソールで試してみます。

$ user = User.first

$ user.paranoid_verification_attempt
=> 1

$ user.paranoid_attempts_remaining
=> 9

# config/devise-security.rbで設定できる値です(デフォルトは10)
$ Devise.paranoid_code_regenerate_after_attempt
=> 10

verify_code

github.com

def verify_code(code)
  attempt = paranoid_verification_attempt

  if (attempt += 1) >= Devise.paranoid_code_regenerate_after_attempt
    generate_paranoid_code
  elsif code == paranoid_verification_code
    attempt = 0
    update_without_password paranoid_verification_code: nil,
                            paranoid_verified_at: Time.now,
                            paranoid_verification_attempt: attempt
  else
    update_without_password paranoid_verification_attempt: attempt
  end
end

Devise.paranoid_code_regenerate_after_attemptで設定した値を上回っていない場合は generate_paranoid_code を実行して、

引数のcodeとparanoid_verification_codeが一致した場合は、

paranoid_verification_codeをnilで更新
paranoid_verified_atに現在時刻で更新
paranoid_verification_attemptに0で更新

をパスワード抜きで行っていました。

wikiを見てみる

github.com

wikiを見ると、locakbleで提供されているメソッドをオーバーライドして使うことを推奨しているようです。

lock after reset password
One example of usage could be that after a user resets their password they need to contact support for the verification code. Just add to your authentication resource code similar to this:

class User < ActiveRecord::Base
  # ...
  def unlock_access!
    generate_paranoid_code
    super
  end
end

他にも管理者アカウントでロックする方法や検証コードの認証試行回数を表示する方法などが記載されていました。




勉強になりました。