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

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

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



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

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

- `rails g sorcery:install reset_password --only-submodules`を実行し、password reset用のモジュールのインストール、マイグレーションファイルを作成する。

```terminal
$ 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サブモジュールを使用するための記述が自動で行われている。

```ruby: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を許可しておく。
  
```ruby: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.erb`と`app/views/layouts/mailer.text.erb`になる。

```ruby: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`が推測される。
 
```ruby: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`というメイラービューを推測し、それを呼び出す。
 
```ruby: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
```

#### メイラービューの設定
 これが実際のメール本文となる。

 ```erb:app/views/user_mailer/change_password_email.html.erb
<%= @user.name %>様


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

<p><a href="<%= @url %>"><%= @url %></a></p>
```

```erb:app/views/user_mailer/change_password_email.text.erb
<%= @user.name %>様
===========================================

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

## 3. コントローラの設定

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

```ruby: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. パスワードリセット用のフォーム作成

#### パスワードリセット申請フォーム 
 
```ruby: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 %>
```

#### パスワードリセット用のフォーム
 
```ruby: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**  
が新しく更新されている。

```terminal
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の部分)

<img width="515" alt="スクリーンショット 2020-04-29 17.34.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/576318/b6b8a44e-e67f-2978-29aa-b111028a8ec9.png">   
   

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

- **id**に**reset_password_token**のデータが格納されている。  
→`params[:id]`、`User.load_from_reset_password_token(params[:id])`のparams[:id]でトークンを取得し、そのトークンが、DBに保存されているトークンと合致するかを確認する。(updateアクションも同様)

```terminal:
    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>
```

## 参考

- [sorcery公式](https://github.com/NoamB/sorcery/wiki/Reset-password)
- [sorceryメソッド](https://github.com/Sorcery/sorcery/blob/7c5fee441e03779ee2888ecef317440cd10a17e7/lib/sorcery/controller.rb#L76-L78)

- [ActionMailer::Base < AbstractController::Base](https://api.rubyonrails.org/classes/ActionMailer/Base.html)
- [Action Mailer の基礎/Railsガイド](https://railsguides.jp/action_mailer_basics.html)