Active Recordのgenerates_token_for:DBカラム不要で一時的なトークンを扱う機能

29 views Post
wakairo @wakairo

generates_token_forとは

generates_token_forは、特定の目的を持つトークンを生成し、そのトークンからレコードを検索・検証するための機能です。 Rails 7.1で標準機能として導入されました。

特長

  • DBへのデータ保存不要:トークンを保存するためのデータベースのカラムやテーブルを追加せずに利用できます。
  • 目的別に分離:目的(purpose)ごとにトークンを分離できるため、別目的で生成されたトークンを使い回すことができません。
  • 失効条件を設定可能:有効期限とモデルのデータ変化による失効条件を柔軟に設定できます。
  • 改ざん耐性:署名付きのトークンのため、改ざん耐性があります。

使いどころ:一時的に有効なURLの送付

以下のような「一時的で目的が限定されたトークン付きURL」を送付する場面に適しています。

  • メールアドレス確認リンク
  • 招待リンク
  • マジックリンク(パスワードなしログイン)
  • 一時アクセスリンク

なお、APIの認証トークンのような永続的な用途には向きません。 Railsで永続的なトークンを扱いたい場合は、has_secure_tokenなどの利用を検討してください。

注意:パスワードリセットには専用APIあり

Rails 8.0以降でhas_secure_passwordを利用している場合、パスワードリセット用トークンを扱う専用APIが自動的に提供されます。そのため、パスワードリセットの用途に限っては、自分でgenerates_token_forを定義する必要はありません。(詳しくは後述の補足を参照してください)

使い方(メールアドレス確認の例)

ここでは「メールアドレス変更時の確認リンク」を題材に、基本的な使い方を説明します。

基本の流れ(定義・生成・検証)

まず、モデルにgenerates_token_forを用いて、トークンの用途名(ここでは:email_verification)を定義します。 なお、用途名が異なっていれば複数定義することも可能です。

class User < ApplicationRecord
  generates_token_for :email_verification
end

トークンを生成する際は、対象のインスタンスに対してgenerate_token_forを呼び出します。

注意:モデルで定義する際は複数形のgenerates_token_forですが、 インスタンスから呼び出す際は単数形のgenerate_token_forになる点に注意してください。

user = User.first # 対象のレコードを取得(ここでは`first`で代替)
token = user.generate_token_for(:email_verification)
# => "BAhJIi..." (生成されたトークン文字列)

生成したトークンは、例えば以下のようにURLパラメータに載せてメールなどで送付するのが典型的な流れです。

# 例:メイラー等でのURL生成
email_verifications_url(token: token)
# => "https://example.com/email_verifications?token=BAhJIi..."

受け取ったトークンからレコードを検索・検証するには、クラスメソッドのfind_by_token_forを使います。 有効なトークンならUserインスタンスが返り、無効なトークンや期限切れのトークンの場合はnilが返ります。

user = User.find_by_token_for(:email_verification, token)

有効期限による失効

定義時にexpires_inオプションを付与することで、トークンに有効期限(例:15分間)を設けることができます。

class User < ApplicationRecord
  generates_token_for :email_verification, expires_in: 15.minutes
end

モデルのデータ変化による失効

generates_token_forは、「一度状態が変わったら、古いトークンを使わせない」挙動を簡単に実装できます。 例えば、新しいメールアドレス確認用に生成されたトークンを、メールアドレスの変更が完了した後に無効化すれば、万が一流出しても悪用を防げます。

無効化の仕組みは「定義時のブロックの戻り値をトークンに埋め込み、検証時にその値が変わっていれば無効とみなす」というものです。

class User < ApplicationRecord
  generates_token_for :email_verification, expires_in: 15.minutes do
    # トークン生成時と検証時で`email`の値が異なれば(=メールアドレスが変更完了していれば)、
    # 有効期限内であっても無効になる
    email
  end
end
注意:ブロックの戻り値に機密情報を含めない

ブロック内で返した値は、改ざん耐性はあるものの、暗号化されるわけではなく、デコードすれば読める形でトークンに埋め込まれます。 そのため、ブロックの戻り値には機密情報を含めてはなりません。

補足:パスワードリセット専用API

Rails 8.0以降では、モデルでhas_secure_passwordを利用している場合、パスワードリセットトークン用の以下のメソッドがデフォルトで追加されます。

  • password_reset_token:トークン生成(インスタンスメソッド)
  • find_by_password_reset_token:トークンからの検索と検証(クラスメソッド)
  • find_by_password_reset_token!:同上(無効時に例外を発生させるバージョン)

Rails 8.1で追加されたトークン有効期限の機能

さらにRails 8.1以降では、トークンの有効期限を柔軟に扱うための機能が追加されています。

has_secure_passwordの呼び出し時にreset_tokenオプションを通して有効期限を変更できるようになりました。

class User < ApplicationRecord
  has_secure_password reset_token: { expires_in: 1.hour }
end

また、以下のメソッドが追加されました。

  • password_reset_token_expires_in:設定されている有効期限の取得(インスタンスメソッド)

参考情報

0
Raw
https://www.techtips.page/en/comments/1117