1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【豆知識】リセットパスワードの流れを改めて整理してみる

Last updated at Posted at 2024-08-21

はじめに

アプリ制作の課題に取り組んでいる中で、改めて過去に行った課題を参考にしていたら流れが理解できていない状態。改めて自分が何をしているのか、コードの意味を含め一度整理しておくためにもリセットパスワードの全体の流れをアウトプットしたいと思います。

環境

  • Windows, WSL
  • Docker
  • Ruby 3.2.3
  • Rails 7.1.3

1.gem 'letter_opener_web'の設定( http://localhost:3000/letter_opener)

  • Gemfileに追記する(開発環境!)

    group :development do
      gem 'letter_opener_web', '~> 3.0'
    end
    
  • コンテナ内に入りインストールを行う

    $ docker exec コンテナ名 bash
    # bundle install
    
    インストール後はサーバーを立ち上げなおす(この過程を省くと変なエラーが起こる可能性あり)
    $ docker compose restart
    
    Dockerコンテナ内で開発環境も起動しておく
    $ docker compose exec web bin/dev 
    
  • config/routes.rbに追記
    ・開発環境 (Rails.env.development? が true の場合) において、/letter_opener というURLで Letter Opener Web のインターフェースにアクセスできるようになります。これにより、送信されたメールの内容をブラウザ上で確認できます。

    Your::Application.routes.draw do
     mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development?
    end
    
  • config/environments/development.rb の設定
    ・1つめはメールの配信方法 (delivery_method) を letter_opener_web に指定しています。
    ・2つ目については RailsのActionMailerでは、メール内に含めるURLを生成する際にdefault_url_optionsで指定されたホスト名とポート番号が使用されますが、この設定がないことでリンク生成が失敗してしまいます。また、パスワードリセットのメールでedit_password_reset_url(@user.reset_token)のようにURLを生成する場合、ホスト名が設定されていないとリンクが正しく構築されません。その結果、ユーザーがリンクをクリックしても期待するページにアクセスできず、エラーが発生する可能性があります。

    # letter_opener_web の設定
    config.action_mailer.delivery_method = :letter_opener_web
    
    # メーラーでのホスト情報
    config.action_mailer.default_url_options = { host: 'localhost:3000' }
    
  • letters_location の設定について
    ・公式にはconfig/environments/development.rbに次の設定をすることも提案されていましたが、開発環境はデフォルトを使用すればいいのでこちらの設定はしませんでした。このコードを追加することで、手紙の保存場所を変更できます

    LetterOpenerWeb.configure do |config|
     config.letters_location = Rails.root.join('your', 'new', 'path')
    end
    

2.パスワードリセット

  • Sorcery のサブモジュール、reset_passwordの導入
    ・ユーザーがパスワードを忘れた場合に、再設定するための手続きを提供する機能を実装。
    rails g sorceryだとSorceryの全ての標準機能をインストールしてしまうが:install reset_password --only-submodulesを加えることでreset_passwordに必要なファイルだけが生成される。

    $ docker exec コンテナ名 bash
    # rails g sorcery:install reset_password --only-submodules
    
  • 生成されたマイグレーションファイルに追記する(reset_password_tokenカラム)
    ・changeメソッドの中に書かれた内容が、データベースに対して行われる変更を定義し、メソッド内のコードが実行されると、データベースが更新される。
    ・add_columnメソッドを使って、usersテーブルに新しいカラムを追加
    ・それぞれのカラムは上から順に、「パスワードリセット用のトークン・トークンの有効期限・パスワードリセット用のメールが送信された日時・パスワードリセットページにアクセスした回数」を保存するためのカラム
    reset_password_tokenカラムにインデックスを追加する事で、トークン1の検索が速くなり、unique: trueと指定してるから、同じトークンを持つレコードが2つ存在できないように制約がかかる

    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
        add_column :users, :access_count_to_reset_password_page, :integer, default: 0
    
        add_index :users, :reset_password_token, unique: true
      end
    end
    
  • マイグレーションファイルに追記後以下実行

    # rails db:migrate
    

3.メールアクションを1つ持つメイラーを追加

  • Railsのメール機能をサポートするためのファイルを生成する
# rails g mailer UserMailer reset_password_email
  • app/mailers/user_mailer.rbファイルにuserパラメータを追加他
    ・user は、メールを送信する対象となるユーザーを指します。
    @user というインスタンス変数を作成し、user.id を使ってデータベースからそのユーザーを取得
    @url というインスタンス変数に、パスワードリセットページへのリンクを生成し、そのURLを代入
    ・mail メソッドは、メールを送信するために使用されます。to: user.email でメールの送信先を指定し、subject: "件名" でメールの件名を設定
    class UserMailer < ApplicationMailer
      default from: 'no-reply@アプリ名.com'
    
      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
    

4.reset_passwordサブモジュールを追加し、使用するメイラーを定義

  • Sorceryの初期設定ファイルの編集(config/initializers/sorcery.rbファイル)
    ・UserMailer を使用してリセットメールを送信するように構成していく。
    ・コメントアウトを外して定義していく。
    configという設定オブジェクトを使いSorceryの動作をカスタマイズしていく
    user_configブロック内では、個々のユーザーに対する認証の動作をカスタマイズ。ここで設定された内容は、アプリケーションの全ユーザーに適用‼
    stretchesは、パスワードのハッシュ化2の回数を指定
    authentications_classを使うことで、ユーザーの認証に関する処理を柔軟に管理できるようになるが不要なら削除。
    ※外部認証機能を使わない場合、externalは外してください。blablu以降はご自身が使用する機能など。
    Rails.application.config.sorcery.submodules = [:reset_password, :external, blablu, ...]
    
    Rails.application.config.sorcery.configure do |config|
      config.user_config do |user|
        user.stretches = 1 if Rails.env.test?
        user.reset_password_mailer = UserMailer
        user.authentications_class = Authentication
      end
    end
    

5.Userモデルにバリテーション追加

validatesはRailsのバリデーションメソッドで、モデルの属性に対して様々な条件を設定するために使う
uniqueness: trueによって同じトークンが存在する場合トークンの再利用を防ぐ
allow_nil: trueにより、リセットパスワードトークンがnilの場合エラー発生を防ぐ。

validates :reset_password_token, uniqueness: true, allow_nil: true

6.パスワードアクションを処理するコントローラの作成

  • コンテナ内で下記コマンド実行

    # rails g controller PasswordResets create edit update
    
  • app/controllers/password_resets_controller.rb編集

    class PasswordResetsController < ApplicationController
      skip_before_action :require_login
    
      def create 
        @user = User.find_by_email(params[:email])
    
        @user.deliver_reset_password_instructions! if @user
    
        redirect_to(root_path, :notice => 'Instructions have been sent to your email.')
      end
    
      def edit
        @token = params[:id]
        @user = User.load_from_reset_password_token(params[:id])
    
        if @user.blank?
          not_authenticated
          return
        end
      end
    
      def update
        @token = params[:id]
        @user = User.load_from_reset_password_token(params[:id])
    
        if @user.blank?
          not_authenticated
          return
        end
    
        @user.password_confirmation = params[:user][:password_confirmation]
    
        if @user.change_password(params[:user][:password])
          redirect_to(root_path, :notice => 'Password was successfully updated.')
        else
          render :action => "edit"
        end
      end
    end
    

    ※上記コードは資料参考のものだが次の通りパラメータを用いたりエラー文を表示するよう変更も可能

    class PasswordResetsController < ApplicationController
      # ログインできないユーザー用
      skip_before_action :require_login
    
      # パスワードリセット申請フォーム
      def new; end
    
      # ユーザーが入力したメールアドレスを取得
      def create
        # メールアドレスからユーザーを特定
        @user = User.find_by(email: params[:email])
        if @user
          # ユーザが見つかれば、トークンを生成し、再設定用のメールを送信
         @user&.deliver_reset_password_instructions!
          redirect_to login_path, notice: 'メールを送信しました'
        else
          flash.now[:alert] = 'メールアドレスが見つかりません'
         render :new, status: :unprocessable_entity
        end
      end
    
      # パスワードリセットフォーム
      def edit
        @token = params[:id]
        @user = User.load_from_reset_password_token(params[:id])
    
        # ユーザーがいなければログインページへ遷移
        if @user.blank?
          not_authenticated
          return
        end
      end
    
      def update
        @token = params[:id]
        # params に含まれるトークンからユーザを特定
        @user = User.load_from_reset_password_token(params[:id])
    
        # ユーザが見つからない場合
        if @user.blank?
          # ログイン画面に遷移
          not_authenticated
          return
        end
    
        # バリデーションチェック
        @user.password_confirmation = password_params[:password_confirmation]
        # トークンをクリアして、パスワードを更新
        if @user.change_password(password_params[:password])
          # ログインページに遷移し、フラッシュメッセージを表示
          redirect_to login_path, notice: 'パスワードを再設定しました'
        else
          # 再設定画面を再表示
          flash.now[:alert] = '再設定に失敗しました'
          render :edit, status: :unprocessable_entity
        end
      end
    
      def password_params
        params.require(:user).permit(:password, :password_confirmation)
      end
    end
    

7.残りを追加

  • ルーティングの設定
    password_resetsコントローラーの各アクション等に対するルート設定をすることで、パスワードリセットのリクエストやフォーム表示、リセット処理が行えるようになる。

    # パスワードリセット
    resources :password_resets, only: %i[new create edit update]
    
  • app/views/layouts/mailer.html.erbファイルについて
    ・資料には= yieldと記載されいていますが。特に修正は不要かと。

  • app/views/user_mailer/reset_password_email.text.erb設定
    ・パスワードリセットのメールテンプレートです。ユーザーのメールアドレスとリセット用のリンクが含まれています。

    <%= @user.name %>様
    ===============================================
    
    パスワード再発行のご依頼を受け付けました。
    
    こちらのリンクからパスワードの再発行を行ってください。 <%= @url %>.
    
  • app/views/user_mailer/reset_password_email.html.erb設定
    ・Rails の Action Mailer では、通常、両方のテンプレートを用意し、受信者のメールクライアントが対応している形式でメールを送信します。なので今回はこちらもコードを入力しておきます。

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

8.パスワードリセット用ビューの設定(editとnewに関しては資料をもとに自身が好きなフォームにアレンジ)

  • app/views/password_resets/edit.html.erb
    ・ユーザーが新しいパスワードを入力するフォームです。トークンを使ってリセットリンクからユーザーを特定し、新しいパスワードを設定します。

  • app/views/user_sessions/new.html.erb
    ・ログインページに「パスワードを忘れた?」リンクを追加

    <%= link_to 'Forgot Password?', new_password_reset_path %>
    
  • app/views/password_resets/new.html.erb
    ・「新しい」ビューを作成し、ログインページからリンクを設定

9.gem 'config'の設定

  • gem 'config'とは、Railsアプリケーションで設定管理を簡単に行うためのgemです。

  • 環境ごとに異なる設定を一元管理することができ、開発環境、テスト環境、本番環境で異なる設定値を使いたい場合に非常に便利です。

    Gemfileに追記
    gem 'config'
    
    コンテナ内で以下コマンド実行してインストールしサーバー立ち上げなおす
    $ bundle install
    # docker compose restart
    # docker compose exec web bin/dev
    
    設定の初期化を行う為つぎのコマンドを実行する
    $ rails g config:install
    
  • config/settings/development.yml例えばこんな感じで設定

    default_url_options:
      host: 'localhost:3000'
    
  • 上記ファイルで定義した値を呼び出すようにconfig/environments/development.rbを編集
    ・to_hは、オブジェクトをハッシュに変換するメソッドで、config.action_mailer.default_url_optionsがハッシュ形式のオプションを期待しているからつけないとエラーになってしまう。

    config.action_mailer.default_url_options = Settings.default_url_options.to_h
    

※config/配下を編集した場合は各種サーバーを立ち上げ直してください

  • .gitignoreを編集する
    ・特定のファイルやディレクトリをGitの管理から除外するため
    ・config/settings.local.ymlファイルなどには、データベースのパスワードやAPIキーなどの機密情報が含まれることが多いから、これらをGitに含めないようにする必要がある
    ・また、.local.ymlというファイル名は、開発環境やテスト環境専用の設定を含むことが一般的。これらは開発者ごとに異なるため、リポジトリに含めると混乱を招くことがある
    config/settings.local.yml
    config/settings/*.local.yml
    config/environments/*.local.yml
    

さいごに

ここまでごらんいただきありがとうございます。今回の記事が何か参考になればうれしい限りです。
また、誤字脱字など気づいた点等ございましたら、ご指摘いただけると幸いです。

  1. トークン:特定の情報を識別するための文字列やコードのこと
    パスワードリセットトークン: ユーザーがパスワードをリセットするために送信されるリンクに含まれるコード。これを使って、リセット画面にアクセスできるようになる。

  2. ハッシュ化:パスワードを暗号化するプロセス。stretchesの回数が増えると暗号化が強くなり、より安全になります。しかしテスト環境(Rails.env.test?)では、テストを速くするために1回に減らします。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?