Bug 1176399 - Multiple requests for master password when GMail OAuth2 is enabled. r=mkmelin
authorPhilipp Kewisch <mozilla@kewis.ch>
Thu, 24 Nov 2016 02:07:21 +0100
changeset 24504 749cb8eaaea2
parent 24503 e604cdcec085
child 24505 6307e1b793f2
push id14750
push usermozilla@jorgk.com
push dateFri, 17 Aug 2018 22:20:45 +0000
treeherdercomm-central@6307e1b793f2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmkmelin
bugs1176399
Bug 1176399 - Multiple requests for master password when GMail OAuth2 is enabled. r=mkmelin
mail/components/im/modules/chatHandler.jsm
mailnews/base/public/msgIOAuth2Module.idl
mailnews/base/public/nsIMsgAsyncPrompter.idl
mailnews/base/src/msgAsyncPrompter.js
mailnews/base/src/msgOAuth2Module.js
mailnews/compose/src/nsSmtpProtocol.cpp
mailnews/imap/src/nsImapProtocol.cpp
mailnews/imap/src/nsSyncRunnableHelpers.cpp
mailnews/local/src/nsPop3Protocol.cpp
mailnews/news/src/nsNNTPProtocol.cpp
--- a/mail/components/im/modules/chatHandler.jsm
+++ b/mail/components/im/modules/chatHandler.jsm
@@ -28,16 +28,19 @@ var ChatCore = {
     Services.obs.addObserver(this, "contact-removed");
 
     // The initialization of the im core may trigger a master password prompt,
     // so wrap it with the async prompter service. Note this service already
     // waits for the asynchronous initialization of the password service.
     Cc["@mozilla.org/messenger/msgAsyncPrompter;1"]
       .getService(Ci.nsIMsgAsyncPrompter)
       .queueAsyncAuthPrompt("im", false, {
+      onPromptStartAsync: function(callback) {
+        callback.onAuthResult(this.onPromptStart());
+      },
       onPromptStart: function() {
         Services.core.init();
 
         // Find the accounts that exist in the im account service but
         // not in nsMsgAccountManager. They have probably been lost if
         // the user has used an older version of Thunderbird on a
         // profile with IM accounts. See bug 736035.
         let accountsById = {};
--- a/mailnews/base/public/msgIOAuth2Module.idl
+++ b/mailnews/base/public/msgIOAuth2Module.idl
@@ -9,17 +9,20 @@ interface nsIMsgIncomingServer;
 interface nsISmtpServer;
 
 /**
  * A listener callback for OAuth2 SASL authentication. This would be represented
  * as a promise, but this needs to be consumed by C++ code.
  */
 [scriptable, uuid(9a088b49-bc13-4f99-9478-053a6a43e370)]
 interface msgIOAuth2ModuleListener : nsISupports {
-  /// Called on successful OAuth2 authentication with the bearer token to use.
+  /**
+   * Called on successful OAuth2 authentication with the base64-encoded
+   * string to send as the client initial response for SASL XOAUTH2.
+   */
   void onSuccess(in ACString aBearerToken);
 
   /// Called on failed OAuth2 authentication.
   void onFailure(in nsresult aError);
 };
 
 /**
  * An interface for managing the responsibilities of using OAuth2 to produce a
@@ -41,19 +44,13 @@ interface msgIOAuth2Module : nsISupports
 
   /**
    * Connect to the OAuth2 server to get an access token.
    * @param aWithUI   If false, do not allow a dialog to be popped up to query
    *                  for a password.
    * @param aCallback Listener that handles the async response.
    */
   void connect(in boolean aWithUI, in msgIOAuth2ModuleListener aCallback);
-
-  /**
-   * Return the base64-encoded string to send as the client initial response for
-   * SASL XOAUTH2.
-   */
-  ACString buildXOAuth2String();
 };
 
 %{C++
 #define MSGIOAUTH2MODULE_CONTRACTID "@mozilla.org/mail/oauth2-module;1"
 %}
--- a/mailnews/base/public/nsIMsgAsyncPrompter.idl
+++ b/mailnews/base/public/nsIMsgAsyncPrompter.idl
@@ -30,31 +30,47 @@ interface nsIMsgAsyncPrompter : nsISuppo
    *                           immediately may not be synchronously, on OS/X.
    * @param aCaller An nsIMsgAsyncPromptListener to call back to when the prompt
    *                is ready to be made.
    */
   void queueAsyncAuthPrompt(in ACString aKey, in boolean aPromptImmediately,
                             in nsIMsgAsyncPromptListener aCaller);
 };
 
+[scriptable, function, uuid(acca94c9-378e-46e3-9a91-6655bf9c91a3)]
+interface nsIMsgAsyncPromptCallback : nsISupports {
+  /**
+   * Called when an auth result is available. Can be passed as a function.
+   *
+   * @param aResult   True if there is auth information available following the
+   *                    prompt, false otherwise.
+   */
+  void onAuthResult(in boolean aResult);
+};
+
 /**
  * This is used in combination with nsIMsgAsyncPrompter.
  */
 [scriptable, uuid(fb5307a3-39d0-462e-92c8-c5c288a2612f)]
 interface nsIMsgAsyncPromptListener : nsISupports {
   /**
-   * Called when the listener should do its prompt. The listener
-   * should not return until the prompt is complete.
-   *
-   * @return  True if there is auth information available following the prompt,
-   *          false otherwise.
+   * This method has been deprecated, please use onPromptStartAsync instead.
    */
   boolean onPromptStart();
 
   /**
+   * Called when the listener should do its prompt. This can happen
+   * synchronously or asynchronously, but in any case when done the callback
+   * method should be called.
+   *
+   * @param aCallback   The callback to execute when auth prompt has completed.
+   */
+  void onPromptStartAsync(in nsIMsgAsyncPromptCallback aCallback);
+
+  /**
    * Called in the case that the queued prompt was combined with another and
    * there is now authentication information available.
    */
   void onPromptAuthAvailable();
 
   /**
    * Called in the case that the queued prompt was combined with another but
    * the prompt was canceled.
--- a/mailnews/base/src/msgAsyncPrompter.js
+++ b/mailnews/base/src/msgAsyncPrompter.js
@@ -1,48 +1,67 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+ChromeUtils.import("resource://gre/modules/Deprecated.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/Task.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource:///modules/gloda/log4moz.js");
 
 function runnablePrompter(asyncPrompter, hashKey) {
   this._asyncPrompter = asyncPrompter;
   this._hashKey = hashKey;
 }
 
 runnablePrompter.prototype = {
   _asyncPrompter: null,
   _hashKey: null,
 
+  _promiseAuthPrompt: function(listener) {
+    return new Promise((resolve, reject) => {
+      try {
+        listener.onPromptStartAsync({ onAuthResult: resolve });
+      } catch (e) {
+        if (e.result == Components.results.NS_ERROR_XPC_JSOBJECT_HAS_NO_FUNCTION_NAMED) {
+          // Fall back to onPromptStart, for add-ons compat
+          Deprecated.warning("onPromptStart has been replaced by onPromptStartAsync",
+                             "https://bugzilla.mozilla.org/show_bug.cgi?id=1176399");
+          let ok = listener.onPromptStart();
+          resolve(ok);
+        } else {
+          reject(e);
+        }
+      }
+    });
+  },
+
   async run() {
     await Services.logins.initializationPromise;
     this._asyncPrompter._log.debug("Running prompt for " + this._hashKey);
     let prompter = this._asyncPrompter._pendingPrompts[this._hashKey];
     let ok = false;
     try {
-      ok = prompter.first.onPromptStart();
-    }
-    catch (ex) {
+      ok = await this._promiseAuthPrompt(prompter.first);
+    } catch (ex) {
       Cu.reportError("runnablePrompter:run: " + ex + "\n");
+      prompter.first.onPromptCanceled();
     }
 
     delete this._asyncPrompter._pendingPrompts[this._hashKey];
 
     for (var consumer of prompter.consumers) {
       try {
-        if (ok)
+        if (ok) {
           consumer.onPromptAuthAvailable();
-        else
+        } else {
           consumer.onPromptCanceled();
-      }
-      catch (ex) {
+        }
+      } catch (ex) {
         // Log the error for extension devs and others to pick up.
         Cu.reportError("runnablePrompter:run: consumer.onPrompt* reported an exception: " + ex + "\n");
       }
     }
     this._asyncPrompter._asyncPromptInProgress--;
 
     this._asyncPrompter._log.debug("Finished running prompter for " + this._hashKey);
     this._asyncPrompter._doAsyncAuthPrompt();
--- a/mailnews/base/src/msgOAuth2Module.js
+++ b/mailnews/base/src/msgOAuth2Module.js
@@ -117,30 +117,53 @@ OAuth2Module.prototype = {
           loginMgr.modifyLogin(login, propBag);
         }
         else
           loginMgr.removeLogin(login);
         return token;
       }
     }
 
-    // Otherwise, we need a new login, so create one and fill it in.
-    let login = Cc["@mozilla.org/login-manager/loginInfo;1"]
-                  .createInstance(Ci.nsILoginInfo);
-    login.init(this._loginUrl, null, this._scope, this._username, token,
-      '', '');
-    loginMgr.addLogin(login);
+    // Unless the token is null, we need to create and fill in a new login
+    if (token) {
+      let login = Cc["@mozilla.org/login-manager/loginInfo;1"]
+                    .createInstance(Ci.nsILoginInfo);
+      login.init(this._loginUrl, null, this._scope, this._username, token,
+        '', '');
+      loginMgr.addLogin(login);
+    }
     return token;
   },
 
   connect(aWithUI, aListener) {
-    this._oauth.connect(() => aListener.onSuccess(this._oauth.accessToken),
-                        x => aListener.onFailure(x),
-                        aWithUI, false);
-  },
+    let oauth = this._oauth;
+    let promptlistener = {
+      onPromptStartAsync: function(callback) {
+        this.onPromptAuthAvailable(callback);
+      },
 
-  buildXOAuth2String() {
-    return btoa("user=" + this._username + "\x01auth=Bearer " +
-      this._oauth.accessToken + "\x01\x01");
+      onPromptAuthAvailable: (callback) => {
+        oauth.connect(() => {
+          aListener.onSuccess(btoa(`user=${this._username}\x01auth=Bearer ${oauth.accessToken}\x01\x01`));
+          if (callback) {
+            callback.onAuthResult(true);
+          }
+        }, () => {
+          aListener.onFailure(Components.results.NS_ERROR_ABORT);
+          if (callback) {
+            callback.onAuthResult(false);
+          }
+        }, aWithUI, false);
+      },
+      onPromptCanceled: function() {
+        aListener.onFailure(Components.results.NS_ERROR_ABORT);
+      },
+      onPromptStart: function() {}
+    };
+
+    let asyncprompter = Components.classes["@mozilla.org/messenger/msgAsyncPrompter;1"]
+                                  .getService(Components.interfaces.nsIMsgAsyncPrompter);
+    let promptkey = this._loginUrl + "/" + this._username;
+    asyncprompter.queueAsyncAuthPrompt(promptkey, false, promptlistener);
   },
 };
 
 var NSGetFactory = XPCOMUtils.generateNSGetFactory([OAuth2Module]);
--- a/mailnews/compose/src/nsSmtpProtocol.cpp
+++ b/mailnews/compose/src/nsSmtpProtocol.cpp
@@ -1639,28 +1639,25 @@ nsresult nsSmtpProtocol::AuthOAuth2Step1
 
   nsresult rv = mOAuth2Support->Connect(true, this);
   NS_ENSURE_SUCCESS(rv, rv);
 
   m_nextState = SMTP_SUSPENDED;
   return NS_OK;
 }
 
-nsresult nsSmtpProtocol::OnSuccess(const nsACString &aAccessToken)
+nsresult nsSmtpProtocol::OnSuccess(const nsACString &aOAuth2String)
 {
   MOZ_ASSERT(mOAuth2Support, "Can't do anything without OAuth2 support");
 
-  nsCString base64Str;
-  mOAuth2Support->BuildXOAuth2String(base64Str);
-
   // Send the AUTH XOAUTH2 command, and then siphon us back to the regular
   // authentication login stream.
   nsAutoCString buffer;
   buffer.AppendLiteral("AUTH XOAUTH2 ");
-  buffer += base64Str;
+  buffer += aOAuth2String;
   buffer += CRLF;
   nsresult rv = SendData(buffer.get(), true);
   if (NS_FAILED(rv))
   {
     m_nextState = SMTP_ERROR_DONE;
   }
   else
   {
--- a/mailnews/imap/src/nsImapProtocol.cpp
+++ b/mailnews/imap/src/nsImapProtocol.cpp
@@ -8472,16 +8472,23 @@ nsresult nsImapProtocol::GetPassword(nsS
       password = m_password;
     }
   }
   if (!password.IsEmpty())
     m_lastPasswordSent = password;
   return rv;
 }
 
+NS_IMETHODIMP nsImapProtocol::OnPromptStartAsync(nsIMsgAsyncPromptCallback *aCallback)
+{
+  bool result = false;
+  OnPromptStart(&result);
+  return aCallback->OnAuthResult(result);
+}
+
 // This is called from the UI thread.
 NS_IMETHODIMP
 nsImapProtocol::OnPromptStart(bool *aResult)
 {
   nsresult rv;
   nsCOMPtr<nsIImapIncomingServer> imapServer = do_QueryReferent(m_server, &rv);
   NS_ENSURE_SUCCESS(rv, rv);
   nsCOMPtr<nsIMsgWindow> msgWindow;
--- a/mailnews/imap/src/nsSyncRunnableHelpers.cpp
+++ b/mailnews/imap/src/nsSyncRunnableHelpers.cpp
@@ -573,23 +573,23 @@ void OAuth2ThreadHelper::Connect()
   // immediately so that IMAP can react.
   if (NS_FAILED(rv))
   {
     MonitorAutoLock lockGuard(mMonitor);
     mMonitor.Notify();
   }
 }
 
-nsresult OAuth2ThreadHelper::OnSuccess(const nsACString &aAccessToken)
+nsresult OAuth2ThreadHelper::OnSuccess(const nsACString &aOAuth2String)
 {
   MOZ_ASSERT(NS_IsMainThread(), "Can't touch JS off-main-thread");
   MonitorAutoLock lockGuard(mMonitor);
 
   MOZ_ASSERT(mOAuth2Support, "Should not be here if no OAuth2 support");
-  mOAuth2Support->BuildXOAuth2String(mOAuth2String);
+  mOAuth2String = aOAuth2String;
   mMonitor.Notify();
   return NS_OK;
 }
 
 nsresult OAuth2ThreadHelper::OnFailure(nsresult aError)
 {
   MOZ_ASSERT(NS_IsMainThread(), "Can't touch JS off-main-thread");
   MonitorAutoLock lockGuard(mMonitor);
--- a/mailnews/local/src/nsPop3Protocol.cpp
+++ b/mailnews/local/src/nsPop3Protocol.cpp
@@ -767,16 +767,23 @@ nsresult nsPop3Protocol::StartGetAsyncPa
 
   rv = asyncPrompter->QueueAsyncAuthPrompt(server, false, this);
   // Explicit NS_ENSURE_SUCCESS for debug purposes as errors tend to get
   // hidden.
   NS_ENSURE_SUCCESS(rv, rv);
   return rv;
 }
 
+NS_IMETHODIMP nsPop3Protocol::OnPromptStartAsync(nsIMsgAsyncPromptCallback *aCallback)
+{
+  bool result = false;
+  OnPromptStart(&result);
+  return aCallback->OnAuthResult(result);
+}
+
 NS_IMETHODIMP nsPop3Protocol::OnPromptStart(bool *aResult)
 {
   MOZ_LOG(POP3LOGMODULE, LogLevel::Debug, (POP3LOG("OnPromptStart()")));
 
   *aResult = false;
 
   nsresult rv;
   nsCOMPtr<nsIMsgIncomingServer> server = do_QueryInterface(m_pop3Server, &rv);
--- a/mailnews/news/src/nsNNTPProtocol.cpp
+++ b/mailnews/news/src/nsNNTPProtocol.cpp
@@ -2491,16 +2491,23 @@ nsresult nsNNTPProtocol::PasswordRespons
     HandleAuthenticationFailure();
     return NS_OK;
   }
 
   NS_ERROR("should never get here");
   return NS_ERROR_FAILURE;
 }
 
+NS_IMETHODIMP nsNNTPProtocol::OnPromptStartAsync(nsIMsgAsyncPromptCallback *aCallback)
+{
+  bool result = false;
+  OnPromptStart(&result);
+  return aCallback->OnAuthResult(result);
+}
+
 NS_IMETHODIMP nsNNTPProtocol::OnPromptStart(bool *authAvailable)
 {
   NS_ENSURE_ARG_POINTER(authAvailable);
   NS_ENSURE_STATE(m_nextState == NNTP_SUSPENDED);
 
   if (!m_newsFolder)
   {
     // If we don't have a news folder, we may have been closed already.