SECURITY: Add confirmation screen when connecting associated accounts

SECURITY: Add confirmation screen when connecting associated accounts

diff --git a/app/assets/javascripts/discourse/controllers/associate-account-confirm.js.es6 b/app/assets/javascripts/discourse/controllers/associate-account-confirm.js.es6
new file mode 100644
index 0000000..e9685e0
--- /dev/null
+++ b/app/assets/javascripts/discourse/controllers/associate-account-confirm.js.es6
@@ -0,0 +1,26 @@
+import { ajax } from "discourse/lib/ajax";
+import { popupAjaxError } from "discourse/lib/ajax-error";
+import ModalFunctionality from "discourse/mixins/modal-functionality";
+
+export default Ember.Controller.extend(ModalFunctionality, {
+  actions: {
+    finishConnect() {
+      ajax({
+        url: `/associate/${encodeURIComponent(this.model.token)}`,
+        type: "POST"
+      })
+        .then(result => {
+          if (result.success) {
+            this.transitionToRoute(
+              "preferences.account",
+              this.currentUser.findDetails()
+            );
+            this.send("closeModal");
+          } else {
+            this.set("model.error", result.error);
+          }
+        })
+        .catch(popupAjaxError);
+    }
+  }
+});
diff --git a/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/account.js.es6
index 543c3a9..f5c1f6f 100644
--- a/app/assets/javascripts/discourse/controllers/preferences/account.js.es6
+++ b/app/assets/javascripts/discourse/controllers/preferences/account.js.es6
@@ -20,6 +20,7 @@ export default Ember.Controller.extend(
       this._super(...arguments);
 
       this.saveAttrNames = ["name", "title"];
+      this.set("revoking", {});
     },
 
     canEditName: setting("enable_names"),
