services/mobileid/MobileIdentityVerificationFlow.jsm
author Andrea Marchesini <amarchesini@mozilla.com>
Mon, 30 May 2016 13:30:46 +0200
changeset 338568 4af4438a2aecac6fd61c94c62f2c78957b451fc0
parent 242201 dce264e3c9f10e77e24c6450682ff417dd85a58a
permissions -rw-r--r--
Bug 1276116 - userContext-icons should be hidden by default, r=gijs

/* 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";

this.EXPORTED_SYMBOLS = ["MobileIdentityVerificationFlow"];

const { utils: Cu, classes: Cc, interfaces: Ci } = Components;

Cu.import("resource://gre/modules/MobileIdentityCommon.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");

this.MobileIdentityVerificationFlow = function(aVerificationOptions,
                                               aUI,
                                               aClient,
                                               aVerifyStrategy,
                                               aCleanupStrategy) {
  this.verificationOptions = aVerificationOptions;
  this.ui = aUI;
  this.client = aClient;
  this.retries = VERIFICATIONCODE_RETRIES;
  this.verifyStrategy = aVerifyStrategy;
  this.cleanupStrategy = aCleanupStrategy;
};

MobileIdentityVerificationFlow.prototype = {

  doVerification: function() {
    log.debug("Start verification flow");
    return this.register()
    .then(
      (registerResult) => {
        log.debug("Register result ${}", registerResult);
        if (!registerResult || !registerResult.msisdnSessionToken) {
          return Promise.reject(ERROR_INTERNAL_UNEXPECTED);
        }
        this.sessionToken = registerResult.msisdnSessionToken;
        // We save the timestamp of the start of the verification timeout to be
        // able to provide to the UI the remaining time on each retry.
        if (!this.timer) {
          log.debug("Creating verification code timer");
          this.timerCreation = Date.now();
          this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
          this.timer.initWithCallback(this.onVerificationCodeTimeout.bind(this),
                                      VERIFICATIONCODE_TIMEOUT,
                                      this.timer.TYPE_ONE_SHOT);
        }

        if (!this.verifyStrategy) {
          return Promise.reject(ERROR_INTERNAL_INVALID_VERIFICATION_FLOW);
        }

        return this.verifyStrategy()
        .then(() => {
          return this._doVerification();
        }, (reason) => {
          this.verificationCodeDeferred.reject(reason);
        });
      }
    )
  },

  _doVerification: function() {
    log.debug("_doVerification");

    this.verificationCodeDeferred = Promise.defer();

    // If the verification flow can be for an external phone number,
    // we need to ask the user for the verification code.
    // In that case we don't do a notification about the verification
    // process being done until the user enters the verification code
    // in the UI.
    if (this.verificationOptions.external) {
      let timeLeft = 0;
      if (this.timer) {
        timeLeft = this.timerCreation + VERIFICATIONCODE_TIMEOUT -
                   Date.now();
      }
      this.ui.verificationCodePrompt(this.retries,
                                     VERIFICATIONCODE_TIMEOUT / 1000,
                                     timeLeft / 1000)
      .then(
        (verificationCode) => {
          if (!verificationCode) {
            return this.verificationCodeDeferred.reject(
              ERROR_INTERNAL_INVALID_PROMPT_RESULT);
          }
          // If the user got the verification code that means that the
          // introduced phone number didn't belong to any of the inserted
          // SIMs.
          this.ui.verify();
          this.verificationCodeDeferred.resolve(verificationCode);
        }
      );
    } else {
      this.ui.verify();
    }

    return this.verificationCodeDeferred.promise.then(
      this.onVerificationCode.bind(this)
    );
  },

  // When we receive a verification code from the UI, we check it against
  // the server. If the verification code is incorrect, we decrease the
  // number of retries left and allow the user to try again. If there is no
  // possible retry left, we notify about this error so the UI can allow the
  // user to request the resend of a new verification code.
  onVerificationCode: function(aVerificationCode) {
    log.debug("onVerificationCode " + aVerificationCode);
    if (!aVerificationCode) {
      this.ui.error(ERROR_INVALID_VERIFICATION_CODE);
      return this._doVerification();
    }

    // Before checking the verification code against the server we set the
    // "verifying" flag to queue timeout expiration events received before
    // the server request is completed. If the server request is positive
    // we will discard the timeout event, otherwise we will progress the
    // event to the UI to allow the user to retry.
    this.verifying = true;

    return this.verifyCode(aVerificationCode)
    .then(
      (result) => {
        if (!result) {
          return Promise.reject(INTERNAL_UNEXPECTED);
        }
        // The code was correct!
        // At this point the phone number is verified.
        // We return the given verification options with the session token
        // to be stored in the credentials store. With this data we will be
        // asking the server to give us a certificate to generate assertions.
        this.verificationOptions.sessionToken = this.sessionToken;
        this.verificationOptions.msisdn = result.msisdn ||
                                          this.verificationOptions.msisdn;
        return this.verificationOptions;
      },
      (error) => {
        log.error("Verification code error " + error);
        this.retries--;
        log.error("Retries left " + this.retries);
        if (!this.retries) {
          this.ui.error(ERROR_NO_RETRIES_LEFT);
          this.timer.cancel();
          this.timer = null;
          return Promise.reject(ERROR_NO_RETRIES_LEFT);
        }
        this.ui.error(ERROR_INVALID_VERIFICATION_CODE);
        this.verifying = false;
        if (this.queuedTimeout) {
          this.onVerificationCodeTimeout();
        }
        return this._doVerification();
      }
    );
  },

  onVerificationCodeTimeout: function() {
    // It is possible that we get the timeout when we are checking a
    // verification code with the server. In that case, we queue the
    // timeout to be triggered after we receive the reply from the server
    // if needed.
    if (this.verifying) {
      this.queuedTimeout = true;
      return;
    }

    // When the verification process times out we do a clean up, reject
    // the corresponding promise and notify the UI about the timeout.
    if (this.verificationCodeDeferred) {
      this.verificationCodeDeferred.reject(ERROR_VERIFICATION_CODE_TIMEOUT);
    }
    this.ui.error(ERROR_VERIFICATION_CODE_TIMEOUT);
  },

  register: function() {
    return this.client.register();
  },

  verifyCode: function(aVerificationCode) {
    return this.client.verifyCode(this.sessionToken, aVerificationCode);
  },

  unregister: function() {
    return this.client.unregister(this.sessionToken);
  },

  cleanup: function(aUnregister = false) {
    log.debug("Verification flow cleanup");

    this.queuedTimeout = false;
    this.retries = VERIFICATIONCODE_RETRIES;

    if (this.timer) {
      this.timer.cancel();
      this.timer = null;
    }

    if (aUnregister) {
      this.unregister().
      then(
        () => {
          this.sessionToken = null;
        }
      );
    }

    if (this.cleanupStrategy) {
      this.cleanupStrategy();
    }
  }
};