sorcery パスワードリセット〜トークンの流れ〜
Railsでsoceryのreset_passwordを使用した、パスワードリセット機能を実装したのですが、
その中で利用する「トークン」が聞きなれない言葉で、
遊戯王でそんなカードあったなーってレベルでした。
この記事では下記について、書いています。
最初に状況の説明
- UserMailerを作成
- Userコントローラーとは別にpassword_resets_controllerを作成
- ログイン画面からパスワードリセット画面へ遷移し、 メールアドレスを入力するとパスワードをリセットするためのurlのついたメールがuserに送られてくる。
- メールを開きurlを踏むと、パスワードリセット申請画面に飛び、 パスワードを入力するとパスワードが新しく更新される。
1.トークンはいつ作られる?
- トークン発行までの流れ *
パスワードリセット画面でメールアドレスを入力
送信を押すと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がこれ↓
1.トークンはいつ作られる
で作られたトークンと同じトークンが使われています!
トークンはurlに埋め込んで使われてることが分かりますね!
urlを踏むとパスワードリセット画面が開きます↓
画面が開く前に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モデルに上記の記述を加えると、新規登録をする際に下記のエラーが出るようになりました💦
:reset_password_token
カラムはパスワードリセットしてないuserには基本的にnilが入っています。
新しくユーザー登録しようとするとnil
が既にあるのでuniquenessのバリデーションに引っかかっていることが原因でした。
これを解決するために、nilを許容する記述を加えます。
user.rb class User < ApplicationRecord #省略 validates :reset_password_token, uniqueness: true, allow_nil: true end
これでエラー解決できました!
以上、トークンについて学んだことのまとめです。 最後までご覧いただきありがとうございました。