From 9c72c00206c3e9cb9c3076f2d4a297e6296e7e74 Mon Sep 17 00:00:00 2001
From: David Taylor <david@taylorhq.com>
Date: Fri, 27 Jul 2018 12:28:51 +0100
Subject: [PATCH] FEATURE: Revoke and reconnect for Twitter logins

---
 .../discourse/models/login-method.js.es6      |  2 +-
 lib/auth/twitter_authenticator.rb             | 37 +++++++++++++++--
 .../auth/twitter_authenticator_spec.rb        | 41 +++++++++++++++++++
 3 files changed, 76 insertions(+), 4 deletions(-)

diff --git a/app/assets/javascripts/discourse/models/login-method.js.es6 b/app/assets/javascripts/discourse/models/login-method.js.es6
index 515594b4352..ef291594ea2 100644
--- a/app/assets/javascripts/discourse/models/login-method.js.es6
+++ b/app/assets/javascripts/discourse/models/login-method.js.es6
@@ -127,7 +127,7 @@ export function findAll(siteSettings, capabilities, isMobileDevice) {
         params.displayPopup = true;
       }
 
-      if (["facebook", "google_oauth2"].includes(name)) {
+      if (["facebook", "google_oauth2", "twitter"].includes(name)) {
         params.canConnect = true;
       }
 
diff --git a/lib/auth/twitter_authenticator.rb b/lib/auth/twitter_authenticator.rb
index c6ffbe2145f..be591a02357 100644
--- a/lib/auth/twitter_authenticator.rb
+++ b/lib/auth/twitter_authenticator.rb
@@ -13,7 +13,26 @@ class Auth::TwitterAuthenticator < Auth::Authenticator
     info&.email || info&.screen_name || ""
   end
 
-  def after_authenticate(auth_token)
+  def can_revoke?
+    true
+  end
+
+  def revoke(user, skip_remote: false)
+    info = TwitterUserInfo.find_by(user_id: user.id)
+    raise Discourse::NotFound if info.nil?
+
+    # We get a token from twitter upon login but do not need it, and do not store it.
+    # Therefore we do not have any way to revoke the token automatically on twitter's end
+
+    info.destroy!
+    true
+  end
+
+  def can_connect_existing_user?
+    true
+  end
+
+  def after_authenticate(auth_token, existing_account: nil)
     result = Auth::Result.new
 
     data = auth_token[:info]
@@ -35,9 +54,21 @@ class Auth::TwitterAuthenticator < Auth::Authenticator
 
     user_info = TwitterUserInfo.find_by(twitter_user_id: twitter_user_id)
 
-    result.user = user_info.try(:user)
+    if existing_account && (user_info.nil? || existing_account.id != user_info.user_id)
+      user_info.destroy! if user_info
+      result.user = existing_account
+      user_info = TwitterUserInfo.create!(
+        user_id: result.user.id,
+        screen_name: result.username,
+        twitter_user_id: twitter_user_id,
+        email: result.email
+      )
+    else
+      result.user = user_info&.user
+    end
+
     if (!result.user) && result.email_valid && (result.user = User.find_by_email(result.email))
-      TwitterUserInfo.create(
+      TwitterUserInfo.create!(
         user_id: result.user.id,
         screen_name: result.username,
         twitter_user_id: twitter_user_id,
diff --git a/spec/components/auth/twitter_authenticator_spec.rb b/spec/components/auth/twitter_authenticator_spec.rb
index 8c2d4659655..f6b620dfdf3 100644
--- a/spec/components/auth/twitter_authenticator_spec.rb
+++ b/spec/components/auth/twitter_authenticator_spec.rb
@@ -25,4 +25,45 @@ describe Auth::TwitterAuthenticator do
     expect(info.email).to eq(user.email)
   end
 
+  it 'can connect to a different existing user account' do
+    authenticator = Auth::TwitterAuthenticator.new
+    user1 = Fabricate(:user)
+    user2 = Fabricate(:user)
+
+    TwitterUserInfo.create!(user_id: user1.id, twitter_user_id: 100, screen_name: "boris")
+
+    hash = {
+      info: {
+        "email" => user1.email,
+        "username" => "test",
+        "name" => "test",
+        "nickname" => "minion",
+      },
+      "uid" => "100"
+    }
+
+    result = authenticator.after_authenticate(hash, existing_account: user2)
+
+    expect(result.user.id).to eq(user2.id)
+    expect(TwitterUserInfo.exists?(user_id: user1.id)).to eq(false)
+    expect(TwitterUserInfo.exists?(user_id: user2.id)).to eq(true)
+  end
+
+  context 'revoke' do
+    let(:user) { Fabricate(:user) }
+    let(:authenticator) { Auth::TwitterAuthenticator.new }
+
+    it 'raises exception if no entry for user' do
+      expect { authenticator.revoke(user) }.to raise_error(Discourse::NotFound)
+    end
+
+      it 'revokes correctly' do
+        TwitterUserInfo.create!(user_id: user.id, twitter_user_id: 100, screen_name: "boris")
+        expect(authenticator.can_revoke?).to eq(true)
+        expect(authenticator.revoke(user)).to eq(true)
+        expect(authenticator.description_for_user(user)).to eq("")
+      end
+
+  end
+
 end