Rails6.1 + Devise 3.8.0 で、Deviseのデフォルトの挙動を確認してみた

Deviseを使うと認証まわりの機能をRailsに組み込むのが容易です。

ただ、「そういえばDeviseにある各モジュールのデフォルトの挙動はどんな感じだろう」と思ったため、調べた時のメモを残します。

 
目次

 

環境

  • Rails 6.1.3.2
  • Devise 3.8.0

 

検証するためのRails + Deviseアプリを作成

今回使用するモジュールは

  • database_authenticatable
  • registerable
  • recoverable
  • confirmable
  • validatable
  • lockable

です。

ただ、各モジュールの設定はデフォルトのままではなく、気になった設定はカスタマイズしていきます。

具体的には以下です。

  • 変更時にもconfirmableを動作させる
  • Deviseが送信するメール中のトークンを見やすくするため、DeviseのMailerとビューを差し替え
  • 動作確認をしやすいよう、以下の設定へと変更
    • 確認トークンの有効期限を1分
    • ログイン失敗は3回まで
    • ログイン失敗のロックは1分間
    • パスワードリセットで生成するトークンの有効期限は1分

 

アプリ作成

今回は rails_devise_default_app という名前でRailsアプリを作成します。

% rails new rails_devise_default_app

 

Gemfileの修正

各モジュールのViewについては必要最低限とするため、今回不要な webpaker をGemfileを削除します。

一方、今回検証する Devise と、Deviseが送信するメールをブラウザで確認するための Letter Opener を追加します。

# コメントアウト
# gem 'webpacker', '~> 5.0'

# 追加
gem 'devise'

group :development do
  gem 'letter_opener'
end

 
変更したので、インストールします。

% bundle install

 

RailsのControllerとViewの作成

Deviseでログインしている時の挙動を確認するため、

  • 誰でもアクセスできるページ
    • Controllerの index メソッドを使う
  • ログインしたユーザーのみアクセスできるページ
    • Controllerの show メソッドを使う

の2つを用意します *1

% bin/rails g controller home index show --helper=false --assets=false
Running via Spring preloader in process 40269
      create  app/controllers/home_controller.rb
       route  get 'home/index'
get 'home/show'
      invoke  erb
      create    app/views/home
      create    app/views/home/index.html.erb
      create    app/views/home/show.html.erb
      invoke  test_unit
      create    test/controllers/home_controller_test.rb

 
show だけログイン必須とするため、 Controller filter の before_action で Devise の authenticate_user!show メソッドのみ動作するよう定義します。
https://github.com/heartcombo/devise#controller-filters-and-helpers

class HomeController < ApplicationController
  before_action :authenticate_user!, only: ['show']

  def index
  end

  def show
  end
end

 
Viewのうち、index (app/views/home/index.html.erb) は誰でもアクセスできるページのため

<h1>Public Page</h1>

とします。

一方、 show (app/views/home/show.html.erb) はログイン済ユーザーのみアクセス可能なため、その旨をViewにも表示します。

<h1>Private Page</h1>
<p>only signed in users</p>

 
ただ、これだけだとログインしているユーザーが誰か分からないため、共通で使う layout (app/views/layouts/application.html.erb) でユーザー情報を表示させます。

なお、layoutには Webpacker のタグが記載されていたため、そちらは削除しておきます。

<head>
  ...
  <%# webpacker を使用していないため、不要なものを削除 %>
  <%#= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>

<body>
  <%# deviseを使ったログインができているかを確認しやすくするため、追加 %>
  <% if current_user.present? %>
    <p>ログイン済ユーザ: <%= current_user.email %></p>
  <% end %>
  <p class="notice"><%= notice %></p>
  <p class="alert"><%= alert %></p>
  <%# 追加ここまで %>

  <%= yield %>
</body>
</html>

 

Railsの起動するポートを変更

本来この設定を行う必要はないのですが、手元でRailsアプリが増えてしまっているので、app/config/puma.rb を修正し、今回のアプリが起動するポートを 3710 へと変更しておきます。

