Bug 849540, part 2: Implement an OAuth2 SASL module helper, r=rkent.
authorJoshua Cranmer <Pidgeot18@gmail.com>
Mon, 23 Feb 2015 01:01:11 -0600
changeset 17557 bdc11668c7ab4930661a295e5510612bba53cc69
parent 17556 47051fbe6edd14406a3f72501b6b6797c8aef79d
child 17558 1e5560a1eb9688fb79d6a885de4883ce1afd7f80
push id10804
push userPidgeot18@gmail.com
push dateMon, 23 Feb 2015 07:03:52 +0000
treeherdercomm-central@bdc11668c7ab [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrkent
bugs849540
Bug 849540, part 2: Implement an OAuth2 SASL module helper, r=rkent. The code added in msgOAuth2Module.js is glue functionality to use the configuration preferences to run the OAuth2.jsm logon process. It also makes the process usable from C++, so that it can be consumed by IMAP and SMTP implementations. At present, the value for GMail is hardcoded. This is hopefully a temporary measure until dynamic client configuration is supported, at which point it can be removed. Unfortunately, this also requires us to hardcode the identifier for the client instead of using, say, navigator.appName. CLOSED TREE push for SeaMonkey localization changes.
mail/locales/en-US/chrome/messenger/messenger.properties
mailnews/base/public/moz.build
mailnews/base/public/msgIOAuth2Module.idl
mailnews/base/src/moz.build
mailnews/base/src/msgBase.manifest
mailnews/base/src/msgOAuth2Module.js
suite/locales/en-US/chrome/mailnews/messenger.properties
--- a/mail/locales/en-US/chrome/messenger/messenger.properties
+++ b/mail/locales/en-US/chrome/messenger/messenger.properties
@@ -125,16 +125,22 @@ authPasswordCleartextViaSSL=Normal passw
 authPasswordEncrypted=Encrypted password
 authKerberos=Kerberos / GSSAPI
 authExternal=TLS Certificate
 authNTLM=NTLM
 authOAuth2=OAuth2
 authAnySecure=Any secure method (deprecated)
 authAny=Any method (insecure)
 
+# OAuth2 window title
+# LOCALIZATION NOTE(oauth2WindowTitle):
+# %1$S is the username (or full email address) used for authentication.
+# %2$S is the hostname of the account being authenticated.
+oauth2WindowTitle=Enter credentials for %1$S on %2$S
+
 # LOCALIZATION NOTE(serverType-nntp): Do not translate "NNTP" in the line below
 serverType-nntp=News Server (NNTP)
 # LOCALIZATION NOTE(serverType-pop3): Do not translate "POP" in the line below
 serverType-pop3=POP Mail Server
 # LOCALIZATION NOTE(serverType-imap): Do not translate "IMAP" in the line below
 serverType-imap=IMAP Mail Server
 serverType-none=Local Mail Store
 # LOCALIZATION NOTE(serverType-movemail): DONT_TRANSLATE
--- a/mailnews/base/public/moz.build
+++ b/mailnews/base/public/moz.build
@@ -2,16 +2,17 @@
 # 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/.
 
 XPIDL_SOURCES += [
     'MailNewsTypes2.idl',
     'mozINewMailListener.idl',
     'mozINewMailNotificationService.idl',
+    'msgIOAuth2Module.idl',
     'nsICopyMessageListener.idl',
     'nsICopyMsgStreamListener.idl',
     'nsIFolderListener.idl',
     'nsIFolderLookupService.idl',
     'nsIIncomingServerListener.idl',
     'nsIMapiRegistry.idl',
     'nsIMessenger.idl',
     'nsIMessengerMigrator.idl',
new file mode 100644
--- /dev/null
+++ b/mailnews/base/public/msgIOAuth2Module.idl
@@ -0,0 +1,59 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+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.
+  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
+ * bearer token, for use in SASL steps.
+ */
+[scriptable, uuid(68c275f8-cfa7-4622-b279-af290616cae6)]
+interface msgIOAuth2Module : nsISupports {
+  /**
+   * Initialize the OAuth2 parameters from an SMTP server, and return whether or
+   * not we can authenticate with OAuth2.
+   */
+  bool initFromSmtp(in nsISmtpServer aSmtpServer);
+
+  /**
+   * Initialize the OAuth2 parameters from an incoming server, and return
+   * whether or not we can authenticate with OAuth2.
+   */
+  bool initFromMail(in nsIMsgIncomingServer aServer);
+
+  /**
+   * 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/src/moz.build
+++ b/mailnews/base/src/moz.build
@@ -63,16 +63,17 @@ if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'gtk2
     SOURCES += ['nsMessengerUnixIntegration.cpp']
 elif CONFIG['MOZ_WIDGET_TOOLKIT'] == 'cocoa':
     SOURCES += ['nsMessengerOSXIntegration.mm']
 
 EXTRA_COMPONENTS += [
     'folderLookupService.js',
     'msgAsyncPrompter.js',
     'msgBase.manifest',
+    'msgOAuth2Module.js',
     'newMailNotificationService.js',
     'nsMailNewsCommandLineHandler.js',
 ]
 
 EXTRA_JS_MODULES += [
     'virtualFolderWrapper.js',
 ]
 
--- a/mailnews/base/src/msgBase.manifest
+++ b/mailnews/base/src/msgBase.manifest
@@ -3,8 +3,10 @@ contract @mozilla.org/messenger/msgAsync
 component {2f86d554-f9d9-4e76-8eb7-243f047333ee} nsMailNewsCommandLineHandler.js
 contract @mozilla.org/commandlinehandler/general-startup;1?type=mail {2f86d554-f9d9-4e76-8eb7-243f047333ee}
 category command-line-handler m-mail @mozilla.org/commandlinehandler/general-startup;1?type=mail
 component {740880E6-E299-4165-B82F-DF1DCAB3AE22} newMailNotificationService.js
 contract @mozilla.org/newMailNotificationService;1 {740880E6-E299-4165-B82F-DF1DCAB3AE22}
 category profile-after-change NewMailNotificationService @mozilla.org/newMailNotificationService;1 
 component {a30be08c-afc8-4fed-9af7-79778a23db23} folderLookupService.js
 contract @mozilla.org/mail/folder-lookup;1 {a30be08c-afc8-4fed-9af7-79778a23db23}
+component {b63d8e4c-bf60-439b-be0e-7c9f67291042} msgOAuth2Module.js
+contract @mozilla.org/mail/oauth2-module;1 {b63d8e4c-bf60-439b-be0e-7c9f67291042}
new file mode 100644
--- /dev/null
+++ b/mailnews/base/src/msgOAuth2Module.js
@@ -0,0 +1,139 @@
+/* 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/. */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+Components.utils.import("resource://gre/modules/OAuth2.jsm");
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+function OAuth2Module() {
+  this._refreshToken = '';
+}
+OAuth2Module.prototype = {
+  // XPCOM registration stuff
+  QueryInterface: XPCOMUtils.generateQI([Ci.msgIOAuth2Module]),
+  classID: Components.ID("{b63d8e4c-bf60-439b-be0e-7c9f67291042}"),
+
+  _loadOAuthClientDetails(aIssuer) {
+    if (aIssuer == "accounts.google.com") {
+      // For the moment, these details are hard-coded, since Google does not
+      // provide dynamic client registration. Don't copy these values for your
+      // own application--register it yourself. This code (and possibly even the
+      // registration itself) will disappear when this is switched to dynamic
+      // client registration.
+      this._appKey = '572172754692-vfo2oqvu2oju9be729s915glghp1vpfj.apps.googleusercontent.com';
+      this._appSecret = 'YpeuM0eYPQe_r98HZ7p16zUm';
+      this._authURI = "https://accounts.google.com/o/oauth2/auth";
+      this._tokenURI = "https://www.googleapis.com/oauth2/v3/token";
+    } else {
+      throw Cr.NS_ERROR_INVALID_ARGUMENT;
+    }
+  },
+  initFromSmtp(aServer) {
+    return this._initPrefs("mail.smtpserver." + aServer.key + ".",
+      aServer.username, aServer.hostname);
+  },
+  initFromMail(aServer) {
+    return this._initPrefs("mail.server." + aServer.key + ".",
+      aServer.username, aServer.realHostName);
+  },
+  _initPrefs(root, aUsername, aHostname) {
+    // Load all of the parameters from preferences.
+    let issuer = Preferences.get(root + "oauth2.issuer", "");
+    let scope = Preferences.get(root + "oauth2.scope", "");
+
+    // These properties are absolutely essential to OAuth2 support. If we don't
+    // have them, we don't support OAuth2.
+    if (!issuer || !scope)
+      return false;
+
+    // Find the app key we need for the OAuth2 string. Eventually, this should
+    // be using dynamic client registration, but there are no current
+    // implementations that we can test this with.
+    this._loadOAuthClientDetails(issuer);
+
+    // Username is needed to generate the XOAUTH2 string.
+    this._username = aUsername;
+    // LoginURL is needed to save the refresh token in the password manager.
+    this._loginUrl = "oauth://" + issuer;
+    // We use the scope to indicate the realm.
+    this._scope = scope;
+
+    // Define the OAuth property and store it.
+    this._oauth = new OAuth2(this._authURI, scope, this._appKey,
+      this._appSecret);
+    this._oauth.authURI = this._authURI;
+    this._oauth.tokenURI = this._tokenURI;
+
+    // Try hinting the username...
+    this._oauth.extraAuthParams = [
+      ["login_hint", aUsername]
+    ];
+
+    // Set the window title to something more useful than "Unnamed"
+    this._oauth.requestWindowTitle =
+      Services.strings.createBundle("chrome://messenger/locale/messenger.properties")
+                      .formatStringFromName("oauth2WindowTitle",
+                                            [aUsername, aHostname], 2);
+
+    // This stores the refresh token in the login manager.
+    Object.defineProperty(this._oauth, "refreshToken", {
+      get: () => this.refreshToken,
+      set: (token) => this.refreshToken = token
+    });
+
+    return true;
+  },
+
+  get refreshToken() {
+    let loginMgr = Cc["@mozilla.org/login-manager;1"]
+                     .getService(Ci.nsILoginManager);
+    let logins = loginMgr.findLogins({}, this._loginUrl, null, this._scope);
+    for (let login of logins) {
+      if (login.username == this._username)
+        return login.password;
+    }
+    return '';
+  },
+  set refreshToken(token) {
+    let loginMgr = Cc["@mozilla.org/login-manager;1"]
+                     .getService(Ci.nsILoginManager);
+
+    // Check if we already have a login with this username, and modify the
+    // password on that, if we do.
+    let logins = loginMgr.findLogins({}, this._loginUrl, null, this._scope);
+    for (let login of logins) {
+      if (login.username == this._username)
+        loginMgr.modifyLogin(login, {password: token});
+      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);
+    return token;
+  },
+
+  connect(aWithUI, aListener) {
+    this._oauth.connect(() => aListener.onSuccess(this._oauth.accessToken),
+                        x => aListener.onFailure(x),
+                        aWithUI, false);
+  },
+
+  buildXOAuth2String() {
+    return btoa("user=" + this._username + "\x01auth=Bearer " +
+      this._oauth.accessToken + "\x01\x01");
+  },
+};
+
+var NSGetFactory = XPCOMUtils.generateNSGetFactory([OAuth2Module]);
--- a/suite/locales/en-US/chrome/mailnews/messenger.properties
+++ b/suite/locales/en-US/chrome/mailnews/messenger.properties
@@ -123,16 +123,22 @@ authPasswordCleartextViaSSL=Normal passw
 authPasswordEncrypted=Encrypted password
 authKerberos=Kerberos / GSSAPI
 authExternal=TLS Certificate
 authNTLM=NTLM
 authOAuth2=OAuth2
 authAnySecure=Any secure method (deprecated)
 authAny=Any method (insecure)
 
+# OAuth2 window title
+# LOCALIZATION NOTE(oauth2WindowTitle):
+# %1$S is the username (or full email address) used for authentication.
+# %2$S is the hostname of the account being authenticated.
+oauth2WindowTitle=Enter credentials for %1$S on %2$S
+
 # LOCALIZATION NOTE(serverType-nntp): Do not translate "NNTP" in the line below
 serverType-nntp=News Server (NNTP)
 # LOCALIZATION NOTE(serverType-pop3): Do not translate "POP" in the line below
 serverType-pop3=POP Mail Server
 # LOCALIZATION NOTE(serverType-imap): Do not translate "IMAP" in the line below
 serverType-imap=IMAP Mail Server
 serverType-none=Local Mail Store
 # LOCALIZATION NOTE(serverType-movemail): DONT_TRANSLATE