FEATURE: Enforce two-factor authentication. (#6348)

approved

#1

FEATURE: Enforce two-factor authentication. (#6348)

diff --git a/app/assets/javascripts/discourse/controllers/preferences/second-factor.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/second-factor.js.es6
index 662eb31..36baea7 100644
--- a/app/assets/javascripts/discourse/controllers/preferences/second-factor.js.es6
+++ b/app/assets/javascripts/discourse/controllers/preferences/second-factor.js.es6
@@ -39,6 +39,26 @@ export default Ember.Controller.extend({
     return findAll().length > 0;
   },
 
+  @computed(
+    "siteSettings.enforce_second_factor",
+    "currentUser",
+    "currentUser.second_factor_enabled",
+    "currentUser.staff"
+  )
+  showEnforcedNotice(
+    enforce_second_factor,
+    user,
+    second_factor_enabled,
+    staff
+  ) {
+    return (
+      user &&
+      !second_factor_enabled &&
+      (enforce_second_factor === "all" ||
+        (enforce_second_factor === "staff" && staff))
+    );
+  },
+
   toggleSecondFactor(enable) {
     if (!this.get("secondFactorToken")) return;
     this.set("loading", true);
diff --git a/app/assets/javascripts/discourse/routes/preferences-second-factor.js.es6 b/app/assets/javascripts/discourse/routes/preferences-second-factor.js.es6
index 4bfa3a8..703bcf3 100644
--- a/app/assets/javascripts/discourse/routes/preferences-second-factor.js.es6
+++ b/app/assets/javascripts/discourse/routes/preferences-second-factor.js.es6
@@ -13,5 +13,28 @@ export default RestrictedUserRoute.extend({
 
   setupController(controller, model) {
     controller.setProperties({ model, newUsername: model.get("username") });
+  },
+
+  actions: {
+    willTransition(transition) {
+      this._super(...arguments);
+
+      const controller = this.controllerFor("preferences/second-factor");
+      const user = controller.get("currentUser");
+      const settings = controller.get("siteSettings");
+
+      if (
+        transition.targetName === "preferences.second-factor" ||
+        !user ||
+        user.second_factor_enabled ||
+        (settings.enforce_second_factor === "staff" && !user.staff) ||
+        settings.enforce_second_factor === "no"
+      ) {
+        return true;
+      }
+
+      transition.abort();
+      return false;
+    }
   }
 });
diff --git a/app/assets/javascripts/discourse/templates/preferences-second-factor.hbs b/app/assets/javascripts/discourse/templates/preferences-second-factor.hbs
index ad50e77..5fefc9f 100644
--- a/app/assets/javascripts/discourse/templates/preferences-second-factor.hbs
+++ b/app/assets/javascripts/discourse/templates/preferences-second-factor.hbs
@@ -1,6 +1,14 @@
 <section class='user-preferences'>
   <form class="form-horizontal">
 
+    {{#if showEnforcedNotice}}
+      <div class="control-group">
+        <div class="controls">
+          <div class='alert alert-error'>{{i18n 'user.second_factor.enforced_notice'}}</div>
+        </div>
+      </div>
+    {{/if}}
+
     {{#if errorMessage}}
       <div class="control-group">
         <div class="controls">
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index e870e2b..1f1de47 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -685,10 +685,9 @@ class ApplicationController < ActionController::Base
   end
 
   def redirect_to_login_if_required
-    return if current_user || (request.format.json? && is_api?)
-
-    if SiteSetting.login_required?
+    return if request.format.json? && is_api?
 
+    if !current_user && SiteSetting.login_required?
       flash.keep
       dont_cache_page
 
@@ -704,6 +703,18 @@ class ApplicationController < ActionController::Base
         redirect_to path("/login")
       end
     end
+
+    if current_user &&
+      !current_user.totp_enabled? &&
+      !request.format.json? &&
+      !is_api? &&
+      ((SiteSetting.enforce_second_factor == 'staff' && current_user.staff?) ||
+        SiteSetting.enforce_second_factor == 'all')
+      redirect_path = "#{GlobalSetting.relative_url_root}/u/#{current_user.username}/preferences/second-factor"
+      if !request.fullpath.start_with?(redirect_path)
+        redirect_to path(redirect_path)
+      end
+    end
   end
 
   def block_if_readonly_mode
diff --git a/app/controllers/extra_locales_controller.rb b/app/controllers/extra_locales_controller.rb
index d0e4884..72e1cd3 100644
--- a/app/controllers/extra_locales_controller.rb
+++ b/app/controllers/extra_locales_controller.rb
@@ -3,7 +3,7 @@
 class ExtraLocalesController < ApplicationController
 
   layout :false
-  skip_before_action :check_xhr, :preload_json
+  skip_before_action :check_xhr, :preload_json, :redirect_to_login_if_required
 
   def show
     bundle = params[:bundle]
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 10e3d2f..154e6e1 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -810,6 +810,7 @@ en:
           Two factor authentication adds extra security to your account by requiring a one-time token in addition to your password. Tokens can be generated on <a href="https://www.google.com/search?q=authenticator+apps+for+android" target='_blank'>Android</a> and <a href="https://www.google.com/search?q=authenticator+apps+for+ios">iOS</a> devices.
         oauth_enabled_warning: "Please note that social logins will be disabled once two factor authentication has been enabled on your account."
         use: "<a href>Use Authenticator app</a>"
+        enforced_notice: "You are required to enable two factor authentication before accessing this site."
 
       change_about:
         title: "Change About Me"
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index cf60082..61d3777 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -1339,6 +1339,7 @@ en:
     notification_email: "The from: email address used when sending all essential system emails. The domain specified here must have SPF, DKIM and reverse PTR records set correctly for email to arrive."
     email_custom_headers: "A pipe-delimited list of custom email headers"
     email_subject: "Customizable subject format for standard emails. See <a href='https://meta.discourse.org/t/customize-subject-format-for-standard-emails/20801' target='_blank'>https://meta.discourse.org/t/customize-subject-format-for-standard-emails/20801</a>"
+    enforce_second_factor: "Forces users to enable second factor authentication. Select 'all' to enforce it to all users. Select 'staff' to enforce it to staff users only."
     force_https: "Force your site to use HTTPS only. WARNING: do NOT enable this until you verify HTTPS is fully set up and working absolutely everywhere! Did you check your CDN, all social logins, and any external logos / dependencies to make sure they are all HTTPS compatible, too?"
     same_site_cookies: "Use same site cookies, they eliminate all vectors Cross Site Request Forgery on supported browsers (Lax or Strict). Warning: Strict will only work on sites that force login and use SSO."
     summary_score_threshold: "The minimum score required for a post to be included in 'Summarize This Topic'"
diff --git a/config/site_settings.yml b/config/site_settings.yml
index a2c4f31..45860c6 100644
--- a/config/site_settings.yml
+++ b/config/site_settings.yml
@@ -1220,6 +1220,14 @@ trust:
     default: true
 
 security:
+  enforce_second_factor:
+    client: true
+    type: enum
+    default: 'no'
+    choices:
+      - 'no'
+      - 'staff'
+      - 'all'
   force_https:
     default: false
     shadowed_by_global: true
diff --git a/spec/requests/application_controller_spec.rb b/spec/requests/application_controller_spec.rb
index 494752c..06a2719 100644
--- a/spec/requests/application_controller_spec.rb
+++ b/spec/requests/application_controller_spec.rb
@@ -20,6 +20,63 @@ RSpec.describe ApplicationController do
     end
   end
 
+  describe '#redirect_to_second_factor_if_required' do
+    let(:admin) { Fabricate(:admin) }
+    let(:user) { Fabricate(:user) }
+
+    before do

[... diff too long, it was truncated ...]

GitHub sha: d352baa1


Approved #2