port ENV.fetch("PORT") { 3710 }

 

Action Mailerまわりの修正

設定ファイル config/environment/development.rb に対し、Action Mailerまわりについて、Railsの起動ポートと Letter Opener を考慮した形で、末尾に追記します。

# 追加
# メール内のリンクをRailsの起動ポートと合わせる
config.action_mailer.default_url_options = { host: 'localhost', port: 3710 }

# Letter Openerを使うように設定
config.action_mailer.delivery_method = :letter_opener

# Letter Openerで確認するため、実際のメール配信を行う
config.action_mailer.perform_deliveries = true

 

Deviseのジェネレータで初期化

bin/rails g devise:install を実行します。

% bin/rails g devise:install

Running via Spring preloader in process 94335
      create  config/initializers/devise.rb
      create  config/locales/devise.en.yml
===============================================================================

Depending on your application's configuration some manual setup may be required:

  1. Ensure you have defined default url options in your environments files. Here
     is an example of default_url_options appropriate for a development environment
     in config/environments/development.rb:

       config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

     In production, :host should be set to the actual host of your application.

     * Required for all applications. *

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root to: "home#index"
     
     * Not required for API-only Applications *

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>

     * Not required for API-only Applications *

  4. You can copy Devise views (for customization) to your app by running:

       rails g devise:views
       
     * Not required *

 

Devise用の User Model を作成

Deviseのジェネレータを使って生成します。

% bin/rails g devise User
Running via Spring preloader in process 41318
      invoke  active_record
      create    db/migrate/20210605140140_devise_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
      insert    app/models/user.rb
       route  devise_for :users

 
続いて、User Model (app/models/user.rb) に Devise のモジュールを組み込みます。

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  # devise :database_authenticatable, :registerable,
  #        :recoverable, :rememberable, :validatable
  #
  # 変更
  devise :database_authenticatable, # 認証
         :registerable, # 登録・変更・削除
         :recoverable, # パスワードリセット
         :confirmable, # メールでの登録
         :validatable, # メールやパスワードのバリデーション
         :lockable # アカウントロック
end

 

マイグレーションファイルの編集

Deviseのジェネレータが生成したマイグレーションファイル (db/migrate/<タイムスタンプ>_devise_create_user.rb) に対して、以下の編集を行います。

  • 必要なDeviseモジュールの項目やIndexを有効化
  • Deviseとは関係ないフィールド name を用意
# frozen_string_literal: true

class DeviseCreateUsers < ActiveRecord::Migration[6.1]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      # t.datetime :remember_created_at

      ## Trackable
      # t.integer  :sign_in_count, default: 0, null: false
      # t.datetime :current_sign_in_at
      # t.datetime :last_sign_in_at
      # t.string   :current_sign_in_ip
      # t.string   :last_sign_in_ip

      ## Confirmable
      t.string   :confirmation_token
      t.datetime :confirmed_at
      t.datetime :confirmation_sent_at
      t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      t.string   :unlock_token # Only if unlock strategy is :email or :both
      t.datetime :locked_at


      t.timestamps null: false

      # 追加した項目
      t.string :name,              null: false, default: ""
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    add_index :users, :confirmation_token,   unique: true
    add_index :users, :unlock_token,         unique: true
  end
end

 
マイグレーションファイルの編集が終わったら、マイグレーションを実行しておきます。

% bin/rails db:migrate

 

User向けにDeviseのViewを生成

今回使わないものが多いですが、DeviseのViewをジェネレータで生成しておきます。

