認証機能を提供するGem「devise」のtimeoutableのソースコードを追ってみた

こんにちは!kossyです!




今回は認証機能を提供するGem「devise」のtimeoutableのソースコードを追ってみたので、
備忘録としてブログに残してみたいと思います。




環境

Ruby 2.6.6
Rails 6.0.3
devise 4.8.0



github.com





なお、説明の前提として、deviseを利用しているUserモデルが定義されていることとします。





timeoutableとは?

deviseのtimeoutable moduleは、セッションがすでに期限切れになっているかどうかを確認して、
設定された時間が経過した後にセッションが期限切れになると、ユーザーはログインページにリダイレクトされ、もう一度ログインを求められるものです。

導入は簡単で、deviseを用いるモデルに以下を追加するだけで実現できます。

class User < ActiveRecord::Base

  devise :timeoutable

end

また、デフォルトではセッションタイムアウトの時間は30分間ですが、config/initializers/devise.rb内でよしなに変更することができます。

# 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

さて、上記の仕組みはどのようにして実現されているんでしょうか。


ソースコードを追う

メインのソースコードはこちら。

github.com

timedout?メソッドが参照されている箇所で判定しているとみて、使用位置を見に行ってみます。

      def timedout?(last_access)
        !timeout_in.nil? && last_access && last_access <= timeout_in.ago
      end

使用位置はこのファイルでした。

github.com

# frozen_string_literal: true

# Each time a record is set we check whether its session has already timed out
# or not, based on last request time. If so, the record is logged out and
# redirected to the sign in page. Also, each time the request comes and the
# record is set, we set the last request time inside its scoped session to
# verify timeout in the following request.
Warden::Manager.after_set_user do |record, warden, options|
  scope = options[:scope]
  env   = warden.request.env

  if record && record.respond_to?(:timedout?) && warden.authenticated?(scope) &&
     options[:store] != false && !env['devise.skip_timeoutable']
    last_request_at = warden.session(scope)['last_request_at']

    if last_request_at.is_a? Integer
      last_request_at = Time.at(last_request_at).utc
    elsif last_request_at.is_a? String
      last_request_at = Time.parse(last_request_at)
    end

    proxy = Devise::Hooks::Proxy.new(warden)

    if !env['devise.skip_timeout'] &&
        record.timedout?(last_request_at) &&
        !proxy.remember_me_is_active?(record)
      Devise.sign_out_all_scopes ? proxy.sign_out : proxy.sign_out(scope)
      throw :warden, scope: scope, message: :timeout
    end

    unless env['devise.skip_trackable']
      warden.session(scope)['last_request_at'] = Time.now.utc.to_i
    end
  end
end

コメントアウト部分は、

Each time a record is set we check whether its session has already timed outor not, based on last request time.

レコードが設定されるたびに、最後のリクエスト時間に基づいて、そのセッションがすでにタイムアウトしているかどうかを確認します。

If so, the record is logged out and redirected to the sign in page.
Also, each time the request comes and the record is set,
we set the last request time inside its scoped session to verify timeout in the following request.

その場合、レコードはログアウトされ、サインインページにリダイレクトされます。
また、リクエストが来てレコードが設定されるたびに、
スコープセッション内の最後のリクエスト時刻を設定して、次のリクエストのタイムアウトを確認します。

出典: devise/timeoutable.rb at c82e4cf47b02002b2fd7ca31d441cf1043fc634c · heartcombo/devise · GitHub

実際にtimedout?かどうかの判定を行っているのはこの箇所ですね。

    if !env['devise.skip_timeout'] && record.timedout?(last_request_at) && !proxy.remember_me_is_active?(record)
      Devise.sign_out_all_scopes ? proxy.sign_out : proxy.sign_out(scope)
      throw :warden, scope: scope, message: :timeout
    end

Warden内部のコードを読むのは割愛しますが、概ね理解できました。

Wardenの使い方については、下記の記事が詳しかったです。

nekorails.hatenablog.com