From 04ecf44c2f78ae29911027352a3e9fb21187e20c Mon Sep 17 00:00:00 2001
From: Patrick Figel <patrick@figel.email>
Date: Tue, 2 Jan 2018 16:55:00 +0100
Subject: [PATCH] Add confirmation step for email changes (#6071)

* Add confirmation step for email changes

This adds a confirmation step for email changes of existing users.
Like the initial account confirmation, a confirmation link is sent
to the new address.

Additionally, a notification is sent to the existing address when
the change is initiated. This message includes instruction to reset
the password immediately or to contact the instance admin if the
change was not initiated by the account owner.

Fixes #3871

* Add review fixes
---
 Gemfile                                       |  2 +-
 Gemfile.lock                                  |  2 +-
 .../auth/registrations_controller.rb          |  4 +++
 app/mailers/user_mailer.rb                    | 15 ++++++++++-
 app/models/user.rb                            | 11 +++++---
 .../user_mailer/email_changed.en.html.erb     | 15 +++++++++++
 .../user_mailer/email_changed.en.text.erb     | 13 ++++++++++
 .../reconfirmation_instructions.en.html.erb   | 15 +++++++++++
 .../reconfirmation_instructions.en.text.erb   | 12 +++++++++
 config/initializers/devise.rb                 |  5 +++-
 config/locales/devise.en.yml                  |  4 +++
 spec/mailers/user_mailer_spec.rb              | 26 +++++++++++++++++++
 12 files changed, 116 insertions(+), 8 deletions(-)
 create mode 100644 app/views/user_mailer/email_changed.en.html.erb
 create mode 100644 app/views/user_mailer/email_changed.en.text.erb
 create mode 100644 app/views/user_mailer/reconfirmation_instructions.en.html.erb
 create mode 100644 app/views/user_mailer/reconfirmation_instructions.en.text.erb

diff --git a/Gemfile b/Gemfile
index 16df1c376b..a5bf2daffb 100644
--- a/Gemfile
+++ b/Gemfile
@@ -28,7 +28,7 @@ gem 'browser'
 gem 'charlock_holmes', '~> 0.7.5'
 gem 'iso-639'
 gem 'cld3', '~> 3.2.0'
-gem 'devise', '~> 4.2'
+gem 'devise', '~> 4.3'
 gem 'devise-two-factor', '~> 3.0'
 gem 'doorkeeper', '~> 4.2'
 gem 'fast_blank', '~> 1.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 46c87d7f78..1b7af06a49 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -561,7 +561,7 @@ DEPENDENCIES
   charlock_holmes (~> 0.7.5)
   cld3 (~> 3.2.0)
   climate_control (~> 0.2)
-  devise (~> 4.2)
+  devise (~> 4.3)
   devise-two-factor (~> 3.0)
   doorkeeper (~> 4.2)
   dotenv-rails (~> 2.2)
diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb
index da0b6512f2..b8ff4e54f2 100644
--- a/app/controllers/auth/registrations_controller.rb
+++ b/app/controllers/auth/registrations_controller.rb
@@ -37,6 +37,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController
     new_user_session_path
   end
 
+  def after_update_path_for(_resource)
+    edit_user_registration_path
+  end
+
   def check_enabled_registrations
     redirect_to root_path if single_user_mode? || !allowed_registrations?
   end
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
index 5a062dc257..7821be32b2 100644
--- a/app/mailers/user_mailer.rb
+++ b/app/mailers/user_mailer.rb
@@ -13,7 +13,9 @@ class UserMailer < Devise::Mailer
     return if @resource.disabled?
 
     I18n.with_locale(@resource.locale || I18n.default_locale) do
-      mail to: @resource.unconfirmed_email.blank? ? @resource.email : @resource.unconfirmed_email, subject: I18n.t('devise.mailer.confirmation_instructions.subject', instance: @instance)
+      mail to: @resource.unconfirmed_email.blank? ? @resource.email : @resource.unconfirmed_email,
+           subject: I18n.t(@resource.pending_reconfirmation? ? 'devise.mailer.reconfirmation_instructions.subject' : 'devise.mailer.confirmation_instructions.subject', instance: @instance),
+           template_name: @resource.pending_reconfirmation? ? 'reconfirmation_instructions' : 'confirmation_instructions'
     end
   end
 
@@ -39,4 +41,15 @@ class UserMailer < Devise::Mailer
       mail to: @resource.email, subject: I18n.t('devise.mailer.password_change.subject')
     end
   end
+
+  def email_changed(user, **)
+    @resource = user
+    @instance = Rails.configuration.x.local_domain
+
+    return if @resource.disabled?
+
+    I18n.with_locale(@resource.locale || I18n.default_locale) do
+      mail to: @resource.email, subject: I18n.t('devise.mailer.email_changed.subject')
+    end
+  end
 end
diff --git a/app/models/user.rb b/app/models/user.rb
index 3ce6517a65..a82a7d28a0 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -41,12 +41,15 @@ class User < ApplicationRecord
 
   ACTIVE_DURATION = 14.days
 
-  devise :registerable, :recoverable,
-         :rememberable, :trackable, :validatable, :confirmable,
-         :two_factor_authenticatable, :two_factor_backupable,
-         otp_secret_encryption_key: ENV['OTP_SECRET'],
+  devise :two_factor_authenticatable,
+         otp_secret_encryption_key: ENV['OTP_SECRET']
+
+  devise :two_factor_backupable,
          otp_number_of_backup_codes: 10
 
+  devise :registerable, :recoverable, :rememberable, :trackable, :validatable,
+         :confirmable
+
   belongs_to :account, inverse_of: :user, required: true
   belongs_to :invite, counter_cache: :uses
   accepts_nested_attributes_for :account
diff --git a/app/views/user_mailer/email_changed.en.html.erb b/app/views/user_mailer/email_changed.en.html.erb
new file mode 100644
index 0000000000..c106800865
--- /dev/null
+++ b/app/views/user_mailer/email_changed.en.html.erb
@@ -0,0 +1,15 @@
+<p>Hello <%= @resource.email %>!</p>
+
+<% if @resource&.unconfirmed_email? %>
+  <p>We're contacting you to notify you that the email you use on <%= @instance %> is being changed to <%= @resource.unconfirmed_email %>.</p>
+<% else %>
+  <p>We're contacting you to notify you that the email you use on <%= @instance %> has been changed to <%= @resource.email %>.</p>
+<% end %>
+
+<p>
+  If you did not change your email, it is likely that someone has gained access to your account. Please change your password immediately or contact the instance admin if you're locked out of your account.
+</p>
+
+<p>Sincerely,<p>
+
+<p>The <%= @instance %> team</p>
diff --git a/app/views/user_mailer/email_changed.en.text.erb b/app/views/user_mailer/email_changed.en.text.erb
new file mode 100644
index 0000000000..9719724611
--- /dev/null
+++ b/app/views/user_mailer/email_changed.en.text.erb
@@ -0,0 +1,13 @@
+Hello <%= @resource.email %>!
+
+<% if @resource&.unconfirmed_email? %>
+We're contacting you to notify you that the email you use on <%= @instance %> is being changed to <%= @resource.unconfirmed_email %>.
+<% else %>
+We're contacting you to notify you that the email you use on <%= @instance %> has been changed to <%= @resource.email %>.
+<% end %>
+
+If you did not change your email, it is likely that someone has gained access to your account. Please change your password immediately or contact the instance admin if you're locked out of your account.
+
+Sincerely,
+
+The <%= @instance %> team
diff --git a/app/views/user_mailer/reconfirmation_instructions.en.html.erb b/app/views/user_mailer/reconfirmation_instructions.en.html.erb
new file mode 100644
index 0000000000..31866a3c84
--- /dev/null
+++ b/app/views/user_mailer/reconfirmation_instructions.en.html.erb
@@ -0,0 +1,15 @@
+<p>Hello <%= @resource.unconfirmed_email %>!</p>
+
+<p>You requested a change to the email address you use on <%= @instance %>.</p>
+
+<p>To confirm your new email, please click on the following link:<br>
+<%= link_to 'Confirm my email address', confirmation_url(@resource, confirmation_token: @token) %></p>
+
+<p>If the above link did not work, copy and paste this URL into your address bar: <br>
+<span><%= confirmation_url(@resource, confirmation_token: @token) %></span>
+
+<p>Please also check out our <%= link_to 'terms and conditions', terms_url %>.</p>
+
+<p>Sincerely,<p>
+
+<p>The <%= @instance %> team</p>
diff --git a/app/views/user_mailer/reconfirmation_instructions.en.text.erb b/app/views/user_mailer/reconfirmation_instructions.en.text.erb
new file mode 100644
index 0000000000..c1c735b3ac
--- /dev/null
+++ b/app/views/user_mailer/reconfirmation_instructions.en.text.erb
@@ -0,0 +1,12 @@
+Hello <%= @resource.unconfirmed_email %>!
+
+You requested a change to the email address you use on <%= @instance %>.
+
+To confirm your new email, please click on the following link:
+<%= confirmation_url(@resource, confirmation_token: @token) %>
+
+Please also check out our terms and conditions <%= terms_url %>
+
+Sincerely,
+
+The <%= @instance %> team
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index 64c4e12ff4..07912c28b8 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -137,6 +137,9 @@ Devise.setup do |config|
   # Setup a pepper to generate the encrypted password.
   # config.pepper = '104d16705f794923e77c5e5167b52452d00646dc952a2d30b541c24086e647012c7b9625f253c51912e455981e503446772973d5f1638631196c819d7137fad4'
 
+  # 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
 
@@ -160,7 +163,7 @@ Devise.setup do |config|
   # initial account confirmation) to be applied. Requires additional unconfirmed_email
   # db field (see migrations). Until confirmed, new email is stored in
   # unconfirmed_email column, and copied to email column on successful confirmation.
-  config.reconfirmable = false
+  config.reconfirmable = true
 
   # Defines which key will be used when confirming an account
   # config.confirmation_keys = [:email]
diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml
index 586c5349d7..c5ae583ff0 100644
--- a/config/locales/devise.en.yml
+++ b/config/locales/devise.en.yml
@@ -18,8 +18,12 @@ en:
     mailer:
       confirmation_instructions:
         subject: 'Mastodon: Confirmation instructions for %{instance}'
+      email_changed:
+        subject: 'Mastodon: Email changed'
       password_change:
         subject: 'Mastodon: Password changed'
+      reconfirmation_instructions:
+        subject: 'Mastodon: Confirm email for %{instance}'
       reset_password_instructions:
         subject: 'Mastodon: Reset password instructions'
       unlock_instructions:
diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb
index 1f6d440154..9f17993e0e 100644
--- a/spec/mailers/user_mailer_spec.rb
+++ b/spec/mailers/user_mailer_spec.rb
@@ -33,6 +33,20 @@ describe UserMailer, type: :mailer do
                      instance: Rails.configuration.x.local_domain
   end
 
+  describe 'reconfirmation_instructions' do
+    let(:mail) { UserMailer.confirmation_instructions(receiver, 'spec') }
+
+    it 'renders reconfirmation instructions' do
+      receiver.update!(email: 'new-email@example.com', locale: nil)
+      expect(mail.body.encoded).to include 'new-email@example.com'
+      expect(mail.body.encoded).to include 'spec'
+      expect(mail.body.encoded).to include Rails.configuration.x.local_domain
+      expect(mail.subject).to eq I18n.t('devise.mailer.reconfirmation_instructions.subject',
+                                        instance: Rails.configuration.x.local_domain,
+                                        locale: I18n.default_locale)
+    end
+  end
+
   describe 'reset_password_instructions' do
     let(:mail) { UserMailer.reset_password_instructions(receiver, 'spec') }
 
@@ -57,4 +71,16 @@ describe UserMailer, type: :mailer do
     include_examples 'localized subject',
                      'devise.mailer.password_change.subject'
   end
+
+  describe 'email_changed' do
+    let(:mail) { UserMailer.email_changed(receiver) }
+
+    it 'renders email change notification' do
+      receiver.update!(locale: nil)
+      expect(mail.body.encoded).to include receiver.email
+    end
+
+    include_examples 'localized subject',
+                     'devise.mailer.email_changed.subject'
+  end
 end
-- 
GitLab