[Ruby on Rails]sorceryによる認証 – (7)APIでのパスワードリセット

rails

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

以前の記事sorceryを使用してのAPIでの認証について書きました。今回はAPIでのパスワードリセットを試してみました。

今回作成した機能の概要

sorceryを使用したパスワードリセットについては[Ruby on Rails]sorceryによる認証 – (4)パスワードリセットで以前書きました。今回はこの内容を踏襲し、以前の記事で作成したsorcery_api_sampleに機能を追加する形でAPIとして実装しました。なので先に以前の記事を一読することをお勧めします。

以下、今回追加した機能の概要です。

usersテーブルへのカラム追加

ユーザ情報を保持するにusersテーブルに、カラムを追加しました。最終的には以下のようなレイアウトとなります。

項目名 概要 追加
id INTEGER ユーザのID
email 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.メール送信

上記のトークン登録と同時に、以下の様なメールを送信します。 sorcery_api_password_reset_mail
今回は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