Device パスワードリセットの照合プロセス - トークンはどのように照合されるのか


Rails devise でパスワードリセットを行う際のトークンの照合プロセスは次のようになっています。
掲載している devise のコードは 2020-06-11時点での Devise のマスターブランチです。

パスワードをリセットする際に、メールでトークン付きのURLが送られます。
パスワードのリセットは、次のようにコンソールで実行すると行われ、メールが送信されてトークンが生成・保存されます。

u = User.find(XX)
u.send_reset_password_instructions

トークン付きのURLを開いてパスワードをリセットするフォームを送信すると、複雑なカスタマイズをしていない限り、 次の update メソッド が実行されます。
ここで resource_params は 次のようなパラメータ(入力値)です。

{"reset_password_token"=>"xxx_token_xxx", "password"=>"password", "password_confirmation"=>"password"}

class Devise::PasswordsController < DeviseController
  # PUT /resource/password
  def update
    self.resource = resource_class.reset_password_by_token(resource_params)
    yield resource if block_given?

    if resource.errors.empty?
      resource.unlock_access! if unlockable?(resource)
      if Devise.sign_in_after_reset_password
        flash_message = resource.active_for_authentication? ? :updated : :updated_not_active
        set_flash_message!(:notice, flash_message)
        resource.after_database_authentication
        sign_in(resource_name, resource)
      else
        set_flash_message!(:notice, :updated_not_active)
      end
      respond_with resource, location: after_resetting_password_path_for(resource)
    else
      set_minimum_password_length
      respond_with resource
    end
  end

update メソッド ではこの resource_paramsreset_password_by_token 関数 に渡して、 その中でトークンの照合を行います。

URLについていたトークンから Devise.token_generator.digest によってダイジェスト値が生成されます。
その値がデータベースの reset_password_token の値で、 それをキーにして resource を探します。

module Devise
  module Models
    module Recoverable
      module ClassMethods

        # Attempt to find a user by its reset_password_token to reset its
        # password. If a user is found and token is still valid, reset its password and automatically
        # try saving the record. If not user is found, returns a new user
        # containing an error in reset_password_token attribute.
        # Attributes must contain reset_password_token, password and confirmation
        def reset_password_by_token(attributes={})
          original_token       = attributes[:reset_password_token]
          reset_password_token = Devise.token_generator.digest(self, :reset_password_token, original_token)

          recoverable = find_or_initialize_with_error_by(:reset_password_token, reset_password_token)

          if recoverable.persisted?
            if recoverable.reset_password_period_valid?
              recoverable.reset_password(attributes[:password], attributes[:password_confirmation])
            else
              recoverable.errors.add(:reset_password_token, :expired)
            end
          end

          recoverable.reset_password_token = original_token if recoverable.reset_password_token.present?
          recoverable
        end

たとえば、 token の値を ABC にしたかったら、 Devise.token_generator.digest(User, :reset_password_token, 'ABC') を実行して、 出てきた値を データベース に登録すればいいことになります。