Ryoの日記

備忘ログ

sorcery パスワードリセット〜トークンの流れ〜

Railsでsoceryのreset_passwordを使用した、パスワードリセット機能を実装したのですが、

その中で利用する「トークン」が聞きなれない言葉で、

遊戯王でそんなカードあったなーってレベルでした。

この記事では下記について、書いています。

  1. トークンはいつ作られてどう使われる?
  2. なぜトークンを使うのか
  3. トークンにユニーク制約をつける際の注意点

最初に状況の説明

  • UserMailerを作成
  • Userコントローラーとは別にpassword_resets_controllerを作成
  • ログイン画面からパスワードリセット画面へ遷移し、 メールアドレスを入力するとパスワードをリセットするためのurlのついたメールがuserに送られてくる。
  • メールを開きurlを踏むと、パスワードリセット申請画面に飛び、 パスワードを入力するとパスワードが新しく更新される。

1.トークンはいつ作られる?

パスワードリセット画面でメールアドレスを入力

f:id:Ryomimi:20210925054615p:plain

送信を押すとpasswordresetsコントローラーのcreateが走る

class PasswordResetsController < ApplicationControlle

  def create
    @user = User.find_by(email: params[:email])
    @user&.deliver_reset_password_instructions!
    redirect_to login_path, success: 'パスワードリセット手順を送信しました'
  end

この中の@user&.deliver_reset_password_instructions!メソッドが実行した時点で binding.pryで止めると、Userカラムのreset_password_tokenに文字列が入ってきたのがわかると思います。 (一番下の行あたり)

   7: def create
     8:   @user = User.find_by(email: params[:email])
     9:   binding.pry
 => 10:   @user&.deliver_reset_password_instructions!
    11:   redirect_to login_path, success: 'パスワードリセット手順を送信しました'
    12: end

[1] pry(#<PasswordResetsController>)> next
  User Update All (3.9ms)  UPDATE "users" SET "reset_password_token" = 'YLpP-sF4xiH1Y1KYpypN', "reset_password_email_sent_at" = '2021-09-25 05:45:05.692469' WHERE "users"."id" = ?  [["id", 2]]

パスワードリセット申請画面でメールアドレスを入力&送信した時点でトークンを発行したことがわかると思います。 次からそのトークンがどう使われるかみていきます。

トークンはどう使われる?

先程の@user&.deliver_reset_password_instructions!メソッドで、 トークンを作ったのと同時にUserMailerを使ってリセットの案内メールを送っています。

class UserMailer < ApplicationMailer
  def reset_password_email(user) #  このメソッドを使用
    @user = User.find(user.id)
    @url = edit_password_reset_url(@user.reset_password_token) #<=ここでトークンを使用
    mail( to: user.email,
          subject: 'パスワードリセット')
  end
end

password_resetsコントローラーのcreateで定義した@userを引数にして、 reset_password_emailメソッドを呼び出します。 ここではメールのview側で使用するために@userと@urlを定義しています。

urlを定義する際に引数にトークンを使用しています。

<h1>パスワードリセット</h1>
<p>
  <%= @user.last_name %><%= @user.first_name %> 様
  パスワード再発行のご依頼を受け付けました。
  こちらのリンクからパスワードの再発行を行ってください。
  <p><a href="<%= @url %>"><%= @url %></a>
</p>

メール側ではこんな感じに@urlを使用

できたurlがこれ↓

f:id:Ryomimi:20210925061048p:plain

1.トークンはいつ作られるで作られたトークンと同じトークンが使われています!

トークンはurlに埋め込んで使われてることが分かりますね!

urlを踏むとパスワードリセット画面が開きます↓

f:id:Ryomimi:20210925061423p:plain

画面が開く前にeditアクションを通過してます↓

def edit
    @token = params[:id]
    @user = User.load_from_reset_password_token(params[:id])

    return not_authenticated if @user.blank?
  end

ここではurlに記載されていたトークンを@tokenへ入れています。 また、トークンを発行したメールアドレスの持ち主を@userに入れています。

もしトークンがいじられていたりして、トークンがデータベースから見つからない場合は not_authenticatedが実行されるようになっています。(pageが開かないようになってる)

こうやって、urlからパスワードリセットしたいユーザーをトークンを使用して認識しています。

こうして開いたパスワードリセット画面で、新しくパスワードを入力すると、 パスワードがアップデートされます。

class PasswordResetsController < ApplicationController

def update
    @token = params[:id] 
    @user = User.load_from_reset_password_token(params[:id])  #<=ユーザーの認識
    return not_authenticated if @user.blank?                                      #<=もし確認が取れなかったらreturn
    @user.password_confirmation = params[:user][:password_confirmation]  #<=password_confirmationに格納
    if @user.change_password(params[:user][:password])               #<=パスワードをアップデートするメソッド
      redirect_to(root_path, notice: 'パスワード変更に成功しました')
    else
      flash.now[:danger] = 'パスワード変更に失敗しました'
      render :edit
    end
  end

ここまでがトークンの流れでした。

トークンはなぜ使われる?

もしトークンを使わずに001などの番号で識別していたとすると、 パスワード更新用のurlが下のようになります。

http://localhost:3000/password_resets/001/edit

ITの知識がある人だと、001の部分を002にすれば2番目のユーザーのパスワードを 弄れるのではないかと気付かれてしまう可能性があります。

そこをトークンにすることで、連想できなくさせることができるので、 トークンを使用しています。

つまり、悪用を防ぐためのトークンだったのですね。

トークンにユニーク制約をつける際の注意点

トークンは自動生成されるため、万が一同じトークンが生成されたとすると パスワードリセットで他人のパスワードを変更してしまう可能性があります。

可能性はないに等しいですが、一応トークンカラムにユニーク特性を付けておいた方が、 万が一も防ぐことができます。↓

user.rb

class User < ApplicationRecord
#省略
validates :reset_password_token, uniqueness: true

end

userモデルに上記の記述を加えると、新規登録をする際に下記のエラーが出るようになりました💦

f:id:Ryomimi:20210925093821p:plain

:reset_password_tokenカラムはパスワードリセットしてないuserには基本的にnilが入っています。 新しくユーザー登録しようとするとnilが既にあるのでuniquenessのバリデーションに引っかかっていることが原因でした。

これを解決するために、nilを許容する記述を加えます。

user.rb

class User < ApplicationRecord
#省略
validates :reset_password_token, uniqueness: true, allow_nil: true
end

これでエラー解決できました!

以上、トークンについて学んだことのまとめです。 最後までご覧いただきありがとうございました。