11
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

Organization

【Rails】パスワード変更(トークンがどのように使用されているのか)

はじめに

sorceryのパスワードリセット機能の実装を行いました。
以下3点について記述しましたので、ご興味あれば見てってください。

1. sorceryのパスワードリセット機能の実装方法
2. ActionMailerの使い方
3. passwordリセットトークンがリクエスト/レスポンス間でどのように使われているか

1. sorceryのreset_passwordをインストール

モジュールの実装とマイグレーションファイルの作成

  • rails g sorcery:install reset_password --only-submodulesを実行し、password reset用のモジュールのインストール、マイグレーションファイルを作成する。
$ rails g sorcery:install reset_password --only-submodules
Running via Spring preloader in process 99290
        gsub  config/initializers/sorcery.rb
      insert  app/models/user.rb
      create  db/migrate/20200419075054_sorcery_reset_password.rb
  • sorceryの定義ファイルconfig/initializers/sorcery.rbに、password_resetサブモジュールを使用するための記述が自動で行われている。
config/initializers/sorcery.rb
Rails.application.config.sorcery.submodules = [:reset_password]

Userモデルにallow_nil: trueを追加

  • password_reset_token にユニーク制約とallow_nil: trueを付与しておく。
    • パスワードを変更した際、reset_password_tokenがnilになるのでユニーク制約に引っかかってしまう。そこで、allow_nil:trueを加えることでnilを許可しておく。
user.rb
validates :reset_password_token, uniqueness: true, allow_nil: true

2. パスワードリセット用のMailerを作成

application_mailer.rbの設定

メールの差し出し元とMailerビューの共通レイアウトとなるファイルを指定。

  • ここではメールの差出元をadmin@example.comとしておく。
  • レイアウトファイルはapp/views/layouts/mailer.html.erbapp/views/layouts/mailer.text.erbになる。
app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
  # メールの差し出し元
  default from: 'admin@example.com'
  layout 'mailer'
end

パスワードリセットに使用するメイラーの設定

  • rails g mailer UserMailer change_password_emailを実行し、パスワードリセットに使用するメイラーUserMailerを作成。

  • config/initializers/sorcery.rbの中で、sorceryのパスワードリセットに使用するActionMailerとして、UserMailerを指定する。

    • user.reset_password_mailer = UserMailerのUserMailerから、app/mailers/user_mailer.rbが推測される。
config/initializers/sorcery.rb
Rails.application.config.sorcery.submodules = [:reset_password, blabla, blablu, ...]

Rails.application.config.sorcery.configure do |config|
  config.user_config do |user|
    user.reset_password_mailer = UserMailer # パスワードリセット用のMailerにUserMailerが指定されている
  end
end

パスワードリセット用のメソッドを記述

  • メーラー(user_mailer.rb)を編集し、パスワードリセット用メールを送信するためのメソッドを作成し、引数にuserパラメータを追加する。
  • メール(view)内に表示させる情報やメールの送信先を設定。
  • Mailerクラス#メソッドコントローラ#アクションと似た動きをする。
    UserMailerクラスのchange_password_emailメソッドから、user_mailer/change_password_email.html.erbというメイラービューを推測し、それを呼び出す。
app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer

  # change_password_emailメソッドなので、change_password_email.〇〇のビューがメールのフォーマットになる
  # コントローラの場合と同様、メイラーのメソッド内で定義されたインスタンス変数はメイラーのビューで使える。
  def change_password_email(user)
    @user = User.find(user.id)
    @url  = edit_password_change_url(@user.reset_password_token)
    mail(to: user.email,
         subject: 'パスワードリセット')
  end
end

メイラービューの設定

これが実際のメール本文となる。

app/views/user_mailer/change_password_email.html.erb
<%= @user.name %><p>以下のリンクからパスワードの再発行を行ってください。</p>

<p><a href="<%= @url %>"><%= @url %></a></p>
app/views/user_mailer/change_password_email.text.erb
<%= @user.name %>様
===========================================

以下のリンクからパスワードの再発行を行ってください。
<%= @url %>

3. コントローラの設定

パスワードリセットを行うためのコントローラ#アクションを、rails g controller PasswordChanges new create edit updateで追加。

