Fix bug 1079783 - OAuth2 forgets token when offline and a few other minor OAuth2.jsm fixes. r=mconley,r=clokep,a=Standard8
authorPhilipp Kewisch <mozilla@kewis.ch>
Wed, 08 Oct 2014 12:39:43 +0200
changeset 20885 52827cae72f6137d525af88732d612c1a9fb8bfc
parent 20884 0c70f8c7a8643bde38702bc671810c15370d3c90
child 20886 efc7336b2a17638ff72b69943ebbd320b98dc1fb
child 20888 dce40c9f24d8353b43cf274c291b3abbaf069306
push id1247
push usermbanner@mozilla.com
push dateSun, 09 Nov 2014 21:15:35 +0000
treeherdercomm-beta@52827cae72f6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmconley, clokep, Standard8
bugs1079783
Fix bug 1079783 - OAuth2 forgets token when offline and a few other minor OAuth2.jsm fixes. r=mconley,r=clokep,a=Standard8
calendar/providers/caldav/calDavCalendar.js
mail/components/cloudfile/nsBox.js
mailnews/base/util/OAuth2.jsm
--- a/calendar/providers/caldav/calDavCalendar.js
+++ b/calendar/providers/caldav/calDavCalendar.js
@@ -1577,20 +1577,25 @@ calDavCalendar.prototype = {
                 onPromptCanceled: authFailed
             };
             let asyncprompter = Components.classes["@mozilla.org/messenger/msgAsyncPrompter;1"]
                                           .getService(Components.interfaces.nsIMsgAsyncPrompter);
             asyncprompter.queueAsyncAuthPrompt(self.uri.spec, false, promptlistener);
         }
         if (this.mUri.host == "apidata.googleusercontent.com") {
             if (!this.oauth) {
+                let sessionId = this.id;
+                let pwMgrId = "Google CalDAV v2";
+                let authTitle = cal.calGetString("commonDialogs", "EnterUserPasswordFor",
+                                                 [this.name], "global");
+
                 this.oauth = new OAuth2(OAUTH_BASE_URI, OAUTH_SCOPE,
                                         OAUTH_CLIENT_ID, OAUTH_HASH);
-                let sessionId = this.id;
-                let pwMgrId = "Google CalDAV v2";
+                this.oauth.requestWindowTitle = authTitle;
+                this.oauth.requestWindowFeatures = "chrome,private,centerscreen,width=430,height=600";
 
                 Object.defineProperty(this.oauth, "refreshToken", {
                     get: function getRefreshToken() {
                         if (!this.mRefreshToken) {
                             var pass = { value: null };
                             try {
                                 cal.auth.passwordManagerGet(sessionId, pass, sessionId, pwMgrId);
                             } catch (e if e.result == Components.results.NS_ERROR_ABORT) {
--- a/mail/components/cloudfile/nsBox.js
+++ b/mail/components/cloudfile/nsBox.js
@@ -24,17 +24,18 @@ const kAuthBaseUrl = "https://www.box.co
 const kAuthUrl = "oauth2/authorize";
 
 XPCOMUtils.defineLazyServiceGetter(this, "gProtocolService",
                                    "@mozilla.org/uriloader/external-protocol-service;1",
                                    "nsIExternalProtocolService");
 
 function nsBox() {
   this.log = Log4Moz.getConfiguredLogger("BoxService");
-  this._oauth = new OAuth2(kAuthBaseUrl, null, kClientId, kClientSecret, kAuthUrl);
+  this._oauth = new OAuth2(kAuthBaseUrl, null, kClientId, kClientSecret);
+  this._oauth.authURI = kAuthBaseUrl + kAuthUrl;
 
   let account = this;
   Object.defineProperty(this._oauth, "refreshToken", {
     get: function getRefreshToken() {
       if (!this.mRefreshToken) {
         let authToken = cloudFileAccounts.getSecretValue(account.accountKey,
                                                          cloudFileAccounts.kTokenRealm);
         this.mRefreshToken = authToken || "";
--- a/mailnews/base/util/OAuth2.jsm
+++ b/mailnews/base/util/OAuth2.jsm
@@ -11,46 +11,46 @@ const {classes: Cc, interfaces: Ci, resu
 
 Cu.import("resource://gre/modules/Http.jsm");
 Cu.import("resource:///modules/Services.jsm");
 Cu.import("resource:///modules/XPCOMUtils.jsm");
 Cu.import("resource:///modules/gloda/log4moz.js");
 
 function parseURLData(aData) {
   let result = {};
-  aData.split("?", 2)[1].split("&").forEach(function (aParam) {
+  aData.split(/[?#]/, 2)[1].split("&").forEach(function (aParam) {
     let [key, value] = aParam.split("=");
     result[key] = value;
   });
   return result;
 }
 
-function OAuth2(aBaseURI, aScope, aAppKey, aAppSecret, aAuthURI="oauth2/auth") {
-    this.baseURI = aBaseURI;
-    this.authURI = aBaseURI + aAuthURI;
+function OAuth2(aBaseURI, aScope, aAppKey, aAppSecret) {
+    this.authURI = aBaseURI + "oauth2/auth";
     this.tokenURI = aBaseURI + "oauth2/token";
     this.consumerKey = aAppKey;
     this.consumerSecret = aAppSecret;
     this.scope = aScope;
+    this.extraAuthParams = [];
 
     this.log = Log4Moz.getConfiguredLogger("TBOAuth");
 }
 
 OAuth2.CODE_AUTHORIZATION = "authorization_code";
 OAuth2.CODE_REFRESH = "refresh_token";
 
 OAuth2.prototype = {
 
     responseType: "code",
     consumerKey: null,
     consumerSecret: null,
     completionURI: "http://localhost",
     requestWindowURI: "chrome://messenger/content/browserRequest.xul",
-    requestWindowHeight: 600,
-    requestWindowWidth: 980,
+    requestWindowFeatures: "chrome,private,centerscreen,width=980,height=600",
+    requestWindowTitle: "",
     scope: null,
 
     accessToken: null,
     refreshToken: null,
     tokenExpires: 0,
     connecting: false,
 
     connect: function connect(aSuccess, aFailure, aWithUI, aRefresh) {
@@ -81,21 +81,24 @@ OAuth2.prototype = {
             ["response_type", this.responseType],
             ["client_id", this.consumerKey],
             ["redirect_uri", this.completionURI],
         ];
         // The scope can be optional.
         if (this.scope) {
             params.push(["scope", this.scope]);
         }
-        params = params.map(function(p) p[0] + "=" + encodeURIComponent(p[1]))
-                       .join("&");
+
+        // Add extra parameters
+        params.push(...this.extraAuthParams);
+
+        // Now map the parameters to a string
+        params = params.map(([k,v]) => k + "=" + encodeURIComponent(v)).join("&");
 
         this._browserRequest = {
-            promptText: "auth prompt",
             account: this,
             url: this.authURI + "?" + params,
             _active: true,
             iconURI: "",
             cancelled: function() {
                 if (!this._active) {
                     return;
                 }
@@ -140,46 +143,51 @@ OAuth2.prototype = {
                       this._checkForRedirect(aLocation.spec);
                     },
                     onProgressChange: function() {},
                     onStatusChange: function() {},
                     onSecurityChange: function() {},
                 };
                 aWebProgress.addProgressListener(this._listener,
                                                  Ci.nsIWebProgress.NOTIFY_ALL);
+                aWindow.document.title = this.account.requestWindowTitle;
             }
         };
 
         this.wrappedJSObject = this._browserRequest;
-        let features = "chrome,private,centerscreen,width=" + this.requestWindowWidth + "px,height=" + this.requestWindowHeight + "px";
-        Services.ww.openWindow(null, this.requestWindowURI, null, features, this);
+        Services.ww.openWindow(null, this.requestWindowURI, null, this.requestWindowFeatures, this);
     },
     finishAuthorizationRequest: function() {
-      if (!("_browserRequest" in this))
-        return;
+        if (!("_browserRequest" in this)) {
+            return;
+        }
 
-      this._browserRequest._active = false;
-      if ("_listener" in this._browserRequest)
-        this._browserRequest._listener._cleanUp();
-      delete this._browserRequest;
+        this._browserRequest._active = false;
+        if ("_listener" in this._browserRequest) {
+            this._browserRequest._listener._cleanUp();
+        }
+        delete this._browserRequest;
     },
 
     onAuthorizationReceived: function(aData) {
-      this.log.info("authorization received" + aData);
-      let results = parseURLData(aData);
-      this.requestAccessToken(results.code, OAuth2.CODE_AUTHORIZATION);
+        this.log.info("authorization received" + aData);
+        let results = parseURLData(aData);
+        if (this.responseType == "code") {
+            this.requestAccessToken(results.code, OAuth2.CODE_AUTHORIZATION);
+        } else if (this.responseType == "token") {
+            this.onAccessTokenReceived(JSON.stringify(results));
+        }
     },
 
     onAuthorizationFailed: function(aError, aData) {
         this.connecting = false;
         this.connectFailureCallback(aData);
     },
 
     requestAccessToken: function requestAccessToken(aCode, aType) {
-
         let params = [
             ["client_id", this.consumerKey],
             ["client_secret", this.consumerSecret],
             ["grant_type", aType],
         ];
 
         if (aType == OAuth2.CODE_AUTHORIZATION) {
             params.push(["code", aCode]);
@@ -192,27 +200,33 @@ OAuth2.prototype = {
           postData: params,
           onLoad: this.onAccessTokenReceived.bind(this),
           onError: this.onAccessTokenFailed.bind(this)
         }
         httpRequest(this.tokenURI, options);
     },
 
     onAccessTokenFailed: function onAccessTokenFailed(aError, aData) {
-        this.refreshToken = null;
+        if (aError != "offline") {
+            this.refreshToken = null;
+        }
         this.connecting = false;
         this.connectFailureCallback(aData);
     },
 
     onAccessTokenReceived: function onRequestTokenReceived(aData) {
         let result = JSON.parse(aData);
 
         this.accessToken = result.access_token;
         if ("refresh_token" in result) {
             this.refreshToken = result.refresh_token;
         }
-        this.tokenExpires = (new Date()).getTime() + (result.expires_in * 1000);
+        if ("expires_in" in result) {
+            this.tokenExpires = (new Date()).getTime() + (result.expires_in * 1000);
+        } else {
+            this.tokenExpires = Number.MAX_VALUE;
+        }
         this.tokenType = result.token_type;
 
         this.connecting = false;
         this.connectSuccessCallback();
     }
 };