FEATURE: Enforce two-factor authentication. (#6348)
This commit is contained in:
parent
e711d3cb3a
commit
d352baa1a2
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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'"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
admin # to skip welcome wizard at home page `/`
|
||||
end
|
||||
|
||||
it "should redirect admins when enforce_second_factor is 'all'" do
|
||||
SiteSetting.enforce_second_factor = "all"
|
||||
sign_in(admin)
|
||||
|
||||
get "/"
|
||||
expect(response).to redirect_to("/u/#{admin.username}/preferences/second-factor")
|
||||
end
|
||||
|
||||
it "should redirect users when enforce_second_factor is 'all'" do
|
||||
SiteSetting.enforce_second_factor = "all"
|
||||
sign_in(user)
|
||||
|
||||
get "/"
|
||||
expect(response).to redirect_to("/u/#{user.username}/preferences/second-factor")
|
||||
end
|
||||
|
||||
it "should redirect admins when enforce_second_factor is 'staff'" do
|
||||
SiteSetting.enforce_second_factor = "staff"
|
||||
sign_in(admin)
|
||||
|
||||
get "/"
|
||||
expect(response).to redirect_to("/u/#{admin.username}/preferences/second-factor")
|
||||
end
|
||||
|
||||
it "should not redirect users when enforce_second_factor is 'staff'" do
|
||||
SiteSetting.enforce_second_factor = "staff"
|
||||
sign_in(user)
|
||||
|
||||
get "/"
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
|
||||
it "should not redirect admins when turned off" do
|
||||
SiteSetting.enforce_second_factor = "no"
|
||||
sign_in(admin)
|
||||
|
||||
get "/"
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
|
||||
it "should not redirect users when turned off" do
|
||||
SiteSetting.enforce_second_factor = "no"
|
||||
sign_in(user)
|
||||
|
||||
get "/"
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'invalid request params' do
|
||||
before do
|
||||
@old_logger = Rails.logger
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import { acceptance, replaceCurrentUser } from "helpers/qunit-helpers";
|
||||
|
||||
acceptance("Enforce Second Factor", {
|
||||
loggedIn: true
|
||||
});
|
||||
|
||||
QUnit.test("as an admin", async assert => {
|
||||
await visit("/u/eviltrout/preferences/second-factor");
|
||||
Discourse.SiteSettings.enforce_second_factor = "staff";
|
||||
|
||||
await visit("/u/eviltrout/summary");
|
||||
|
||||
assert.equal(
|
||||
find(".control-label").text(),
|
||||
"Password",
|
||||
"it will not transition from second-factor preferences"
|
||||
);
|
||||
|
||||
await click("#toggle-hamburger-menu");
|
||||
await click("a.admin-link");
|
||||
|
||||
assert.equal(
|
||||
find(".control-label").text(),
|
||||
"Password",
|
||||
"it stays at second-factor preferences"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("as a user", async assert => {
|
||||
replaceCurrentUser({ staff: false, admin: false });
|
||||
|
||||
await visit("/u/eviltrout/preferences/second-factor");
|
||||
Discourse.SiteSettings.enforce_second_factor = "all";
|
||||
|
||||
await visit("/u/eviltrout/summary");
|
||||
|
||||
assert.equal(
|
||||
find(".control-label").text(),
|
||||
"Password",
|
||||
"it will not transition from second-factor preferences"
|
||||
);
|
||||
|
||||
await click("#toggle-hamburger-menu");
|
||||
await click("a.about-link");
|
||||
|
||||
assert.equal(
|
||||
find(".control-label").text(),
|
||||
"Password",
|
||||
"it stays at second-factor preferences"
|
||||
);
|
||||
});
|
Loading…
Reference in New Issue