% bin/rails g devise:views users
Running via Spring preloader in process 40901
      invoke  Devise::Generators::SharedViewsGenerator
      create    app/views/users/shared
      create    app/views/users/shared/_error_messages.html.erb
      create    app/views/users/shared/_links.html.erb
      invoke  form_for
      create    app/views/users/confirmations
      create    app/views/users/confirmations/new.html.erb
      create    app/views/users/passwords
      create    app/views/users/passwords/edit.html.erb
      create    app/views/users/passwords/new.html.erb
      create    app/views/users/registrations
      create    app/views/users/registrations/edit.html.erb
      create    app/views/users/registrations/new.html.erb
      create    app/views/users/sessions
      create    app/views/users/sessions/new.html.erb
      create    app/views/users/unlocks
      create    app/views/users/unlocks/new.html.erb
      invoke  erb
      create    app/views/users/mailer
      create    app/views/users/mailer/confirmation_instructions.html.erb
      create    app/views/users/mailer/email_changed.html.erb
      create    app/views/users/mailer/password_change.html.erb
      create    app/views/users/mailer/reset_password_instructions.html.erb
      create    app/views/users/mailer/unlock_instructions.html.erb

 

DeviseのMaierとメールテンプレートの差し替え

Deviseデフォルトのメールテンプレートには、Deviseが生成した各種トークンが記載されていません。

そこで、目視で確認しやすくするよう、メールのテンプレートを差し替えます。

また、差し替えたメールテンプレートを使うために、DeviseのMailerも差し替えます。

なお、前回の記事でふれた通り、Devise 4.8.0の時点ではWikiのやり方ではメールテンプレートが差し替わらないことに注意します。
Devise 4.8.0 でメールテンプレートを変更したい場合、カスタムメーラーの template_path ではなく headers_for をオーバーライドする - メモ的な思考的な

 
まずは、Deviseのジェネレータで生成したメールテンプレートを差し替えるため、ディレクトapp/views/users/mailer にあるテンプレート

  • confirmation_instructions.html.erb
  • reset_password_instructions.html.erb
  • unlock_instructions.html.erb

の各ファイルを開き、テンプレートの末尾にトークンを表示するよう

<p>token <%= @token %></p>

を追加します。

 
次に、Mailerを差し替えるため、 app/views/mailers/devise_my_mailer.rb として以下を作成します。

class DeviseMyMailer < Devise::Mailer
  def headers_for(action, opts)
    super.merge!(template_path: 'users/mailer')
  end
end

 
最後に、Devise向けの設定ファイル config/initializer/devise.rbconfig.mailer を変更します。

config.mailer = 'DeviseMyMailer'

 

User向けにDeviseのControllerを生成

今回はコントローラのカスタマイズをしませんが、参考までに生成しておきます。

% bin/rails generate devise:controllers users
Running via Spring preloader in process 41089
      create  app/controllers/users/confirmations_controller.rb
      create  app/controllers/users/passwords_controller.rb
      create  app/controllers/users/registrations_controller.rb
      create  app/controllers/users/sessions_controller.rb
      create  app/controllers/users/unlocks_controller.rb
      create  app/controllers/users/omniauth_callbacks_controller.rb
===============================================================================

Some setup you must do manually if you haven't yet:

  Ensure you have overridden routes for generated controllers in your routes.rb.
  For example:

    Rails.application.routes.draw do
      devise_for :users, controllers: {
        sessions: 'users/sessions'
      }
    end

 

追加したControllerやDeviseのルーティングを追加

config/routes.rb に各ルーティングを追加します。

なお、ルートルーティングはファイルの先頭に書いておきます。
3.14 rootを使う | Rails のルーティング - Railsガイド

Rails.application.routes.draw do
  root to: 'home#index'

  # Deviseのルーティング
  devise_for :users

  # ログインしていないと表示できないページ
  get '/private', to: 'home#show', as: 'private_page'
end

 

動作確認しやすくするよう設定ファイルの修正

動作確認しやすくするため、設定ファイル (config/initializers/devise.rb) を修正します。

# 確認トークンの有効期限
# config.confirm_within = 3.days
config.confirm_within = 1.minute

# ロック回数
# config.maximum_attempts = 20
config.maximum_attempts = 3

# ロック期間 
# config.unlock_in = 1.hour
config.unlock_in = 1.minute

