【Rails7】パスワードの再設定を実装する流れ

Railsチュートリアル第11章でアカウントの有効化の実装が完了し、ユーザーの本人確信ができるようになったので、次はパスワードを忘れた時のパスワードの再設定機能を実装していきたいと思います。

今回の記事で見ていく内容のほとんどは、アカウント有効化で見てきた内容と似通っていますが、用意すべきレイアウトの違いなど多少異なる点もあります。

その点も踏まえて、パスワード再設定の実装の流れを振り返っていきたいと思います。

あわせて読みたい
【Rails7】ユーザー新規登録時にアカウントを有効化する(メール認証) Railsチュートリアル第11章にて、メール認証によるアカウントの有効化について学んだので、その復習とメモ用に本記事を残そうと思います。 ここでは、すでにログイン機...
目次

開発環境

  • Ruby 3.1.2
  • Ruby on Rails 7.0.3
  • M1 Macbook Air 2020
  • mac OS Monterey (ver. 12.4)
  • ターミナル bash (Rosetta 2 使用)

ソースコード

GitHub
GitHub - hirokirokki0820/user-authentication Contribute to hirokirokki0820/user-authentication development by creating an account on GitHub.

パスワード再設定の仕上がりイメージ

今回実装するパスワード再設定の、システム上の動作イメージは以下の通りです。

  1. ユーザーがパスワードの再設定をリクエストする
  2. ユーザーが送信したメールアドレスをキーにしてDBからユーザーを見つける
  3. 該当ユーザーのメールアドレスがDBにある場合は、再設定用トークン(reset_token)を生成する
  4. ③に対応する再設定ダイジェスト(reset_digest)生成し、DBに保存する
  5. 再設定用トークン(reset_token)とユーザーのメールアドレスを含めた有効化リンクをメールに添付し、ユーザーに送信する
  6. ユーザーがメールを開き、有効化リンクをクリックする
  7. ⑥に該当するユーザーをDBから(メールアドレスをキーとして)検索し、保存された再設定用ダイジェスト(reset_digest)と再設定トークン(reset_token)を比較し認証する
  8. 認証に成功したら、パスワード変更用のフォームをユーザーに表示する
  9. ユーザーが任意のパスワードを入力し、更新ボタンでパスワードの再登録が完了する
  10. ちなみに、パスワード再登録の期限は2時間以内に設定(2時間を過ぎたら有効化リンクは無効となる)

トークンの生成、およびハッシュかしたトークン(ダイジェスト)のDBへの保存など、第11章のアカウント有効化と流れがよく似ていることがわかります。

とはいえ、アカウント有効化の場合は有効化リンクをクリックして認証に成功したらログインすれば良いだけですが、

今回のパスワード再設定の場合は、認証に成功したらパスワード変更用のフォームをユーザーに表示し、さらにパスワードの再登録までの処理を記述する必要があります。

実際に、以下のようなイメージでパスワードの再設定を行なっていきます。

まず、ユーザーはパスワード再設定用のフォームへ進みます。

次に、登録したメールアドレスを入力して送信ボタンを押します。

メールアドレスを登録していた場合は、そのメールアドレス宛てに有効化リンクを含んだメールが送信されます。

届いたメールを開いたら、パスワードの再設定用の有効化リンクをクリックしてパスワードの再設定に進みます。(今回はリンクの有効期限を2時間以内としています)

認証に成功したらパスワード再設定フォームが表示されるので、変更したい任意のパスワードを入力し、更新ボタンを押してパスワードの再設定を行います。

以上が、パスワード再設定の流れです。

パスワードの再設定を実装する具体的な手順

それでは、いよいよパスワードの再設定を実装していきましょう。

実装の大まかな流れは以下の通りです。

  1. PasswordResetsコントローラを生成する
  2. ハッシュ化したトークンを保存する再設定用ダイジェストreset_digest、および再設定用リンクの送信日時reset_sent_atを示すカラムを生成する
  3. パスワード再設定リクエスト用のメールアドレス入力フォームを作成する
  4. パスワード再設定用メールのテンプレート(password_reset)を作成する
  5. パスワード再設定用のメソッド、およびcreateアクションを追加する
  6. パスワードの再設定フォームを作成する(editアクションで再設定)
  7. 再設定用のパスワードを更新する

こんな感じです。

今回のパスワードの再設定は、アカウント有効化を実装する手順とよく似ているため、基本的な説明は省略する形で進めていきます。