@@ -32,6 +33,8 @@ export default Ember.Controller.extend(
 
     showAllAuthTokens: false,
 
+    revoking: null,
+
     cannotDeleteAccount: Ember.computed.not("currentUser.can_delete_account"),
     deleteDisabled: Ember.computed.or(
       "model.isSaving",
@@ -202,7 +205,7 @@ export default Ember.Controller.extend(
       },
 
       revokeAccount(account) {
-        this.set("revoking", true);
+        this.set(`revoking.${account.name}`, true);
 
         this.model
           .revokeAssociatedAccount(account.name)
@@ -214,7 +217,7 @@ export default Ember.Controller.extend(
             }
           })
           .catch(popupAjaxError)
-          .finally(() => this.set("revoking", false));
+          .finally(() => this.set(`revoking.${account.name}`, false));
       },
 
       toggleShowAllAuthTokens() {
diff --git a/app/assets/javascripts/discourse/models/login-method.js.es6 b/app/assets/javascripts/discourse/models/login-method.js.es6
index 083559a..a916663 100644
--- a/app/assets/javascripts/discourse/models/login-method.js.es6
+++ b/app/assets/javascripts/discourse/models/login-method.js.es6
@@ -29,7 +29,7 @@ const LoginMethod = Ember.Object.extend({
         authUrl += "?reconnect=true";
       }
 
-      if (fullScreenLogin || this.full_screen_login) {
+      if (reconnect || fullScreenLogin || this.full_screen_login) {
         document.cookie = "fsl=true";
         window.location = authUrl;
       } else {
diff --git a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 b/app/assets/javascripts/discourse/routes/app-route-map.js.es6
index bfec70d..654c683 100644
--- a/app/assets/javascripts/discourse/routes/app-route-map.js.es6
+++ b/app/assets/javascripts/discourse/routes/app-route-map.js.es6
@@ -178,6 +178,7 @@ export default function() {
   this.route("signup", { path: "/signup" });
   this.route("login", { path: "/login" });
   this.route("email-login", { path: "/session/email-login/:token" });
+  this.route("associate-account", { path: "/associate/:token" });
   this.route("login-preferences");
   this.route("forgot-password", { path: "/password-reset" });
   this.route("faq", { path: "/faq" });
diff --git a/app/assets/javascripts/discourse/routes/associate-account.js.es6 b/app/assets/javascripts/discourse/routes/associate-account.js.es6
new file mode 100644
index 0000000..dfbe2e5
--- /dev/null
+++ b/app/assets/javascripts/discourse/routes/associate-account.js.es6
@@ -0,0 +1,16 @@
+import { ajax } from "discourse/lib/ajax";
+import showModal from "discourse/lib/show-modal";
+import { popupAjaxError } from "discourse/lib/ajax-error";
+
+export default Discourse.Route.extend({
+  beforeModel() {
+    const params = this.paramsFor("associate-account");
+    this.replaceWith(`preferences.account`, this.currentUser).then(() =>
+      Ember.run.next(() =>
+        ajax(`/associate/${encodeURIComponent(params.token)}`)
+          .then(model => showModal("associate-account-confirm", { model }))
+          .catch(popupAjaxError)
+      )
+    );
+  }
+});
diff --git a/app/assets/javascripts/discourse/templates/modal/associate-account-confirm.hbs b/app/assets/javascripts/discourse/templates/modal/associate-account-confirm.hbs
new file mode 100644
index 0000000..3fec07f
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/modal/associate-account-confirm.hbs
@@ -0,0 +1,21 @@
+{{#d-modal-body 
+    rawTitle=(
+      i18n "user.associated_accounts.confirm_modal_title" 
+      provider=(i18n (concat "login." model.provider_name ".name"))
+    )
+}}
+  {{#if model.error}}
+    <div class='alert alert-error'>
+      {{model.error}}
+    </div>
+  {{/if}}
+
+  {{i18n "user.associated_accounts.confirm_description"
+      provider=(i18n (concat "login." model.provider_name ".name"))
+      account_description=model.account_description}}
+{{/d-modal-body}}
+
+<div class="modal-footer">
+  {{d-button label="user.associated_accounts.connect" action=(action "finishConnect") class="btn-primary" icon="plug"}}
+  {{d-button label="user.associated_accounts.cancel" action=(action "closeModal")}}
+</div>
diff --git a/app/assets/javascripts/discourse/templates/preferences/account.hbs b/app/assets/javascripts/discourse/templates/preferences/account.hbs
index 0c904e4..9f71dcc 100644
--- a/app/assets/javascripts/discourse/templates/preferences/account.hbs
+++ b/app/assets/javascripts/discourse/templates/preferences/account.hbs
@@ -111,9 +111,7 @@
                 <td>{{authProvider.account.description}}</td>
                 <td>
                   {{#if authProvider.method.can_revoke}}
-                    {{#conditional-loading-spinner condition=revoking size='small'}}
-                      {{d-button action=(action "revokeAccount") actionParam=authProvider.account title="user.associated_accounts.revoke" class="btn-danger no-text" icon="trash-alt" }}
-                    {{/conditional-loading-spinner}}
+                    {{d-button action=(action "revokeAccount") actionParam=authProvider.account title="user.associated_accounts.revoke" class="btn-danger no-text" icon="trash-alt" disabled=(get revoking authProvider.method.name) }}
                   {{/if}}
                 </td>
             </tr>
diff --git a/app/controllers/users/associate_accounts_controller.rb b/app/controllers/users/associate_accounts_controller.rb
new file mode 100644
index 0000000..6505afa
--- /dev/null
+++ b/app/controllers/users/associate_accounts_controller.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+class Users::AssociateAccountsController < ApplicationController
+  REDIS_PREFIX ||= "omniauth_reconnect"
+
+  ##
+  # Presents a confirmation screen to the user. Accessed via GET, with no CSRF checks
+  def connect_info
+    auth = get_auth_hash
+
+    provider_name = auth.provider
+    authenticator = Discourse.enabled_authenticators.find { |a| a.name == provider_name }
+    raise Discourse::InvalidAccess.new(I18n.t('authenticator_not_found')) if authenticator.nil?
+
+    account_description = authenticator.description_for_auth_hash(auth)
+
+    render json: { token: params[:token], provider_name: provider_name, account_description: account_description }
+  end
+
+  ##
+  # Presents a confirmation screen to the user. Accessed via GET, with no CSRF checks
+  def connect
+    auth = get_auth_hash
+    $redis.del "#{REDIS_PREFIX}_#{current_user&.id}_#{params[:token]}"
+

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

GitHub sha: 0a6cae65

1 Like