[Ruby on Rails]sorceryによる認証 – (7)APIでのパスワードリセット
はじめに
以前の記事でsorceryを使用してのAPIでの認証について書きました。今回はAPIでのパスワードリセットを試してみました。
今回作成した機能の概要
sorceryを使用したパスワードリセットについては[Ruby on Rails]sorceryによる認証 – (4)パスワードリセットで以前書きました。今回はこの内容を踏襲し、以前の記事で作成したsorcery_api_sampleに機能を追加する形でAPIとして実装しました。なので先に以前の記事を一読することをお勧めします。
以下、今回追加した機能の概要です。
usersテーブルへのカラム追加
ユーザ情報を保持するにusersテーブルに、カラムを追加しました。最終的には以下のようなレイアウトとなります。
項目名 | 型 | 概要 | 追加 |
---|---|---|---|
id | INTEGER | ユーザのID | |
varchar | メールアドレス | ||
name | varchar | ユーザ名 | |
crypted_password | varchar | 暗号化したパスワード | |
salt | varchar | 暗号化時のsalt | |
created_at | datetime | 作成日時 | |
updated_at | datetime | 更新日時 | |
reset_password_token | varchar | パスワードリセット時のトークン | ○ |
reset_password_token_expires_at | datetime | パスワードリセット時のトークン作成日時 | ○ |
reset_password_email_sent_at | datetime | パスワードリセットのメール送信日時 | ○ |
「追加」欄に「○」となっている項目が、今回追加したカラムです。
PasswordResetsController
パスワードリセットを行うためのコントローラを新たに作成しました。以下の様なアクションとURLとなります。
アクション名 | 動詞(GET,PUT,POST,DELETE) | URL | 概要 |
---|---|---|---|
create | POST | api/v1/password_resets.json | パスワードリセットの要求を行う。要求があるとトークンを発行し、パスワードリセットの案内メールを送信する。 |
edit | GET | api/v1/password_resets/トークン/edit.json | トークンを受け取り、そのトークンが正しいかを判定する |
update | POST | api/v1/password_resets/トークン.json | パスワードリセットを行う |
UserMailer
上記にも書きましたが、パスワードリセットの要求時にメールを送信します。このためApplicationMailerを継承したUserMailerクラスを作成します。
パスワードリセットのフロー
今回作成する機能のイメージを掴みやすくするため、パスワードリセットのフロー順にAPIを呼び出した結果を貼付けておきます。 上記のPasswordResetsControllerの表と対応して見てください。
1.パスワードリセットの要求
パスワードリセットの要求を行います。この時に送信するデータはメールアドレスのみとなります。
$ curl -i -X POST http://localhost:3000/api/v1/password_resets.json -d 'email=xxxx@xxxx.co.jp' HTTP/1.1 201 Created
Usersテーブルには以下のようにパスワードリセット時のトークンが登録されます。
sqlite> select id, email, reset_password_token, reset_password_token_expires_at, reset_password_email_sent_at from users; id|email|reset_password_token|reset_password_token_expires_at|reset_password_email_sent_at 53|xxxx@xxxx.co.jp|CpYDpyuHKrLVUJFjUzAE||2015-09-29 13:32:41.597365
2.メール送信
上記のトークン登録と同時に、以下の様なメールを送信します。
今回はAPIのみの実装であるためメールのリンクをクリックしても何も起きませんが、ポイントとしてはメールでトークンを送信している(id=・・・の部分)です。実案件ではURLに対応した画面を用意し、次のAPIにトークンを渡すことでトークンチェック・パスワードの変更を行います。
3.トークンチェック
リセットする前にトークンをチェックします。メールで送信したトークンを正しく送った場合、以下のようになります。
$ curl -i -X GET http://0.0.0.0:3000/api/v1/password_resets/CpYDpyuHKrLVUJFjUzAE/edit.json HTTP/1.1 200 OK
トークンが違う場合は404を返すようにしました。
$ curl -i -X GET http://0.0.0.0:3000/api/v1/password_resets/CpYDpyuHKrLVUJFjUzA/edit.json HTTP/1.1 404 Not Found
4.パスワードリセット
新しいパスワードと、確認用に同一のパスワードをユーザに入力してもらい、それらをAPIに送ります。
$ curl -i -X PUT http://0.0.0.0:3000/api/v1/password_resets/CpYDpyuHKrLVUJFjUzAE.json -d 'user[password]=password' -d 'user[password_confirmation]=password' HTTP/1.1 200 OK
UsersテーブルのパスワードがAPIに送ったもので書き換えられ、トークンは削除されます。
sqlite> select id, email, reset_password_token, reset_password_token_expires_at, reset_password_email_sent_at from users; id|email|reset_password_token|reset_password_token_expires_at|reset_password_email_sent_at 53|xxxx@xxxx.co.jp|||2015-09-29 13:32:41.597365
実装について
では、実装方法についてです。sorceryの「reset_password」サブモジュールを使用するため、基本的には[Ruby on Rails]sorceryによる認証 – (4)パスワードリセットと同じ手順となります。
1.「reset_password」サブモジュールのインストール
パスワードリセットを行うためのサブモジュールをインストールします。以下のコマンドを実行してください。
$ rails g sorcery:install reset_password --migrations
以下のようなマイグレーションファイルが作成されます。
class SorceryResetPassword < ActiveRecord::Migration def change add_column :users, :reset_password_token, :string, :default => nil add_column :users, :reset_password_token_expires_at, :datetime, :default => nil add_column :users, :reset_password_email_sent_at, :datetime, :default => nil end end
マイグレーションを実行してDBに反映します。またsorceryの定義ファイルに、使用するサブモジュールとして「reset_password」が追加されていることを確認してください(無ければ追加してください)。
config/initializers/sorcery.rb
Rails.application.config.sorcery.submodules = [:reset_password, ・・・]
2.ActionMailerの定義
ActionMailerの定義を行います。まず以下のコマンドを実行してください。
$ rails g mailer UserMailer reset_password_email
sorceryの定義ファイルにパスワードリセットに使用するActionMailerとしてUserMailerを定義します。
config/initializers/sorcery.rb
config.user_config do |user| (中略) user.reset_password_mailer = UserMailer (中略) end
パスワードリセット時のメール本文を定義します。
app/views/user_mailer/reset_password_email.text.erb
Hello, <%= @user.email %> =============================================== You have requested to reset your password. To choose a new password, just follow this link: <%= @url %>. Have a great day!
テキスト形式のメールのみ定義したため「app/views/user_mailer」配下の「〜.html.erb」は削除します。またメールを送信するためのアクションも実装します。
app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer def reset_password_email(user) @user = User.find user.id @url = "http://0.0.0.0:8888/path?id=" + @user.reset_password_token mail(:to => user.email, :subject => "Your password has been reset") end end
「@url」に格納するURLについては実案件では作成する画面のURLに合わせてください。先にも書いたように、ここでトークンをメールに渡すようにしています。
sorceryの定義ファイルにアクティベーションで使用するActionMailerとしてUserMailerを定義します。
config/initializers/sorcery.rb
config.user_config do |user| (中略) user.user_activation_mailer = UserMailer (中略) end
メールで送信を行うためのsmtpの定義も行います。
config/environments/development.rb
config.action_mailer.delivery_method = :smtp config.action_mailer.raise_delivery_errors = true config.action_mailer.smtp_settings = { :enable_starttls_auto => true, :address => 'smtp.gmail.com', :port => '587', :domain => 'smtp.gmail.com', :authentication => 'plain', :user_name => ENV['MAIL_USER'], :password => ENV['MAIL_PASSWORD'], :password => ENV['MAIL_APP_PASSWORD'] }
メールのユーザ名、パスワードについては環境変数から読み取るようにしています。環境変数についてはアプリ起動時に読み込ませるため、application.rbに以下の記述を追加します。
application.rb
ENV.update YAML.load_file('環境変数を記述したymlのパス')[Rails.env] rescue {}
メール送信については、漏れている手順などあれば[Ruby on Rails]sorceryによる認証 – (3)メールによるアクティベーションも参考にしてください。
3.パスワードリセットを行うコントローラを定義
パスワードリセットを行うコントローラを作成します。
$ rails g controller Api::V1::PasswordResets create edit update
作成されたコントローラを以下のように編集します。
app/controllers/api/v1/password_resets_controller.rb
class Api::V1::PasswordResetsController < Api::V1::ApplicationBaseController skip_before_filter :require_valid_token def create @user = User.find_by_email(params[:email]) if @user respond_to do |format| @user.deliver_reset_password_instructions! format.json { render nothing: true, status: :created } end else respond_to do |format| format.json { render nothing: true, status: :not_found } end end end def edit if set_token_user_from_params? respond_to do |format| format.json { render nothing: true, status: :ok } end else respond_to do |format| format.json { render nothing: true, status: :not_found } end end end def update if set_token_user_from_params? @user.password_confirmation = params[:user][:password_confirmation] if @user.change_password!(params[:user][:password]) respond_to do |format| format.json { render nothing: true, status: :ok } end else respond_to do |format| format.json { render nothing: true, status: :not_acceptable } end end else respond_to do |format| format.json { render nothing: true, status: :not_found } end end end private def set_token_user_from_params? @token = params[:id] @user = User.load_from_reset_password_token(params[:id]) return !@user.blank? end end
create()では送信されてきたメールに対応するUserを検索し、「deliver_reset_password_instructions!」を呼び出すことでトークンの発行とメール送信を行っています。edit()はトークンの存在チェックです。update()では「change_password!」を呼び出してパスワードの変更を行っています。APIであるため、それぞれのアクションではHTTPステータスコードを状態に応じて返しています。
作成したコントローラをルーティングに追加します。
config/routes.rb
namespace :api do namespace :v1 do (中略) resources :password_resets (中略) end end
まとめ
「reset_password」サブモジュールを使用することで、APIの場合でも簡単にパスワードリセット機能を実装することができました。
今回作成したソースコードは以下のGithubに置いてあります。全ソースを見たい方は参考にしてください。
sorcery_api_sample