# パスワード忘れの有効期限
# config.reset_password_within = 6.hours
config.reset_password_within = 1.minute

 
以上で、Deviseのデフォルトの挙動を確認するためのアプリができました。

引き続き、Deviseのデフォルトの挙動を見ていきます。スクリーンショット多めです。

 

新規ユーザー登録(サインアップ)について

流れ

今回は bar@example.com ユーザーを登録してみます。

新規ユーザー登録のURLは以下です。
http://localhost:3710/users/sign_up

 
メールとパスワードを入力し、sign upボタンをクリックします。

なお、Deviseデフォルトのテンプレートを修正しない場合、Modelに追加した項目 name は表示されません。

 
ボタンをクリック後、ルートパスに戻り、確認待ちのメッセージが表示されます。

 
一方、メールを確認すると、トークン付URLが記載されたメールが届いています。

 
トークン付URLのリンクをクリックすると、ユーザを認証できました。

また、この時点ではログインできていないため、ログインページへ移動します。

 
先ほど設定したメールアドレスとパスワードを入力すると、ログインできました。

 

ログイン済でのサインアップ・サインイン・パスワードリセットの挙動

ログイン済の状態で

の各ページへアクセスしたところ、ルートパスへリダイレクトしました。

 

メールアドレス変更

流れ

今回は bar@example.com から foo@example.com へメールアドレスを変更してみます。

Deviseのデフォルトでは、ユーザー情報の編集ページでメールアドレスを変更します。
http://localhost:3710/users/edit

 
変更後のメールアドレスと、現在のパスワードを入力し、Updateボタンをクリックします。

なお、Deviseデフォルトのテンプレートを修正しない場合、Modelに追加した項目 name は表示されません。

 
ボタンをクリック後、ルートパスに戻り、確認待ちのメッセージが表示されます。

 
一方、メールを確認すると、トークン付URLが記載されたメールが届いています。

 
メール中のトークン付URLのリンクをログイン中のブラウザで開くと、変更が完了します。ログイン状態も維持されます。

 

メールアドレス確認待ちでの挙動

変更後のメールアドレスが確認待ちの状態の場合、変更後のメールアドレスではログインできません。

 
また、ユーザー情報の編集ページを開くと、現在メールアドレスが変更中と表示されます。

 
ただし、現在メールアドレスが変更中であってもメールアドレスの変更は可能なため、トークン付URLのリンクが含まれるメールが送信されます。

 

変更後のメールアドレスに、変更前と同じメールアドレスを入力した時の挙動

こんな感じで入力します。

 
この場合、トークン付URLが記載されたメールは届かず、変更が完了したと表示されます。

 

パスワード変更について

パスワード変更は、ユーザー情報編集ページで行なえます。

新しいパスワードと現在のパスワードを入力します。

変更されました。ログイン状態は維持されます。

 

パスワードリセットについて

流れ

以下のURLからパスワードのリセットができます。
http://localhost:3710/users/password/new

   
今回、パスワードリセットする時の状態として、

  • ユーザー登録したもののメールアドレスの確認前の場合
  • ユーザー登録に加えメールアドレス確認が完了している場合
  • アカウントがロックされている場合

の3パターンを確認しましたが、いずれも挙動は同じでした。

 
まずはパスワードリセットの画面に、登録済のメールアドレスを入力します。

 
メールを確認すると、トークン付URLが記載されたメールが届いています。

 
画面にもメールを送ったと表示されます。

 
メールのリンクを踏むと、パスワードの再入力画面が表示されます。

 
新しいパスワードを入力し、Change my password ボタンをクリックすると、パスワードが変更されました。

また、ログインも完了しています。

 

パスワードリセットトークンの有効期限が切れた場合

新しいパスワードの入力画面にエラーメッセージが表示されます。

 

パスワードリセット中に、もう一度パスワードリセットを行った場合

新しいパスワードを再入力した際に、トークンが不正というエラーが表示されます。

 

