diff --git a/app/assets/javascripts/discourse/app/components/dialog-messages/confirm-session.gjs b/app/assets/javascripts/discourse/app/components/dialog-messages/confirm-session.gjs index 172552a86b5..c22c7bc9ed9 100644 --- a/app/assets/javascripts/discourse/app/components/dialog-messages/confirm-session.gjs +++ b/app/assets/javascripts/discourse/app/components/dialog-messages/confirm-session.gjs @@ -45,7 +45,7 @@ export default class ConfirmSession extends Component { return; } - const result = await ajax("/u/confirm-session", { + const result = await ajax("/u/confirm-session.json", { type: "POST", data: { publicKeyCredential }, }); @@ -67,7 +67,7 @@ export default class ConfirmSession extends Component { ? null : I18n.t("user.confirm_access.incorrect_password"); - const result = await ajax("/u/confirm-session", { + const result = await ajax("/u/confirm-session.json", { type: "POST", data: { password: this.password, diff --git a/app/assets/javascripts/discourse/app/controllers/preferences/second-factor.js b/app/assets/javascripts/discourse/app/controllers/preferences/second-factor.js index 81fc760ecfb..53a27644b8c 100644 --- a/app/assets/javascripts/discourse/app/controllers/preferences/second-factor.js +++ b/app/assets/javascripts/discourse/app/controllers/preferences/second-factor.js @@ -2,6 +2,7 @@ import Controller from "@ember/controller"; import { action } from "@ember/object"; import { alias } from "@ember/object/computed"; import { inject as service } from "@ember/service"; +import ConfirmSession from "discourse/components/dialog-messages/confirm-session"; import SecondFactorConfirmPhrase from "discourse/components/dialog-messages/second-factor-confirm-phrase"; import SecondFactorAddSecurityKey from "discourse/components/modal/second-factor-add-security-key"; import SecondFactorAddTotp from "discourse/components/modal/second-factor-add-totp"; @@ -25,11 +26,11 @@ export default Controller.extend(CanCheckEmails, { newUsername: null, backupEnabled: alias("model.second_factor_backup_enabled"), secondFactorMethod: SECOND_FACTOR_METHODS.TOTP, - totps: null, + totps: [], + security_keys: [], init() { this._super(...arguments); - this.set("totps", []); }, @discourseComputed @@ -47,6 +48,36 @@ export default Controller.extend(CanCheckEmails, { return totps.length > 0 || security_keys.length > 0; }, + async createToTpModal() { + try { + await this.modal.show(SecondFactorAddTotp, { + model: { + secondFactor: this.model, + markDirty: () => this.markDirty(), + onError: (e) => this.handleError(e), + }, + }); + this.loadSecondFactors(); + } catch (error) { + popupAjaxError(error); + } + }, + + async createSecurityKeyModal() { + try { + await this.modal.show(SecondFactorAddSecurityKey, { + model: { + secondFactor: this.model, + markDirty: this.markDirty, + onError: this.handleError, + }, + }); + this.loadSecondFactors(); + } catch (error) { + popupAjaxError(error); + } + }, + @action handleError(error) { if (error.jqXHR) { @@ -104,6 +135,46 @@ export default Controller.extend(CanCheckEmails, { this.set("dirty", true); }, + @action + async createTotp() { + try { + const trustedSession = await this.model.trustedSession(); + + if (!trustedSession.success) { + this.dialog.dialog({ + title: I18n.t("user.confirm_access.title"), + type: "notice", + bodyComponent: ConfirmSession, + didConfirm: () => this.createToTpModal(), + }); + } else { + await this.createToTpModal(); + } + } catch (error) { + popupAjaxError(error); + } + }, + + @action + async createSecurityKey() { + try { + const trustedSession = await this.model.trustedSession(); + + if (!trustedSession.success) { + this.dialog.dialog({ + title: I18n.t("user.confirm_access.title"), + type: "notice", + bodyComponent: ConfirmSession, + didConfirm: () => this.createSecurityKeyModal(), + }); + } else { + await this.createSecurityKeyModal(); + } + } catch (error) { + popupAjaxError(error); + } + }, + actions: { disableAllSecondFactors() { if (this.loading) { @@ -237,28 +308,6 @@ export default Controller.extend(CanCheckEmails, { }); }, - async createTotp() { - await this.modal.show(SecondFactorAddTotp, { - model: { - secondFactor: this.model, - markDirty: () => this.markDirty(), - onError: (e) => this.handleError(e), - }, - }); - this.loadSecondFactors(); - }, - - async createSecurityKey() { - await this.modal.show(SecondFactorAddSecurityKey, { - model: { - secondFactor: this.model, - markDirty: this.markDirty, - onError: this.handleError, - }, - }); - this.loadSecondFactors(); - }, - async editSecurityKey(security_key) { await this.modal.show(SecondFactorEditSecurityKey, { model: { diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-second-factor-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-second-factor-test.js index cae9e17598e..1f1e089717c 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-second-factor-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-second-factor-test.js @@ -51,6 +51,10 @@ acceptance("User Preferences - Second Factor", function (needs) { backup_codes: ["dsffdsd", "fdfdfdsf", "fddsds"], }); }); + + server.get("/u/trusted-session.json", () => { + return helper.response({ success: "OK" }); + }); }); test("second factor totp", async function (assert) { diff --git a/spec/system/user_page/user_preferences_security_spec.rb b/spec/system/user_page/user_preferences_security_spec.rb index ba1ffa7c262..ad4c9d892dd 100644 --- a/spec/system/user_page/user_preferences_security_spec.rb +++ b/spec/system/user_page/user_preferences_security_spec.rb @@ -109,13 +109,38 @@ describe "User preferences for Security", type: :system do end end + shared_examples "enforced second factor" do + it "allows user to add 2FA" do + SiteSetting.enforce_second_factor = "all" + + visit("/") + + expect(page).to have_selector( + ".alert-error", + text: "You are required to enable two-factor authentication before accessing this site.", + ) + + expect(page).to have_css(".user-preferences .totp") + expect(page).to have_css(".user-preferences .security-key") + + find(".user-preferences .totp .btn.new-totp").click + + find(".dialog-body input#password").fill_in(with: password) + find(".confirm-session .btn-primary").click + + expect(page).to have_css(".qr-code") + end + end + context "when desktop" do include_examples "security keys" include_examples "passkeys" + include_examples "enforced second factor" end context "when mobile", mobile: true do include_examples "security keys" include_examples "passkeys" + include_examples "enforced second factor" end end