それでは、それぞれの項目について詳しく見ていきましょう。

①PasswordResetsコントローラを生成する

まず最初に、パスワード再設定用のPasswordResetsコントローラを作成していきます。

今回はビューも扱うので、newアクションとeditアクションも一緒に生成します。

$ rails g controller PasswordResets new edit

また今回の実装では、新しいパスワードを再設定するためのフォーム、およびUserモデル内のパスワードを変更するためのフォームが必要になるので、

config/routes.rbnewcreateeditupdateのルーティングも用意しましょう。

Rails.application.routes.draw do
  root "home#index"
  get "about", to: "home#about"
  get "signup", to: "users#new"
  get "login", to: "sessions#new"
  post "login", to: "sessions#create"
  delete "logout", to: "sessions#destroy"
  resources :users, except: [:new]
  resources :account_activations, only: [:edit]
  resources :password_resets, only: [:new, :create, :edit, :update]
end

②ハッシュ化したトークンを保存する再設定用ダイジェストreset_digest、および再設定用リンクの送信日時reset_sent_atを示すカラムを生成する

続いては、パスワードの再設定に必要なカラムをUserモデルに追加していきます。

今回新たに追加するカラムは以下の2つです。

  1. 再設定用ダイジェスト…「reset_digest
  2. 再設定用リンクの送信日時…「reset_sent_at
追加するカラム名
reset_digeststring
reset_sent_atdatetime

これらを、以下のコマンドを実行して追加します。

$ rails g migration add_reset_to_users reset_digest:string reset_sent_at:datetime

マイグレーションファイルが生成されるので、中身を開いて確認します。

class AddResetToUsers < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :reset_digest, :string
    add_column :users, :reset_sent_at, :datetime
  end
end

追加するカラム名などに間違いがなければ、マイグレーションを実行してカラムを追加します。

$ rails db:migrate

③パスワード再設定リクエスト用のメールアドレス入力フォームを作成する

まずは、ログインフォームからパスワード再設定用のページに飛べるよう、下記のようにリンクを貼ります。

 ・・・

  <div class="fields">
    <%= form.label "パスワード" %>
    <div class="ui left icon input">
      <i class="lock icon"></i>
      <%= form.password_field :password, placeholder: "パスワード" %>
    </div>
    <!------------------ この行を追記 -------------------------------->
    <%= link_to ">>パスワードをお忘れですか?", new_password_reset_path %>
    <!-------------------------------------------------------------->
  </div>

  ・・・

次に、パスワード再設定リクエスト用のメールアドレス入力フォームを作成します。

作成済みのログインフォームを参考に、以下のように作成します。

<div class="ui middle aligned grid mt15">
  <div class="column">
    <h2 class="ui teal image header">
      <i class="user icon"></i>
      <div class="content">
        パスワードの再設定
      </div>
    </h2>

    <%= form_with(scope: :password_reset, url: password_resets_path, local: true, class: "ui form segment") do |form| %>
      <div class="field">
        <%= form.label "メールアドレス" %>
        <div class="ui left icon input">
          <i class="envelope icon"></i>
          <%= form.email_field :email, placeholder: "メールアドレス" %>
        </div>
      </div>
      <div>
        <%= form.submit "送信する", class: "ui teal submit button" %>
      </div>
    <% end %>

  </div>
</div>

④メール送信用(パスワード再設定用)のテンプレート(password_reset)を作成する

パスワード再設定用のメール送信テンプレートを以下のコマンドで作成します。(Userメイラーにpassword_resetメソッドを生成)

$ rails g mailer UserMailer password_reset

続いて、Userメイラーuser_mailer.rbにpassword_resetメソッドの内容を記述していきます(account_activationメソッドを参照)。

class UserMailer < ApplicationMailer

  def account_activation(user)
    @user = user
    mail to: user.email, subject: "Account activation"
  end

  def password_reset(user)
    @user = user
    mail to: user.email, subject: "Password reset"
  end
end

今回のパスワード再設定は、アカウント有効化の時と同様にDBからユーザーをメールアドレスで検索して再設定トークンを認証できるようにするために、リンクには再設定トークンとメールアドレスを両方含めておく必要があります。

(アカウントの有効化と手順は同じ)

ゆえに、パスワード再設定用の有効化リンクのパス(再設定トークンとメールアドレスを両方含んだURL)は以下のように表されます。

edit_account_activation_url(@user.reset_token, email: @user.email)

