Bug 1023780 - Ensure that Mobile ID client support the 401/110 error (invalid token) properly. r=jedp, a=2.0+
authorFernando Jiménez <ferjmoreno@gmail.com>
Fri, 11 Jul 2014 14:56:56 +0200
changeset 208972 5b786c7afc790b66c4fd2299dc2d50b25acec571
parent 208971 e79e3269116d7147737d3e2d03e8bb5c261fb9fe
child 208973 5525d9118f72043c073b8e45eb9e0d2145f7eef6
push id494
push userraliiev@mozilla.com
push dateMon, 25 Aug 2014 18:42:16 +0000
treeherdermozilla-release@a3cc3e46b571 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjedp, 2
bugs1023780
milestone32.0a2
Bug 1023780 - Ensure that Mobile ID client support the 401/110 error (invalid token) properly. r=jedp, a=2.0+
services/mobileid/MobileIdentityManager.jsm
services/mobileid/tests/xpcshell/test_mobileid_manager.js
--- a/services/mobileid/MobileIdentityManager.jsm
+++ b/services/mobileid/MobileIdentityManager.jsm
@@ -288,31 +288,33 @@ this.MobileIdentityManager = {
       delete this.certificates[aSessionToken];
       deferred.resolve(kp);
     });
 
     return deferred.promise;
   },
 
   getCertificate: function(aSessionToken, aPublicKey) {
+    log.debug("getCertificate");
     if (this.certificates[aSessionToken] &&
         this.certificates[aSessionToken].validUntil > this.client.hawk.now()) {
       return Promise.resolve(this.certificates[aSessionToken].cert);
     }
 
     if (Services.io.offline) {
       return Promise.reject(ERROR_OFFLINE);
     }
 
     let validUntil = this.client.hawk.now() + KEY_LIFETIME;
     let deferred = Promise.defer();
     this.client.sign(aSessionToken, CERTIFICATE_LIFETIME,
                      aPublicKey)
     .then(
       (signedCert) => {
+        log.debug("Got signed certificate");
         this.certificates[aSessionToken] = {
           cert: signedCert.cert,
           validUntil: validUntil
         };
         deferred.resolve(signedCert.cert);
       },
       deferred.reject
     );
@@ -560,28 +562,31 @@ this.MobileIdentityManager = {
         );
         phoneInfoArray.push(phoneInfo);
       }
     }
 
     return this.ui.startFlow(aManifestURL, phoneInfoArray)
     .then(
       (result) => {
+        log.debug("startFlow result ${} ", result);
         if (!result ||
             (!result.phoneNumber && (result.serviceId === undefined))) {
           return Promise.reject(ERROR_INTERNAL_INVALID_PROMPT_RESULT);
         }
 
         let msisdn;
         let mcc;
 
         // If the user selected one of the existing SIM cards we have to check
         // that we either have the MSISDN for that SIM or we can do a silent
         // verification that does not require us to have the MSISDN in advance.
-        if (result.serviceId !== undefined) {
+        // result.serviceId can be "0".
+        if (result.serviceId !== undefined &&
+            result.serviceId !== null) {
           let icc = this.iccInfo[result.serviceId];
           log.debug("icc ${}", icc);
           if (!icc || !icc.msisdn && !icc.canDoSilentVerification) {
             return Promise.reject(ERROR_INTERNAL_CANNOT_VERIFY_SELECTION);
           }
           msisdn = icc.msisdn;
           mcc = icc.mcc;
         } else {
@@ -772,32 +777,36 @@ this.MobileIdentityManager = {
   getMobileIdAssertion: function(aPrincipal, aPromiseId, aOptions) {
     log.debug("getMobileIdAssertion ${}", aPrincipal);
 
     let uri = Services.io.newURI(aPrincipal.origin, null, null);
     let principal = securityManager.getAppCodebasePrincipal(
       uri, aPrincipal.appid, aPrincipal.isInBrowserElement);
     let manifestURL = appsService.getManifestURLByLocalId(aPrincipal.appId);
 
+    let _creds;
+
     // First of all we look if we already have credentials for this origin.
     // If we don't have credentials it means that it is the first time that
     // the caller requested an assertion.
     this.credStore.getByOrigin(aPrincipal.origin)
     .then(
       (creds) => {
         log.debug("creds ${creds} - ${origin}", { creds: creds,
                                                   origin: aPrincipal.origin });
         if (!creds || !creds.sessionToken) {
           log.debug("No credentials");
           return;
         }
 
+        _creds = creds;
+
         // Even if we already have credentials for this origin, the consumer
         // of the API might want to force the identity selection dialog.
-        if (aOptions.forceSelection) {
+        if (aOptions.forceSelection || aOptions.refreshCredentials) {
           return this.promptAndVerify(principal, manifestURL, creds)
           .then(
             (newCreds) => {
               return this.checkNewCredentials(creds, newCreds,
                                               principal.origin);
             }
           );
         }
@@ -906,17 +915,31 @@ this.MobileIdentityManager = {
           promiseId: aPromiseId,
           result: assertion
         });
       }
     )
     .then(
       null,
       (error) => {
-        log.error("getMobileIdAssertion rejected with " + error);
+        log.error("getMobileIdAssertion rejected with ${}", error);
+
+        // If we got an invalid token error means that the credentials that
+        // we have are not valid anymore and so we need to refresh them. We
+        // do that removing the stored credentials and starting over. We also
+        // make sure that we do this only once.
+        if (error === ERROR_INVALID_AUTH_TOKEN &&
+            !aOptions.refreshCredentials) {
+          log.debug("Need to get new credentials");
+          aOptions.refreshCredentials = true;
+          _creds && this.credStore.delete(_creds.msisdn);
+          this.getMobileIdAssertion(aPrincipal, aPromiseId, aOptions);
+          return;
+        }
+
         // Notify the error to the UI.
         this.ui.error(error);
 
         let mm = this.messageManagers[aPromiseId];
         mm.sendAsyncMessage("MobileId:GetAssertion:Return:KO", {
           promiseId: aPromiseId,
           error: error
         });
--- a/services/mobileid/tests/xpcshell/test_mobileid_manager.js
+++ b/services/mobileid/tests/xpcshell/test_mobileid_manager.js
@@ -189,18 +189,26 @@ MockCredStore.prototype = {
   _getByOriginResult: null,
 
   _getByMsisdnResult: null,
 
   _getByIccIdResult: null,
 
   getByOrigin: function() {
     this._spy("getByOrigin", arguments);
-    return Promise.resolve(this._options.getByOriginResult ||
-                           this._getByOriginResult);
+    let result = this._getByOriginResult;
+    if (this._options.getByOriginResult) {
+      if (Array.isArray(this._options.getByOriginResult)) {
+        result = this._options.getByOriginResult.length ?
+                 this._options.getByOriginResult.shift() : null;
+      } else {
+        result = this._options.getByOriginResult;
+      }
+    }
+    return Promise.resolve(result);
   },
 
   getByMsisdn: function() {
     this._spy("getByMsisdn", arguments);
     return Promise.resolve(this._options.getByMsisdnResult ||
                            this._getByMsisdnResult);
   },
 
@@ -218,41 +226,42 @@ MockCredStore.prototype = {
   setDeviceIccIds: function() {
     this._spy("setDeviceIccIds", arguments);
     return Promise.resolve();
   },
 
   removeOrigin: function() {
     this._spy("removeOrigin", arguments);
     return Promise.resolve();
+  },
+
+  delete: function() {
+    this._spy("delete", arguments);
+    return Promise.resolve();
   }
 };
 
 // Save original client instance.
 const kMobileIdentityClient = MobileIdentityManager.client;
 
 // Client mock up.
 let MockClient = function(aOptions) {
   Mock.call(this, aOptions);
 };
 
 MockClient.prototype = {
 
   __proto__: Mock.prototype,
 
   _discoverResult: {
-    verificationMethods: ["sms/momt", "sms/mt"],
+    verificationMethods: ["sms/mt"],
     verificationDetails: {
       "sms/mt": {
         mtSender: "123",
         url: "https://msisdn.accounts.firefox.com/v1/msisdn/sms/mt/verify"
-      },
-      "sms/momt": {
-        mtSender: "123",
-        moVerifier: "234"
       }
     }
   },
 
   _registerResult: {
     msisdnSessionToken: SESSION_TOKEN
   },
 
@@ -293,16 +302,22 @@ MockClient.prototype = {
   verifyCode: function() {
     this._spy("verifyCode", arguments);
     return Promise.resolve(this._options.verifyCodeResult ||
                            this._verifyCodeResult);
   },
 
   sign: function() {
     this._spy("sign", arguments);
+    if (this._options.signError) {
+      let error = Array.isArray(this._options.signError) ?
+                  this._options.signError.shift() :
+                  this._options.signError;
+      return Promise.reject(error);
+    }
     return Promise.resolve(this._options.signResult || this._signResult);
   }
 };
 
 // The test rely on having an app registered. Otherwise, it will throw.
 // Override XULAppInfo.
 const XUL_APP_INFO_UUID = Components.ID("{84fdc459-d96d-421c-9bff-a8193233ae75}");
 const XUL_APP_INFO_CONTRACT_ID = "@mozilla.org/xre/app-info;1";
@@ -369,27 +384,17 @@ add_test(function() {
   do_register_cleanup(cleanup);
 
   do_test_pending();
 
   let ui = new MockUi();
   MobileIdentityManager.ui = ui;
   let credStore = new MockCredStore();
   MobileIdentityManager.credStore = credStore;
-  let client = new MockClient({
-    discoverResult: {
-      verificationMethods: ["sms/mt"],
-      verificationDetails: {
-        "sms/mt": {
-          mtSender: "123",
-          url: "https://msisdn.accounts.firefox.com/v1/msisdn/sms/mt/verify"
-        }
-      }
-    }
-  });
+  let client = new MockClient();
   MobileIdentityManager.client = client;
 
   let promiseId = Date.now();
   let mm = {
     sendAsyncMessage: function(aMsg, aData) {
       do_print("sendAsyncMessage " + aMsg + " - " + JSON.stringify(aData));
 
       // Check result.
@@ -610,25 +615,16 @@ add_test(function() {
 
   let _sessionToken = Date.now();
 
   let ui = new MockUi();
   MobileIdentityManager.ui = ui;
   let credStore = new MockCredStore();
   MobileIdentityManager.credStore = credStore;
   let client = new MockClient({
-    discoverResult: {
-      verificationMethods: ["sms/mt"],
-      verificationDetails: {
-        "sms/mt": {
-          mtSender: "123",
-          url: "https://msisdn.accounts.firefox.com/v1/msisdn/sms/mt/verify"
-        }
-      }
-    },
     signResult: {
       cert: "aInvalidCert"
     },
     registerResult: {
       msisdnSessionToken: _sessionToken
     }
   });
   MobileIdentityManager.client = client;
@@ -808,17 +804,17 @@ add_test(function() {
     json: {
       promiseId: promiseId,
       options: {}
     }
   });
 });
 
 add_test(function() {
-  do_print("= Existing credentials - No Icc - Permission denied - OK result =");
+  do_print("= Existing credentials - No Icc - Permission denied - KO result =");
 
   do_register_cleanup(cleanup);
 
   do_test_pending();
 
   let ui = new MockUi();
   MobileIdentityManager.ui = ui;
   let credStore = new MockCredStore({
@@ -972,25 +968,16 @@ add_test(function() {
   });
   MobileIdentityManager.ui = ui;
   let credStore = new MockCredStore({
     getByOriginResult: existingCredentials
   });
   MobileIdentityManager.credStore = credStore;
   let client = new MockClient({
     verifyCodeResult: ANOTHER_PHONE_NUMBER,
-    discoverResult: {
-      verificationMethods: ["sms/mt"],
-      verificationDetails: {
-        "sms/mt": {
-          mtSender: "123",
-          url: "https://msisdn.accounts.firefox.com/v1/msisdn/sms/mt/verify"
-        }
-      }
-    },
     registerResult: {
       msisdnSessionToken: _sessionToken
     }
   });
   MobileIdentityManager.client = client;
 
   let promiseId = Date.now();
   let mm = {
@@ -1156,25 +1143,16 @@ add_test(function() {
   });
   MobileIdentityManager.ui = ui;
   let credStore = new MockCredStore({
     getByOriginResult: existingCredentials
   });
   MobileIdentityManager.credStore = credStore;
   let client = new MockClient({
     verifyCodeResult: ANOTHER_PHONE_NUMBER,
-    discoverResult: {
-      verificationMethods: ["sms/mt"],
-      verificationDetails: {
-        "sms/mt": {
-          mtSender: "123",
-          url: "https://msisdn.accounts.firefox.com/v1/msisdn/sms/mt/verify"
-        }
-      }
-    },
     registerResult: {
       msisdnSessionToken: _sessionToken
     }
   });
   MobileIdentityManager.client = client;
 
   let promiseId = Date.now();
   let mm = {
@@ -1228,8 +1206,101 @@ add_test(function() {
     json: {
       promiseId: promiseId,
       options: {
         forceSelection: true
       }
     }
   });
 });
+
+add_test(function() {
+  do_print("= Existing credentials - No Icc - INVALID_AUTH_TOKEN - OK =");
+
+  do_register_cleanup(cleanup);
+
+  do_test_pending();
+
+  let _sessionToken = Date.now();
+
+  let existingCredentials = {
+    sessionToken: _sessionToken,
+    msisdn: PHONE_NUMBER,
+    origin: ORIGIN,
+    deviceIccIds: null
+  };
+
+  let ui = new MockUi({
+    startFlowResult: {
+      phoneNumber: PHONE_NUMBER
+    }
+  });
+  MobileIdentityManager.ui = ui;
+  let credStore = new MockCredStore({
+    getByOriginResult: [existingCredentials, null]
+  });
+  MobileIdentityManager.credStore = credStore;
+  let client = new MockClient({
+    signError: [ERROR_INVALID_AUTH_TOKEN],
+    verifyCodeResult: PHONE_NUMBER,
+    registerResult: {
+      msisdnSessionToken: SESSION_TOKEN
+    }
+  });
+  MobileIdentityManager.client = client;
+
+  let promiseId = Date.now();
+  let mm = {
+    sendAsyncMessage: function(aMsg, aData) {
+      do_print("sendAsyncMessage " + aMsg + " - " + JSON.stringify(aData));
+
+      // Check result.
+      do_check_eq(aMsg, GET_ASSERTION_RETURN_OK);
+      do_check_eq(typeof aData, "object");
+      do_check_eq(aData.promiseId, promiseId);
+
+      // Check spied calls.
+
+      // MockCredStore.
+      credStore._("getByOrigin").callsLength(2);
+      credStore._("getByOrigin").call(1).arg(1, ORIGIN);
+      credStore._("getByOrigin").call(2).arg(1, ORIGIN);
+      credStore._("getByMsisdn").callsLength(1);
+      credStore._("getByMsisdn").call(1).arg(1, PHONE_NUMBER);
+      credStore._("add").callsLength(1);
+      credStore._("add").call(1).arg(1, undefined);
+      credStore._("add").call(1).arg(2, PHONE_NUMBER);
+      credStore._("add").call(1).arg(3, ORIGIN);
+      credStore._("add").call(1).arg(4, SESSION_TOKEN);
+      credStore._("add").call(1).arg(5, null);
+      credStore._("setDeviceIccIds").callsLength(0);
+      credStore._("delete").callsLength(1);
+      credStore._("delete").call(1).arg(1, PHONE_NUMBER);
+
+      // MockUI.
+      ui._("startFlow").callsLength(1);
+      ui._("verifyCodePrompt").callsLength(1);
+      ui._("verify").callsLength(1);
+
+      // MockClient.
+      client._("discover").callsLength(1);
+      client._("register").callsLength(1);
+      client._("smsMtVerify").callsLength(1);
+      client._("verifyCode").callsLength(1);
+      client._("sign").callsLength(1);
+
+      do_test_finished();
+      run_next_test();
+    }
+  };
+
+  addPermission(ORIGIN, Ci.nsIPermissionManager.ALLOW_ACTION);
+
+  MobileIdentityManager.receiveMessage({
+    name: GET_ASSERTION_IPC_MSG,
+    principal: PRINCIPAL,
+    target: mm,
+    json: {
+      promiseId: promiseId,
+      options: {}
+    }
+  });
+});