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
メソッドを使う
- ログインしたユーザーのみアクセスできるページ
の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 を考慮した形で、末尾に追記します。
config.action_mailer.default_url_options = { host: 'localhost', port: 3710 }
config.action_mailer.delivery_method = :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
devise :database_authenticatable,
:registerable,
:recoverable,
:confirmable,
:validatable,
:lockable
end
Deviseのジェネレータが生成したマイグレーションファイル (db/migrate/<タイムスタンプ>_devise_create_user.rb
) に対して、以下の編集を行います。
- 必要なDeviseモジュールの項目やIndexを有効化
- Deviseとは関係ないフィールド
name
を用意
class DeviseCreateUsers < ActiveRecord::Migration[6.1]
def change
create_table :users do |t|
t.string :email, null: false, default: ""
t.string :encrypted_password, null: false, default: ""
t.string :reset_password_token
t.datetime :reset_password_sent_at
t.string :confirmation_token
t.datetime :confirmed_at
t.datetime :confirmation_sent_at
t.string :unconfirmed_email
t.integer :failed_attempts, default: 0, null: false
t.string :unlock_token
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.rb
の config.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_for :users
get '/private', to: 'home#show', as: 'private_page'
end
動作確認しやすくするよう設定ファイルの修正
動作確認しやすくするため、設定ファイル (config/initializers/devise.rb
) を修正します。
config.confirm_within = 1.minute
config.maximum_attempts = 3
config.unlock_in = 1.minute
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 の paranoid
を true
とすることで、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を再起動し、挙動を確認してみます。
config.send_email_changed_notification = true
config.send_password_change_notification = true
ユーザー情報編集画面の場合
ユーザー情報の編集画面では、メールアドレスとパスワードを変更できます。
今回はメールアドレスとパスワードを同時に変更します。
変更後、ルートに戻ります。
新しいメールアドレスにトークン付URLメールが飛びます。
また、メールアドレス変更の通知も飛びます。
トークン付URLをクリックしてメールアドレスの確認をする前ですが通知が飛ぶようです。
他に、パスワード変更の通知も飛びます。
パスワードリセット画面の場合
パスワードリセットでメールアドレスを入力します。
トークン付URLメールが届きますので、パスワードを変更します。
変更後、パスワード変更のメールが届きます。
参考:Deviseのカスタマイズについて
ここまでDevise標準の機能を見てきました。
もし、Deviseの標準以外の機能を使いたい場合は、自分でカスタマイズすることになります。
カスタマイズ方法については、以下が詳しいです。
Githubに上げました。
https://github.com/thinkAmi-sandbox/devise_default_app