ここで、@userの再設定トークン、およびメールアドレスがそれぞれ以下の場合、

  • 再設定トークン…X9FkWNOAQru914FbR-rdAQ
  • メールアドレス…sample@example.com

パスワード再設定用リンクのURLは以下のように表されます。

http://localhost:3000/password_resets/X9FkWNOAQru914FbR-rdAQ/edit?email=sample%40example.com

ここで、%40となっているのは、@などの特殊記号はURLでは扱えないため、自動的にエスケープされたためです。

(後でparams[:email]でメールアドレスを読み込むときにエスケープは自動的に解除されます。)

それでは、上記の内容を踏まえてメールのテンプレートを作成していきましょう。

Userメイラー追加時にapp/views/user_mailerディレクトリ内に自動生成されたテンプレート(html用、text用)を以下のように編集します(文章は各自好きな文言を入れてください)。

<h1>User Authentication App</h1>
下記のリンクよりパスワードの再設定を行ってください。
</p>
<p>
<%= link_to "パスワードを再設定する", edit_password_reset_url(@user.reset_token, email: @user.email) %>
</p>
<p>このリンクの有効期限は2時間です。</p>
<p>パスワードを変更する場合は2時間以内に行ってください。</p>
下記のリンクよりパスワードの再設定を行ってください。

<%= edit_password_reset_url(@user.reset_token, email: @user.email) %>

このリンクの有効期限は2時間です。
パスワードを変更する場合は2時間以内に行ってください。

テンプレートはこんな感じでOKです。

送信メールのプレビューしたい場合は、test/mailers/previews/user_mailer_preview.rbに以下のように記述します。

class UserMailerPreview < ActionMailer::Preview

  # Preview this email at http://localhost:3000/rails/mailers/user_mailer/account_activation
  def account_activation
    user = User.first
    user.activation_token = User.new_token
    UserMailer.account_activation(user)
  end

  # Preview this email at http://localhost:3000/rails/mailers/user_mailer/password_reset
  def password_reset
    user = User.first
    user.reset_token = User.new_token
    UserMailer.password_reset(user)
  end

end

続いて、コード中にコメントアウトされた状態で記載されている以下のURLをブラウザのアドレスバーに入力します。

http://localhost:3000/rails/mailers/user_mailer/password_reset

すると、送信メールのプレビューを表示することができるはずです。(railsサーバーが起動した状態で)

⑤パスワード再設定用のメソッド、およびcreateアクションを追加する

app/models/user.rbに、パスワード再設定用のトークンを生成およびハッシュ化してDBに保存するメソッドと、再設定用のメールを送信するメソッドを記述します。

class User < ApplicationRecord
  attr_accessor :remember_token, :activation_token, :reset_token
  before_save   :downcase_email
  before_create :create_activation_digest

  ・・・

  # パスワード再設定の属性を設定する
  def create_reset_digest
    self.reset_token = User.new_token
    update_attribute(:reset_digest, User.digest(reset_token))
    update_attribute(:reset_sent_at, Time.zone.now)
  end

  # パスワード再設定のメールを送信する
  def send_password_reset_email
    UserMailer.password_reset(self).deliver_now
  end

  ・・・

end

続いて、password_resetコントローラーのcreateアクションに、パスワード再設定リクエスト時の処理を記述します。

(再設定リクエスト用フォームにメールアドレスを入力して送信ボタンを押したときの処理)

class PasswordResetsController < ApplicationController

  def new
  end

  def create
    @user = User.find_by(email: params[:password_reset][:email].downcase)
    if @user
      @user.create_reset_digest
      @user.send_password_reset_email
      flash[:notice] = "パスワード再設定用のメールを送信しました。メールをご確認の上、パスワードの再設定を行ってください。"
      redirect_to root_url
    else
      flash.now[:error] = "メールアドレスが存在しません"
      render "new", status: :unprocessable_entity
    end
  end

  def edit
  end

end

上記のcreateアクションでは、

  1. 入力したメールアドレスに該当するユーザーがDBに存在するかどうかを確認
  2. ユーザーが存在したら再設定トークンを生成し、該当ユーザーのDBに再設定用ダイジェストを保存する
  3. 同時にパスワード再設定用の有効化リンクを含んだメールを送信する
  4. フラッシュメッセージ(成功)を表示し、ルートURLにリダイレクトする
  5. ユーザーが存在しない場合はフラッシュメッセージ(失敗)を表示し、再び入力フォームをレンダリングする