app/controllers/password_changes_controller.rb
class PasswordChangesController < ApplicationController
  skip_before_action :require_login

  # パスワードリセット申請フォーム用のアクション
  def new; end

  # パスワードリセットをリクエストするアクション
  # ユーザーがパスワードのリセットフォームにemailを入力し、送信したときにこのアクションが実行される
  def create
    # form_withで送られてきたemailをparamsで受け取る
    @user = User.find_by(email: params[:email])
    # DBからデータを受け取れていれば、パスワードリセットの方法を記載したメールをユーザーに送信する(ランダムトークン付きのURL/有効期限付き)
    @user&.deliver_reset_password_instructions! 
    # 上記は、@user.deliver_reset_password_instructions! if @user と同じ

    # フォームに入力したemailがアプリ(DB)内に存在するか否かを問わず、リダイレクトして成功メッセージを表示させる。
    # DBに存在した時だけ成功メッセージを表示させると、DB内にそのemailが存在するかどうかを悪意ある第三者でさえも確認できてしまう。
    redirect_to login_path, success: "成功しました"
  end

  # パスワードリセットフォームページへ遷移するアクション
  def edit
    # postされてきた値を取得
    @token = params[:id]
    # リクエストで送信されてきたトークンを使って、ユーザーの検索を行い, 有効期限のチェックも行う。
    # トークンが見つかり、有効であればそのユーザーオブジェクトを@userに格納する
    @user = User.load_from_reset_password_token(params[:id])
    # @userがnilまたは空の場合、not_authenticatedメソッドを実行する
    return not_authenticated if @user.blank?
  end

  # ユーザーがパスワードのリセットフォームを送信(新しいパスワードの入力)したときに実行される
  def update
    @token = params[:id]
    @user = User.load_from_reset_password_token(@token)
    return not_authenticated if @user.blank?
    # password_confirmation属性の有効性を確認
    @user.password_confirmation = params[:user][:password_confirmation]
    # change_passwordメソッドで、パスワードリセットに使用したトークンを削除し、パスワードを更新する
    if @user.change_password(params[:user][:password])
      redirect_to login_path, success: "成功しました"
    else
      flash.now[:danger] = "失敗しました"
      render :edit
    end
  end
end

4. パスワードリセット用のフォーム作成

パスワードリセット申請フォーム

app/views/password_changes/new.html.erb
<%= form_with url: password_changes_path, local: true do |f| %>
    <%= f.label :email, "メールアドレス" %>
    <%= f.email_field :email %>
    <%= f.submit "送信する" %>
<% end %>

パスワードリセット用のフォーム

app/views/password_changes/edit.html.erb
      <%= form_with model: @user, url: password_changes_path(@token), local: true do |f| %>

          <%= f.label :email %><br>
          <%= @user.email %>

          <%= f.label :password %><br>
          <%= f.password_field :password %>

          <%= f.label :password_confirmation %><br>
          <%= f.password_field :password_confirmation %>

            <%= f.submit %>
      <% end %>

5. トークンはどのように動いて、どのように使用されているのか

パスワードリセットの一連の流れの中で、どのようにトークンが使用されているのか?

1. createアクション実行時のログを確認

  • 新しく発行されたトークンreset_password_token
  • パスワードリセット用メールが送信された日時change_password_email_sent_at
    が新しく更新されている。
User Update All (4.9ms)  UPDATE "users" SET "reset_password_token" = 'h1mJ......2i', 
"change_password_email_sent_at" = '2020-04-29 17:09:12.464274' WHERE "users"."id" = ?  [["id", 43]]

2. アプリケーションから送信されてきたメールを確認

URLにreset_password_tokenが埋め込まれている事が分かる。(h1mj~2iの部分)

スクリーンショット 2020-04-29 17.34.24.png

3. editアクションでデバッグしてみる

  • idreset_password_tokenのデータが格納されている。
    params[:id]User.load_from_reset_password_token(params[:id])のparams[:id]でトークンを取得し、そのトークンが、DBに保存されているトークンと合致するかを確認する。(updateアクションも同様)
    12: def edit
    13: 
    14:   binding.pry
    15:   
 => 16:   @token = params[:id]
    17:   @user = User.load_from_reset_password_token(params[:id])
    18:   return not_authenticated if @user.blank?
    19: end

[1] pry(#<PasswordChangesController>)> params
=> <ActionController::Parameters {"controller"=>"password_changes",
 "action"=>"edit", "id"=>"h1mJZ.....2i"} permitted: false>

参考

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
11
Help us understand the problem. What are the problem?