Post

Deviseやsorceryを使わないやり方で、パスワードリセット機能の流れを理解する

SorceryのReset passwordモジュールを使って、パスワードリセット機能を実装するという課題だったが、sorceryの手順でなんとなくできたけど、メソッドの意味や流れがなかなかうまく理解できなかった。

それで補充情報を検索してみたら、gemを使わないやり方のyoutube解説動画を見つけた。

必要最小限のコードでパスワードリセット機能を作りながら、大体の流れを理解できてから、 それを参照にして、soceryの動きもなんとなくイメージできるようになった。

1. まずサンプルアプリの土台を作る

  • Rails 6.1のsigned_id機能を使うので、Rails 6.1以上のバージョンを使う必要
  • 簡単なユーザー登録、ログインとログアウト機能を作る。
    • Userモデルのコラムはemailとpassword_digestだけでも良い
    • has_secure_password使用
  • password_resets_controllerのnewアクション、そしてリセット申請用のnewページとパスワード更新用のeditページを作成
  • password_resets部分のroutes設定
  • password_mailerresetというメソッドを作る

ここまで実現できた流れは、パスワードリセットリンクをクリックして、newページが表示され、フォームからemailを記入するまで。

2. createアクション

1
2
3
4
5
6
7
  def create
    @user = User.find_by(email: params[:email])   # フォームで記入したemailでユーザーを特定する
    if @user.present?                                                   
      PasswordMailer.with(user: @user).reset.deliver_later  # ユーザーが存在する場合、PasswordMailerでresetの案内メールを送信する
    end
    redirect_to root_path, notice: "パスワードリセット手順を送信しました"  # ユーザーが見つからなかった場合でも、送信したと伝える
  end

PasswordMailer.with(user: @user).reset.deliver_laterについて

with(user:@user)でMailerに引数を渡して、Mailerでparams[:user]を使えるようになる。後でこのuserのglobal_idを使ってtokenを作る。

今すぐ送信するdeliver_nowより、deliver_laterを使うのは、メール送信するのは時間かかるので、とりあえずcontrollerのアクションを実行完了させて、 メールが送信したとユーザーに知らせることを優先。controllerのアクションが実行完了してから、ActionJobでメール送信を行う。

3. PasswordMailerの動き

resetアクション

1
2
3
4
5
6
7
  def reset
    @token = params[:user].signed_id(purpose: "password reset", expires_in: 15.minutes)   
# リセットの目的と15分の有効期限を引数で設定し、userのsigned_idを生成して、リセット用のURLに渡すための引数@tokenに代入する

    mail(to:  params[:user].email,
      subject:  'パスワードリセットのご案内' )
  end

signed_idについて

signed_idはRails 6.1での新機能で、モデルのインスタンスのglobal_idを暗号化したハッシュ値(署名)。 Rails consoleで確認してみると、こんな感じ

1
2
3
>user = User.last
>user.to_global_id.to_s
=> "gid://sample-app/User/3"   # userインスタンスを定義するURI 

メール本文で使うURL

password_reset_edit_url(token: @token)でurlにtokenを渡す。後でパスワードリセットの時、このtokenをfind_signedメソッドに渡してuserを特定することができる。

ちなみに、ここのURLヘルパは絶対パスを生成する_urlを使うべき。_pathだと、相対パスになるので、外からアクセスできないため。

configでMailerのhostを設定する

Mailerは、HTTPリクエストとは無関係で、どのドメインを使ってメールを送信するのか、Mailer自体はわからないため、host情報を明示する必要。

hostパラメータを指定しないままで、リセットを申請したら、 Missing host to link to! Please provide the :host parameter, set default_url_options[:host]というエラーが出る。

それで、hostを設定する。(課題ではconfigというgemを使って一元管理するのが推奨)

1
  config.action_mailer.default_url_options = { host: "localhost:3000" }

ここまで実現できた流れは、パスワードリセット申請を提出して、対象のemailアドレスにリセット用のURLを告知する案内メールを送る。

4. editアクションとupdateアクション(新しいパスワードの設定と更新)

editアクション password_reset_edit_url(token: @token)で生成したリンクをクリックしたら、editアクションが動く

1
2
3
4
5
  def edit
    @user = User.find_signed!(params[:token], purpose: "password reset")  # もし15分のtoken期限が過ぎた場合、token(つまりsigned_id)がnilになる
  rescue ActiveSupport::MessageVerifier::InvalidSignature       # 発生した例外を処理する
    redirect_to  login_path, alert: "トークンの有効期限が切れました。再度申請してください。"
  end

まずは、User.find_signed!(params[:token], purpose: "password reset")でリセットを申請したユーザーを特定する。

もし15分のtoken期限が過ぎた場合、ActiveSupport::MessageVerifier::InvalidSignatureエラーが起こる。 その例外の処理として、ログインページにredirectして、再度申請することを提示する。

また、editページのform_withにはurl: password_reset_edit_path(token: params[:token]) を渡す必要。

updateアクション

新しいパスワードを記入して、提出したら、updateアクションが動く。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  def update
    @user = User.find_signed!(params[:token], purpose: "password reset")

    if @user.update(password_params)
      redirect_to login_path, notice: "パスワードは更新しました"
    else
      render :edit
    end
  end

  private

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

これで、パスワードリセットリンクをクリックして、tokenが検証通過されてから、パスワード更新することができる。

このやり方はsorceryの難しいメソッドより、だいぶわかりやすくなったので、 もしsorceryの挙動が理解しづらいと感じたら、まずこれを参照にして、sorceryのコードを改めて理解するのが良いかも。

This post is licensed under CC BY 4.0 by the author.