といった処理を行っています。

ユーザーが存在しなかった場合は以下のように、

ユーザーが存在した場合は以下のように、

それぞれ、フラッシュメッセージが表示されるようになりました。

パスワードの再設定フォームを作成するeditアクションで再設定)

ここでは、メールの有効化リンクをクリック(トークンの認証に成功)した時に表示されるパスワードの再設定フォームを作成していきます。

<div class="ui middle aligned grid mt15">
  <div class="column">
    <h2 class="ui teal image header">
      <i class="user icon"></i>
      <div class="content">
        パスワード再設定フォーム
      </div>
    </h2>

    <%= form_with(model: @user, url: password_reset_path(params[:id]), local: true, class: "ui form segment") do |form| %>

      <%= hidden_field_tag :email, @user.email %>

      <div class="field <%= @user.errors.include?(:password) ? 'error' : '' %>">
        <%= form.label "パスワード" %>
        <div class="ui left icon input">
          <i class="lock icon"></i>
          <%= form.password_field :password, placeholder: "パスワード" %>
        </div>
        <%= render 'shared/error_messages', obj: @user, key: :password %>
      </div>

      <div class="field <%= @user.errors.include?(:password_confirmation) ? 'error' : '' %>">
        <%= form.label "パスワード(確認用)" %>
        <div class="ui left icon input">
          <i class="lock icon"></i>
          <%= form.password_field :password_confirmation, placeholder: "パスワード(確認用)" %>
        </div>
        <%= render 'shared/error_messages', obj: @user, key: :password_confirmation %>
      </div>

      <div>
        <%= form.submit "更新する", class: "ui teal submit button" %>
      </div>
    <% end %>

  </div>
</div>

ここで、パスワード再設定フォームの中には以下のコードが仕込まれていることがわかります。

<%= hidden_field_tag :email, @user.email %>

これは、隠しフィールドとしてページ内にメールアドレスを保存するためのタグ(Railsのフォームタグヘルパー)です。

では、なぜこのタグが必要なのでしょうか。

パスワードの再設定を完了し特定ユーザーのパスワードダイジェストに保存するためには、キーとなるメールアドレスが必要不可欠です。

ユーザーのメールアドレスはパスワード再設定用の有効化リンク(メールアドレスが埋め込まれている)から一度取得していますが、

今回の再設定フォームの中でフォームを一度送信してしまうと(パスワードの再設定を完了させてしまうと)、メールアドレスの情報が消えてしまいます。

(再設定後のパスワードを更新したくても、キーとなるメールアドレスがないのでupdateアクションで保存先のユーザーを参照できなくなってしまいます)

そこで、フォーム送信後もメールアドレスを保持しておくためにhidden_field_tagを使い、隠しフィールドとしてメールアドレス情報をページ内に生成(保存)しておく必要があったのです。

こうすることで、フォーム送信後にupdateアクションにて、メールアドレスをキーとして変更後のパスワードを特定ユーザーのパスワードダイジェストに保存(更新)できるようになります。

以上の流れを踏まえて、editupdateアクション用に以下のメソッドを追加します。

class PasswordResetsController < ApplicationController
  before_action :get_user, only: [:edit, :update]
  before_action :valid_user, only: [:edit, :update]

 ・・・

  private
    # メールアドレスをキーに該当するユーザー情報をDBから取得する
    def get_user
      @user = User.find_by(email: params[:email])
    end

    # 正しいユーザーかどうか確認する
    def valid_user
      unless (@user && @user.activated? && @user.authenticated?(:reset, params[:id]))
        redirect_to root_url
      end
    end

end

メールアドレスをキーとして該当のユーザー情報を取得するget_userメソッド、そして正統なユーザーかどうか(ユーザーが存在する、かつトークン認証済みである)を判定するvalid_userメソッド、

これらを、before_actioneditアクション、updateアクションに適用してやります。

⑦再設定用のパスワードを更新する

最後に、再設定したパスワードを更新するためのupdateアクションを記述していきます。

今回のupdateアクションでは以下2つのケースを考慮する必要があります。

  1. パスワード再設定の有効期限が切れていないかどうか
  2. 新しいパスワードが空文字になっていないかどうか

①の「パスワード再設定の有効期限が切れていないかどうか」については、editupdateアクションにそれぞれ以下のメソッドを用意することで対応します。

before_action :check_expiration, only: [:edit, :update]