パスワードリセット中に、今までのパスワードでログインしようとした場合

スクリーンショットでの表現は難しいため言葉だけとなりますが、ログインできます。

ただし、パスワードリセットのトークンが有効な間は、トークン付URLからパスワード変更が可能です。

 

アカウントロックについて

流れ

アカウントロックは、Deviseの lockable モジュールになります。

ログイン時にパスワード間違いを続け、ロックまであと1回の状態はこのような状態です。

 
デフォルトでは20回間違えるとアカウントがロックされます。

この後、正しいパスワードを入力したとしても、アカウントはロックされたままです。

 
アカウントがロックされたと同時に、メールアドレスにトークン付URLを含んだメールが届きます。

 
メールのURLをクリックすると、ロックを解除され、ログインできるようになります。

 
正しいメールアドレスとパスワードを入力すると、ログインできました。

 

メール再送について

Deviseでは、メールアドレスの所有者確認やパスワード忘れのメール再送も可能です。

 

メールアドレス確認のメールを再送する場合

Resend confirmation instructions にて再送可能です。
http://localhost:3710/users/confirmation/new

 
サインアップ時のメールアドレス確認の場合でも再送が可能です。

この場合、トークンは同じになります。

1回目

2回目

 

ロック解除メールを再送する場合

こちらは Resend unlock instructions になります。
http://localhost:3710/users/unlock/new

 
メールアドレスの確認とは異なり、再送時のトークンは異なります。

1回目

2回目

 

気になるところ

今まで見てきたとおり、Deviseには色々実装されています。

一方で、気になった部分を書いておきます。

 

別ブラウザでもトークン付URLを踏んだら認証できること

Chromeでユーザ登録を行ってトークン付URLを含むメールを受信した後、トークン付URLをEdgeへコピーしてアクセスしたところ、ユーザの認証ができました。

「同一セッションで認証する」などの制限は行っておらず、トークン付URLを知っていれば誰でも認証できるようです。

既存メールアドレスの存在が分かってしまうこと

デフォルトの状態では、そのアプリケーションにメールアドレスが登録されているかどうかが判断できる作りとなっていました。

 

スクリーンショット
サインアップ時で既存メールアドレスを入力した時

 

メールアドレス変更で既存メールアドレスを入力した時

 

パスワード忘れで、メールアドレスが存在しない時

 

トークン付URLを踏む前のメールアドレスを、サインインで入力した時

 

メールアドレス確認の再送画面で、既に認証済のメールを入力した時

 

メールアドレス確認の再送画面で、メールアドレスが存在しない時

 

ロック解除再送で、ロックしていない時

 

ロック解除再送で、メールアドレスが存在していない時

 

Deviseの paranoid モードを使った対応

Deviseでもこの挙動は認識されています。

DeviseのWiki では paranoid mode を使うと良いと記載されています。
How To: Using paranoid mode, avoid user enumeration on registerable · heartcombo/devise Wiki

 
config/initializer/devise.rb の paranoidtrue とすることで、paranoid modeになります。

config.paranoid = true

 
そこで、paranoid mode を有効化した後の挙動を確認していました。

 

paranoid modeでメールアドレスの存在が分からなくなったケース

サインイン前でも使用可能な画面の

  • パスワード忘れで、メールアドレスが存在しない時
  • メールアドレス確認の再送画面で、既に認証済のメールを入力した時
  • メールアドレス確認の再送画面で、メールアドレスが存在しない時
  • ロック解除再送で、ロックしていない時
  • ロック解除再送で、メールアドレスが存在していない時

については、エラーメッセージが変わり、サインイン画面へと遷移しています。

 

変更なし:サインアップ時で既存のメールアドレスを入力した場合

Wikiにある通り、サインアップ時は変わりません。

 

変更なし:メールアドレス変更で既存のメールアドレスを入力した場合

ログイン済で操作できる画面のせいか、エラーは変わりませんでした。

 

