devise-securityのpassword_expirableを使ってみる

こんにちは!kossyです!




さて、今回はdeviseのextensionであるdevise-securityを使ってみましたので、使い方をブログに残してみたいと思います。
なお、devise_token_authを用いてToken認証をしていることを前提とします。


環境

Ruby 2.6.6
Rails 6.0.3.6
devise 4.7.3
devise-security 0.15.0
devise_token_auth 1.1.3



devise-securityとは?

deviseベースの認証機能を使用するアプリに、エンタープライズ向けなより強固なセキュリティ機能を提供するGemです。
例えば、過去に設定したパスワードを再度設定することを禁止する機能や、パスワードの有効期限を設定する機能、
秘密の質問機能等があります。

これらの機能を、
・各種モジュールのinclude
・カラムの追加
・configの設定

等だけで使えるようにしてくれる優れものです。


導入

まずは何はともあれGemfileに以下を追加。

gem 'devise-security'

でbundle installを実行。

your_app $ bundle install

devise-securityを導入すると devise_security:install コマンドが実行できるようになりますので、実行します。

$ rails g devise_security:install

実行するとconfig/initializers/devise-security.rbが作成されます。
このファイルで初期の設定を行うことができます。

参考: GitHub - devise-security/devise-security: A security extension for devise, meeting industrial standard security demands for web applications.

devise-securityを使うといくつかのセキュアな設定を追加できますが、今回はpassword_expirableを使ってみようと思います。


password_expirable

パスワードの有効期限を設定できるようになります。

password_expirableを使う場合はpassword_changed_atカラムを生やす必要があるので、マイグレーションファイルを作成します。

# db/migrate/add_password_changed_at_to_resources_timestamps.rb

create_table :the_resources do |t|
  # other devise fields

  t.datetime :password_changed_at
end
add_index :the_resources, :password_changed_at
$ bundle exec rails db:migrate

password_expirableを使いたいモデルに以下を追加します。

# app/models/resource.rb

devise :password_expirable

有効期限の設定はconfig/initializers/devise-security.rbのexpire_password_afterで行います。

# config/initializers/devise-security.rb

Devise.setup do |config|
  # ==> Security Extension
  # Configure security extension for devise

  # Should the password expire (e.g 3.months)
  config.expire_password_after = 3.months

app/controllers/devise_token_auth/sessions_contoller.rbを修正して、パスワードの有効期限が切れているかどうかの検証を行うようにします。

    def create
      # Check
      field = (resource_params.keys.map(&:to_sym) & resource_class.authentication_keys).first

      @resource = nil
      if field
        q_value = get_case_insensitive_field_from_resource_params(field)

        @resource = find_resource(field, q_value)
      end

      if @resource && valid_params?(field, q_value) && (!@resource.respond_to?(:active_for_authentication?) || @resource.active_for_authentication?)
        valid_password = @resource.valid_password?(resource_params[:password])
        if (@resource.respond_to?(:valid_for_authentication?) && !@resource.valid_for_authentication? { valid_password }) || !valid_password
          return render_create_error_bad_credentials
        end

        return render_password_change if @resource.need_change_password? # 追加

        @token = @resource.create_token
        @resource.save

        省略

password_expirableモジュールをincludeすると、need_change_password?メソッドを実行できるようになります。

中身の処理はこちら。

need_change_password?がtrueの場合は、render_pasword_changeを呼ぶようにしています。

    def render_destroy_error
      render_error(404, I18n.t('devise_token_auth.sessions.user_not_found'))
    end

    # 追加
    def render_password_change
      render_error(401, 'need_change_password')
    end

SPAでの利用を想定しているので、need_change_passwordが渡されてきたら、パスワード変更のページを表示するように分岐する必要がありますが、、、

この状態でPostman等のツールで動作を確認してみます。

動作確認のために、一旦expire_password_afterの値を短くします。

config.expire_password_after = 1.second

そしてコンソール等でpassword_changed_atに値を入れます。

# devise-securityを利用しているUserモデルがあると想定
$ User.first.update!(password_changed_at: Time.now.ago(7.days))

この状態でログインを試みます。

f:id:kossy-web-engineer:20210403204107p:plain

想定通りのレスポンスを得られました。

まとめ

devise_token_authと組み合わせて使うことを想定していない(パスワード変更画面はerbでレンダリングする)ので、
ある程度カスタムが必要ですが、ロジックはモジュールで提供してくれるので、十分使えると思います。

大いに参考にさせていただいたサイト

この場を借りてお礼を申し上げます。

devise-securityを使って、よりセキュアなRailsアプリを構築する - Qiita