# 期限切れかどうかを確認する
def check_expiration
  if @user.password_reset_expired?
    flash[:danger] = "Password reset has expired."
    redirect_to new_password_reset_url
  end
end

有効期限を設定するメソッドはapp/models/user.rbに以下のように記述します。

class User < ApplicationRecord

  ・・・

  # パスワード再設定の有効期限が切れている場合はtrueを返す
  def password_reset_expired?
    reset_sent_at < 2.hours.ago
  end

 private
  
 ・・・

end

ここで、以下の条件式

reset_sent_at < 2.hours.ago

の「<」について、今回の場合は「〜より小さい」ではなく「〜より以前(早い)」と読むようにし、

「パスワード再設定メールの送信時刻(reset_sent_at)が、現在時刻より2時間以上前(早い)の場合」

と考えるとわかりやすいでしょう。

②の「新しいパスワードが空文字になっていないかどうか」については、

params[:user][:password].blank?

で、受け取ったパラメータの値が空白だった場合にエラーを表示するよう記述することで対応します。

if params[:user][:password].blank?
  @user.errors.add(:password, :blank)
  render "edit", status: :unprocessable_entity
  ・・・
end

最終的に、password_resetコントローラーは以下のようになります。

class PasswordResetsController < ApplicationController
  before_action :get_user, only: [:edit, :update]
  before_action :valid_user, only: [:edit, :update]
  before_action :check_expiration, only: [:edit, :update]

  def new
  end

  def create
    @user = User.find_by(email: params[:password_reset][:email].downcase)
    if @user
      @user.create_reset_digest
      @user.send_password_reset_email
      flash[:notice] = "パスワード再設定用のメールを送信しました。メールをご確認の上、パスワードの再設定を行ってください。"
      redirect_to root_url
    else
      flash.now[:error] = "メールアドレスが存在しません"
      render "new", status: :unprocessable_entity
    end
  end

  def update
    if params[:user][:password].blank?
      @user.errors.add(:password, :blank)
      render "edit", status: :unprocessable_entity
    elsif @user.update(user_params)
      log_in @user
      flash[:notice] = "パスワードが更新されました"
      redirect_to @user
    else
      render "edit", status: :unprocessable_entity
    end
  end

  def edit
  end

  private
    def user_params
      params.require(:user).permit(:password, :password_confirmation)
    end

    def get_user
      @user = User.find_by(email: params[:email])
    end

    # 正しいユーザーかどうか確認する
    def valid_user
      unless (@user && @user.activated? && @user.authenticated?(:reset, params[:id]))
        redirect_to root_url
      end
    end

    # トークンが期限切れかどうか確認する
    def check_expiration
      if @user.password_reset_expired?
        flash[:error] = "有効期限を過ぎているため、パスワードの再設定ができません。"
        redirect_to new_password_reset_url
      end
    end

end

それでは、実際にパスワード入力時にバリデーション等が機能するか試してみましょう。

パスワードを空欄の状態、もしくはスペースのみ入力して更新ボタンを押した場合、

こちらは、パスワードが不一致の場合、

どちらもしっかりバリデーションが効いていますね。

最後に、パスワードの更新に成功した場合、

フラッシュメッセージが表示されるとともに、ユーザー詳細ページにリダイレクトされました。

これで、パスワード再設定の実装は完了です。

本番環境でのメール送信(SendGrid)

パスワード再設定の実装ができたら、実際に本番環境でメールが送信できるか確認してみましょう。

本番環境(および開発環境)におけるメールサーバーの設定方法は下記記事を参考にしてみてください。

あわせて読みたい
【Rails7】開発環境、本番環境でメール送信できるようにする(SendGrid, Gmail) Railsで作ったアプリからメールを送信したい場合、外部のメールサーバーを利用してメールを送ることになります。 今回は、Railsのアプリ制作でよく取り上げられているメ...

新規ユーザー登録時に、以下のようなメールが届くかと思います。

(Gメールアドレス宛てに届いたhtmlメールの例です。メールサーバーをSendGridにした場合、送信元に以下のように「sendgrid.net 経由」と記載されているかと思います)

パスワード再設定用のリンクをクリックして、問題なければOKです。

以上です。

お疲れ様でした。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

愛知の34歳。無職で暇になり始めたプログラミング(Ruby on Rails)の忘備録をまとめたブログです。最近は別にやりたいことができたのでプログラミングほぼやっていません。気が向いたらまた再開するかも。僕の日常はメインブログの方で更新しています。

コメント

コメントする

目次