diff --git a/app/assets/javascripts/discourse/app/components/second-factor-form.js b/app/assets/javascripts/discourse/app/components/second-factor-form.js index e208a5f3145..9b23ed64ac3 100644 --- a/app/assets/javascripts/discourse/app/components/second-factor-form.js +++ b/app/assets/javascripts/discourse/app/components/second-factor-form.js @@ -42,10 +42,12 @@ export default Component.extend({ } }, - @discourseComputed("backupEnabled", "secondFactorMethod") - showToggleMethodLink(backupEnabled, secondFactorMethod) { + @discourseComputed("backupEnabled", "totpEnabled", "secondFactorMethod") + showToggleMethodLink(backupEnabled, totpEnabled, secondFactorMethod) { return ( - backupEnabled && secondFactorMethod !== SECOND_FACTOR_METHODS.SECURITY_KEY + backupEnabled && + totpEnabled && + secondFactorMethod !== SECOND_FACTOR_METHODS.SECURITY_KEY ); }, diff --git a/app/assets/javascripts/discourse/app/controllers/login.js b/app/assets/javascripts/discourse/app/controllers/login.js index 401c85f354a..482509d68a7 100644 --- a/app/assets/javascripts/discourse/app/controllers/login.js +++ b/app/assets/javascripts/discourse/app/controllers/login.js @@ -223,21 +223,30 @@ export default Controller.extend(ModalFunctionality, { this.clearFlash(); if ( - (result.security_key_enabled || result.totp_enabled) && + (result.security_key_enabled || + result.totp_enabled || + result.backup_enabled) && !this.secondFactorRequired ) { + let secondFactorMethod; + if (result.security_key_enabled) { + secondFactorMethod = SECOND_FACTOR_METHODS.SECURITY_KEY; + } else if (result.totp_enabled) { + secondFactorMethod = SECOND_FACTOR_METHODS.TOTP; + } else { + secondFactorMethod = SECOND_FACTOR_METHODS.BACKUP_CODE; + } this.setProperties({ otherMethodAllowed: result.multiple_second_factor_methods, secondFactorRequired: true, showLoginButtons: false, backupEnabled: result.backup_enabled, - showSecondFactor: result.totp_enabled, + totpEnabled: result.totp_enabled, + showSecondFactor: result.totp_enabled || result.backup_enabled, showSecurityKey: result.security_key_enabled, - secondFactorMethod: result.security_key_enabled - ? SECOND_FACTOR_METHODS.SECURITY_KEY - : SECOND_FACTOR_METHODS.TOTP, securityKeyChallenge: result.challenge, securityKeyAllowedCredentialIds: result.allowed_credential_ids, + secondFactorMethod, }); // only need to focus the 2FA input for TOTP diff --git a/app/assets/javascripts/discourse/app/templates/modal/login.hbs b/app/assets/javascripts/discourse/app/templates/modal/login.hbs index d7465134af5..39de9f023e9 100644 --- a/app/assets/javascripts/discourse/app/templates/modal/login.hbs +++ b/app/assets/javascripts/discourse/app/templates/modal/login.hbs @@ -26,7 +26,7 @@
{{d-icon "exclamation-triangle"}} {{i18n "login.caps_lock_warning"}}
- + {{#if this.showSecurityKey}} diff --git a/app/assets/javascripts/discourse/tests/acceptance/second-factor-auth-test.js b/app/assets/javascripts/discourse/tests/acceptance/second-factor-auth-test.js index 32b57a7e6d7..0d8c4cc0c65 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/second-factor-auth-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/second-factor-auth-test.js @@ -44,6 +44,12 @@ const RESPONSES = { security_keys_enabled: true, allowed_methods: [BACKUP_CODE], }, + ok010010: { + totp_enabled: false, + backup_enabled: true, + security_keys_enabled: false, + allowed_methods: [BACKUP_CODE], + }, }; Object.keys(RESPONSES).forEach((k) => { @@ -178,6 +184,14 @@ acceptance("Second Factor Auth Page", function (needs) { !exists(".toggle-second-factor-method"), "no alternative methods are shown if only 1 method is allowed" ); + + // only backup codes + await visit("/session/2fa?nonce=ok010010"); + assert.ok(exists("form.backup-code-token"), "backup code form is shown"); + assert.ok( + !exists(".toggle-second-factor-method"), + "no alternative methods are shown if only 1 method is allowed" + ); }); test("switching 2FA methods", async function (assert) { diff --git a/app/models/concerns/second_factor_manager.rb b/app/models/concerns/second_factor_manager.rb index e21957e75ff..4e212306af6 100644 --- a/app/models/concerns/second_factor_manager.rb +++ b/app/models/concerns/second_factor_manager.rb @@ -79,7 +79,7 @@ module SecondFactorManager end def has_any_second_factor_methods_enabled? - totp_enabled? || security_keys_enabled? + totp_enabled? || security_keys_enabled? || backup_codes_enabled? end def has_multiple_second_factor_methods? diff --git a/app/serializers/admin_detailed_user_serializer.rb b/app/serializers/admin_detailed_user_serializer.rb index aab939247c5..454fcf6cbd9 100644 --- a/app/serializers/admin_detailed_user_serializer.rb +++ b/app/serializers/admin_detailed_user_serializer.rb @@ -45,7 +45,7 @@ class AdminDetailedUserSerializer < AdminUserSerializer has_many :groups, embed: :object, serializer: BasicGroupSerializer def second_factor_enabled - object.totp_enabled? || object.security_keys_enabled? + object.totp_enabled? || object.security_keys_enabled? || object.backup_codes_enabled? end def can_disable_second_factor diff --git a/app/serializers/current_user_serializer.rb b/app/serializers/current_user_serializer.rb index 9f53ff09376..70920d01a51 100644 --- a/app/serializers/current_user_serializer.rb +++ b/app/serializers/current_user_serializer.rb @@ -323,7 +323,7 @@ class CurrentUserSerializer < BasicUserSerializer end def second_factor_enabled - object.totp_enabled? || object.security_keys_enabled? + object.totp_enabled? || object.security_keys_enabled? || object.backup_codes_enabled? end def featured_topic diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb index 0e9f975032e..eb2db4e56ed 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -105,7 +105,7 @@ class UserSerializer < UserCardSerializer end def second_factor_enabled - object.totp_enabled? || object.security_keys_enabled? + object.totp_enabled? || object.security_keys_enabled? || object.backup_codes_enabled? end def include_second_factor_backup_enabled? diff --git a/spec/serializers/admin_user_list_serializer_spec.rb b/spec/serializers/admin_user_list_serializer_spec.rb index 955e726719e..2c1e3ebadb4 100644 --- a/spec/serializers/admin_user_list_serializer_spec.rb +++ b/spec/serializers/admin_user_list_serializer_spec.rb @@ -31,6 +31,18 @@ RSpec.describe AdminUserListSerializer do end end + context "when backup codes enabled" do + before do + Fabricate(:user_second_factor_backup, user: user) + end + + it "is true" do + json = serializer.as_json + + expect(json[:second_factor_enabled]).to eq(true) + end + end + describe "emails" do fab!(:admin) { Fabricate(:user, admin: true, email: "admin@email.com") } fab!(:moderator) { Fabricate(:user, moderator: true, email: "moderator@email.com") } diff --git a/spec/serializers/current_user_serializer_spec.rb b/spec/serializers/current_user_serializer_spec.rb index 3b6ce305831..b25f9c94e09 100644 --- a/spec/serializers/current_user_serializer_spec.rb +++ b/spec/serializers/current_user_serializer_spec.rb @@ -102,6 +102,16 @@ RSpec.describe CurrentUserSerializer do expect(json[:second_factor_enabled]).to eq(true) end end + + context "when backup codes enabled" do + before do + User.any_instance.stubs(:backup_codes_enabled?).returns(true) + end + + it "is true" do + expect(json[:second_factor_enabled]).to eq(true) + end + end end describe "#groups" do diff --git a/spec/serializers/user_serializer_spec.rb b/spec/serializers/user_serializer_spec.rb index 05765453258..beb8637d244 100644 --- a/spec/serializers/user_serializer_spec.rb +++ b/spec/serializers/user_serializer_spec.rb @@ -250,6 +250,16 @@ RSpec.describe UserSerializer do expect(json[:second_factor_enabled]).to eq(true) end end + + context "when backup codes enabled" do + before do + User.any_instance.stubs(:backup_codes_enabled?).returns(true) + end + + it "is true" do + expect(json[:second_factor_enabled]).to eq(true) + end + end end describe "ignored and muted" do