変更なし:トークン付URLを踏む前のメールアドレスを、サインインで入力した時

 

メールアドレス変更で他人のメールアドレスを誤入力すると、他人がアカウントを奪える

例えば、以下の記事にあるような操作をした場合です。
観点2: ユーザが間違って第三者の連絡手段を入力してしまった場合、第三者に個人情報が流出してしまう | Webサービスによくある各機能の仕様とセキュリティ観点(ユーザ登録機能) - Qiita

 

流れ

メールアドレス変更時に、誤ったメールアドレスと正しいパスワードを入力すると、誤ったメールアドレスの持ち主にメールが届きます。

そこで、トークン付URLを踏むと、誤ったメールアドレスへアカウントのメールアドレスが変更されました。しかし、この時点では未ログイン状態です。

 
続いて、パスワードリセット画面へ移動して、メールアドレスを入力します。

 
リセットURL付きのメールがメールアドレスに届きます。

 
続いて、パスワードリセットのURLをクリックします。

 
新しいパスワードを入力します。

 
パスワード変更が完了し、ログインできました。これで、誤ったメールアドレスの持ち主がアカウントを奪えました。

 

徳丸本での記載

徳丸本 第2版 p502 の「メールアドレス変更に必要な機能的対策」によると、

  • 新規メールアドレスによる受信確認
  • 再認証
  • メール通知

このうち、新規メールアドレスによる受信確認を徳丸本 p497 にある方式B (確認番号を入力する方式) にすれば、誤ったメールアドレスを入力しても問題なさそうです。

以下のさきほど挙げた記事でも、似たような内容がふれられています。
Webサービスによくある各機能の仕様とセキュリティ観点(ユーザ登録機能) - Qiita

 
ただ、Deviseの標準機能では、パスワードリセットを確認番号入力化する機能は見当たりませんでした。

そこで、他のWebフレームワークでの実装はあるのか気になりTweetしたところ、徳丸先生に拾っていただけました。ありがとうございます。

 
もし、実装しているWebフレームワークをご存じの方がいれば、教えていただけるとありがたいです。

 

Deviseでの対応 (通知機能)

さて、標準のDeviseではパスワードリセットを確認番号化できないものの、通知機能はあります。
Notify users via email when their passwords change · heartcombo/devise Wiki

上記Wikiにはパスワード変更のみの記載されていますが、Devise 4.8.0 時点ではメールアドレス変更での通知にも対応しています。

 
そこで、devise.rbの以下の設定をアンコメントした上でRailsを再起動し、挙動を確認してみます。

# Send a notification to the original email when the user's email is changed.
config.send_email_changed_notification = true

# Send a notification email when the user's password is changed.
config.send_password_change_notification = true

 

ユーザー情報編集画面の場合

 
ユーザー情報の編集画面では、メールアドレスとパスワードを変更できます。

今回はメールアドレスとパスワードを同時に変更します。

 
変更後、ルートに戻ります。

 
新しいメールアドレスにトークン付URLメールが飛びます。

 
また、メールアドレス変更の通知も飛びます。

トークン付URLをクリックしてメールアドレスの確認をする前ですが通知が飛ぶようです。

 
他に、パスワード変更の通知も飛びます。

 

パスワードリセット画面の場合

パスワードリセットでメールアドレスを入力します。

 
トークン付URLメールが届きますので、パスワードを変更します。

 
変更後、パスワード変更のメールが届きます。

 

参考:Deviseのカスタマイズについて

ここまでDevise標準の機能を見てきました。

もし、Deviseの標準以外の機能を使いたい場合は、自分でカスタマイズすることになります。

カスタマイズ方法については、以下が詳しいです。

 

ソースコード

Githubに上げました。
https://github.com/thinkAmi-sandbox/devise_default_app

*1:DeviseではURLが sign_in となっているものの、Viewでは Login と表示されています。そのため、サインインとログインのどちらを使えばよいか迷いましたが、今回は「ログイン」を使います