こんにちは!kossyです!
今回は認証機能を提供するGem「devise」のrememberableのソースコードを追ってみたので、
備忘録としてブログに残してみたいと思います。
remeberableとは?
Webサイトにログインする時に、「ログイン状態を保持する」というチェックボックスを見たことがある方も多いと思いますが、
deviseのremeberableを使うと、上記の機能を簡単に実装することができます。
その仕組みを、ソースコードを追って理解するのが本記事の狙いです。
rememberableの機能を使うための準備
remeberableの機能を有効にするには、deviseを利用するモデルに、「rememberable」モジュールを追加するのと、remember_created_atカラム を追加することです。
class User < ActiveRecord::Base devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable end
class DeviseUsers < ActiveRecord::Migration[6.0] def change create_table(:users) do |t| # 省略 ## Rememberable t.datetime :remember_created_at # 省略 end end end
これで準備完了です。
ソースコードを追う
まずはコメントアウト部分を訳してみます。
Rememberable manages generating and clearing token for remembering the userfrom a saved cookie.
Rememberable also has utility methods for dealing with serializing the user into the cookie and back from the cookie,
trying to lookup the record based on the saved information.
You probably wouldn't use rememberable methods directly, they are used mostly internally for handling the remember token.Rememberableは、保存されたCookieからユーザーを記憶するためのトークンの生成とクリアを管理します。
Rememberableには、ユーザーをCookieにシリアル化し、Cookieから戻すためのユーティリティメソッドもあります。
保存された情報に基づいてレコードを検索しようとしています。
おそらく、rememberableに定義されたメソッドを直接使用することはないでしょう。
それらは、ほとんどの場合、記憶トークンを処理するために内部的に使用されます。出典: https://github.com/heartcombo/devise/blob/master/lib/devise/models/rememberable.rb#L9
なるほど、開発者自らrememberableのメソッドを呼び出すことは稀だそうです。
そうなるとより内部の実装に興味が湧いてきました。開発者の性でしょうか。
Options以下の文章も訳してみます。
Rememberable adds the following options in devise_for:
* +remember_for+: the time you want the user will be remembered without asking for credentials.
After this time the user will be blocked and will have to enter their credentials again.
This configuration is also used to calculate the expires time for the cookie created to remember the user.
By default remember_for is 2.weeks.
* +extend_remember_period+: if true, extends the user's remember period when remembered via cookie. False by default.
* +rememberable_options+: configuration options passed to the created cookie.Rememberableは、devise_forに次のオプションを追加します。
* + Remember_for +:クレデンシャルを要求せずにユーザーに記憶させたい時刻。
この時間の後、ユーザーはブロックされ、資格情報を再度入力する必要があります。
この構成は、ユーザーを記憶するために作成されたCookieの有効期限を計算するためにも使用されます。
デフォルトでは、remember_forは2週間です。
* + extend_remember_period +:trueの場合、Cookieを介して記憶されるときにユーザーの記憶期間を延長します。 デフォルトではFalseです。
* + Rememberable_options +:作成されたCookieに渡される構成オプション。出典: https://github.com/heartcombo/devise/blob/master/lib/devise/models/rememberable.rb#L16
Userモデルに上記3つのメソッドが追加されているようなので、コンソールで試してみます。
remember_for
$ rails c $ User.remember_for => 2 weeks $ User.remember_for.class => ActiveSupport::Duration
デフォルトで2週間とのことだったので、 2 weeks が返り値になっていました。クラスはActiveSupport::Durationクラスのインスタンスです。
remeber_forは、config/iniailizers/devise.rbで値を変更することができます。
# config/initializers/devise.rb # ==> Configuration for :rememberable # The time the user will be remembered without asking for credentials again. (訳: 認証情報を再度要求せずにユーザーが記憶される時間) config.remember_for = 1.days # コメントアウトを外して、1 daysに変更
この状態でrails cを再度実行してremember_forメソッドを実行してみます。
$ rails c $ User.remember_for => 1 day
devise.rbで設定した値が正しく反映されています。
ついでにremember_forの定義元を確認してみます。
$ User.method(:remember_for).source_location => ["/usr/local/bundle/gems/devise-4.8.0/lib/devise/models.rb", 37]
ソースコードはこちら。devise/models.rb at main · heartcombo/devise · GitHub
class_evalを使って、config/initializers/devise.rbの内容を元に動的にメソッドを定義するメソッドでした。
extend_remember_period
こちらもコンソールで試してみます。
$ User.extend_remember_period => false
ソースコードはこちら。devise/rememberable.rb at main · heartcombo/devise · GitHub
こちらもconfigファイルから設定値を変更することができます。
# config/initializers/devise.rb # If true, extends the user's remember period when remembered via cookie.(訳: trueの場合、Cookieを介して記憶されるときにユーザーの記憶期間を延長します) config.extend_remember_period = true
$ User.extend_remember_period => true
Rememberable_options
config/initializers/devise.rbのコメントアウトを読むと、
Options to be passed to the created cookie.
For instance, you can set
secure: true in order to force SSL only cookies.作成されたCookieに渡されるオプション。
たとえば、secure: true
$ User.rememberable_options => {} # secure: true(ついでにhttponly: trueも)を追加した後 $ User.rememberable_options => {:secure=>true, :httponly=>true}
httponlyは、JavaScriptからのcookie操作をできなくするオプションです。クロスサイトスクリプティング (XSS) 攻撃を緩和するのに役立ちますので、
JavaScriptからcookieを操作したいニーズがない場合は迷わず付与すべきでしょう。
remember_me!
ソースコードはこちら。devise/rememberable.rb at main · heartcombo/devise · GitHub
「認証状態を保持する」にチェックが入った状態でログイン処理が行われた場合に呼び出されるメソッドです。
def remember_me! self.remember_token ||= self.class.remember_token if respond_to?(:remember_token) self.remember_created_at ||= Time.now.utc save(validate: false) if self.changed? end
remember_tokenメソッドが定義されていて、deviseを利用しているモデルのremember_tokenカラムの値がnilの場合は、remember_tokenの値を代入します。
現在時刻もremember_created_atに値が入っていなければ代入し、selfのattrのいずれかに変更があればvalidationをskipさせてレコードを保存していました。
呼び出し箇所はこちらです。
devise/rememberable.rb at c82e4cf47b02002b2fd7ca31d441cf1043fc634c · heartcombo/devise · GitHub
# Remembers the given resource by setting up a cookie def remember_me(resource) return if request.env["devise.skip_storage"] scope = Devise::Mapping.find_scope!(resource) resource.remember_me! cookies.signed[remember_key(resource, scope)] = remember_cookie_values(resource) end
forget_me!
devise/rememberable.rb at main · heartcombo/devise · GitHub
# If the record is persisted, remove the remember token (but only if # it exists), and save the record without validations. def forget_me! return unless persisted? self.remember_token = nil if respond_to?(:remember_token) self.remember_created_at = nil if self.class.expire_all_remember_me_on_sign_out save(validate: false) end
レコードが保存されていなければ、remember_tokenとremember_created_atの値を条件を見つつnilにして、バリデーションをskipして保存しています。
こちらの呼び出し箇所はこちら。
devise/rememberable.rb at c82e4cf47b02002b2fd7ca31d441cf1043fc634c · heartcombo/devise · GitHub
rememberable_value
devise/rememberable.rb at c82e4cf47b02002b2fd7ca31d441cf1043fc634c · heartcombo/devise · GitHub
def rememberable_value if respond_to?(:remember_token) remember_token elsif respond_to?(:authenticatable_salt) && (salt = authenticatable_salt.presence) salt else raise "authenticatable_salt returned nil for the #{self.class.name} model. " \ "In order to use rememberable, you must ensure a password is always set " \ "or have a remember_token column in your model or implement your own " \ "rememberable_value in the model with custom logic." end end
こちらは、Userモデルにインスタンスメソッドとして定義して、binding.pryでREPLで試してみます。
$ rails c $ User.first.rememberable_value # ここからREPL $ respond_to?(:remember_token) => false # remember_tokenカラムがないためfalseが返る $ respond_to?(:authenticatable_salt) => true $ (salt = authenticatable_salt.presence) => "$2a$12$x6w/bZliXjk9NtMh23BGJ." $ encrypted_password => "$2a$12$x6w/bZliXjk9NtMh23BGJ.GW64j1r8Mxg6clDGAcGIRxv6mc6J18q"
下記記事でも言及されていますが、saltはencrypted_passwordの前半部分が返り値になっています。
Devise3.2.2 のデフォルト設定では、Rememberable の remember_token のカラムがないのでソースを解読してみた | EasyRamble
つまり、rememberable_valueの返り値は、remember_tokenカラムがあればremember_tokenの値が、ない場合はencrypted_passwordの前半部分の値が返るメソッドでした。
remember_me?
devise/rememberable.rb at c82e4cf47b02002b2fd7ca31d441cf1043fc634c · heartcombo/devise · GitHub
def remember_me?(token, generated_at) # TODO: Normalize the JSON type coercion along with the Timeoutable hook # in a single place https://github.com/heartcombo/devise/blob/ffe9d6d406e79108cf32a2c6a1d0b3828849c40b/lib/devise/hooks/timeoutable.rb#L14-L18 if generated_at.is_a?(String) generated_at = time_from_json(generated_at) end # The token is only valid if: # 1. we have a date # 2. the current time does not pass the expiry period # 3. the record has a remember_created_at date # 4. the token date is bigger than the remember_created_at # 5. the token matches generated_at.is_a?(Time) && (self.class.remember_for.ago < generated_at) && (generated_at > (remember_created_at || Time.now).utc) && Devise.secure_compare(rememberable_value, token) end
このメソッドの呼び出し箇所はこちら。
devise/rememberable.rb at c82e4cf47b02002b2fd7ca31d441cf1043fc634c · heartcombo/devise · GitHub
def remember_me_is_active?(resource) return false unless resource.respond_to?(:remember_me) scope = Devise::Mapping.find_scope!(resource) _, token, generated_at = cookies.signed[remember_key(resource, scope)] resource.remember_me?(token, generated_at) end
引数のgenerated_atはcookies.signedの返り値のようです。
The token is ...の部分を意訳します。
The token is only valid if:
1. we have a date
2. the current time does not pass the expiry period
3. the record has a remember_created_at date
4. the token date is bigger than the remember_created_at
5. the token matchesトークンは次の場合にのみ有効です。
1.generated_atがDateクラスのインスタンスであること
2.現在の時刻が有効期限を過ぎていないこと
3.レコードにremember_created_at日付があること
4.トークンの日付がremember_created_atよりも大きいこと
5.トークンが一致すること出典: devise/rememberable.rb at c82e4cf47b02002b2fd7ca31d441cf1043fc634c · heartcombo/devise · GitHub
上記の条件判定の結果を「認証状態を保持しているかどうか」という値としているメソッドでした。
おわりに
remember_token周りの実装の意図が最後までよくわかりませんでした、、、
が、どうやらOmniauthを使ってSNS認証でユーザー機能を実装しようとする際に、deviseのfriendly_tokenを使ってダミーのパスワードを作らないとRemember me機能が使えなくなる問題があるようで、
その対策のためにremember_tokenが存在するようです。
大いに参考にさせていただいた記事・サイト
この場を借りて御礼を申し上げます。
Devise3.2.2 のデフォルト設定では、Rememberable の remember_token のカラムがないのでソースを解読してみた | EasyRamble
deviseのRemberable機能をデータベースにカラムを用意して使用する方法 - higan96技術メモ