services/fxaccounts/FxAccountsManager.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 /**
     6  * Temporary abstraction layer for common Fx Accounts operations.
     7  * For now, we will be using this module only from B2G but in the end we might
     8  * want this to be merged with FxAccounts.jsm and let other products also use
     9  * it.
    10  */
    12 "use strict";
    14 this.EXPORTED_SYMBOLS = ["FxAccountsManager"];
    16 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
    18 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    19 Cu.import("resource://gre/modules/Services.jsm");
    20 Cu.import("resource://gre/modules/FxAccounts.jsm");
    21 Cu.import("resource://gre/modules/Promise.jsm");
    22 Cu.import("resource://gre/modules/FxAccountsCommon.js");
    24 this.FxAccountsManager = {
    26   init: function() {
    27     Services.obs.addObserver(this, ONLOGOUT_NOTIFICATION, false);
    28   },
    30   observe: function(aSubject, aTopic, aData) {
    31     if (aTopic !== ONLOGOUT_NOTIFICATION) {
    32       return;
    33     }
    35     // Remove the cached session if we get a logout notification.
    36     this._activeSession = null;
    37   },
    39   // We don't really need to save fxAccounts instance but this way we allow
    40   // to mock FxAccounts from tests.
    41   _fxAccounts: fxAccounts,
    43   // We keep the session details here so consumers don't need to deal with
    44   // session tokens and are only required to handle the email.
    45   _activeSession: null,
    47   // We only expose the email and the verified status so far.
    48   get _user() {
    49     if (!this._activeSession || !this._activeSession.email) {
    50       return null;
    51     }
    53     return {
    54       accountId: this._activeSession.email,
    55       verified: this._activeSession.verified
    56     }
    57   },
    59   _error: function(aError, aDetails) {
    60     log.error(aError);
    61     let reason = {
    62       error: aError
    63     };
    64     if (aDetails) {
    65       reason.details = aDetails;
    66     }
    67     return Promise.reject(reason);
    68   },
    70   _getError: function(aServerResponse) {
    71     if (!aServerResponse || !aServerResponse.error || !aServerResponse.error.errno) {
    72       return;
    73     }
    74     let error = SERVER_ERRNO_TO_ERROR[aServerResponse.error.errno];
    75     return error;
    76   },
    78   _serverError: function(aServerResponse) {
    79     let error = this._getError({ error: aServerResponse });
    80     return this._error(error ? error : ERROR_SERVER_ERROR, aServerResponse);
    81   },
    83   // As with _fxAccounts, we don't really need this method, but this way we
    84   // allow tests to mock FxAccountsClient.  By default, we want to return the
    85   // client used by the fxAccounts object because deep down they should have
    86   // access to the same hawk request object which will enable them to share
    87   // local clock skeq data.
    88   _getFxAccountsClient: function() {
    89     return this._fxAccounts.getAccountsClient();
    90   },
    92   _signInSignUp: function(aMethod, aAccountId, aPassword) {
    93     if (Services.io.offline) {
    94       return this._error(ERROR_OFFLINE);
    95     }
    97     if (!aAccountId) {
    98       return this._error(ERROR_INVALID_ACCOUNTID);
    99     }
   101     if (!aPassword) {
   102       return this._error(ERROR_INVALID_PASSWORD);
   103     }
   105     // Check that there is no signed in account first.
   106     if (this._activeSession) {
   107       return this._error(ERROR_ALREADY_SIGNED_IN_USER, {
   108         user: this._user
   109       });
   110     }
   112     let client = this._getFxAccountsClient();
   113     return this._fxAccounts.getSignedInUser().then(
   114       user => {
   115         if (user) {
   116           return this._error(ERROR_ALREADY_SIGNED_IN_USER, {
   117             user: this._user
   118           });
   119         }
   120         return client[aMethod](aAccountId, aPassword);
   121       }
   122     ).then(
   123       user => {
   124         let error = this._getError(user);
   125         if (!user || !user.uid || !user.sessionToken || error) {
   126           return this._error(error ? error : ERROR_INTERNAL_INVALID_USER, {
   127             user: user
   128           });
   129         }
   131         // If the user object includes an email field, it may differ in
   132         // capitalization from what we sent down.  This is the server's
   133         // canonical capitalization and should be used instead.
   134         user.email = user.email || aAccountId;
   135         return this._fxAccounts.setSignedInUser(user).then(
   136           () => {
   137             this._activeSession = user;
   138             log.debug("User signed in: " + JSON.stringify(this._user) +
   139                       " - Account created " + (aMethod == "signUp"));
   140             return Promise.resolve({
   141               accountCreated: aMethod === "signUp",
   142               user: this._user
   143             });
   144           }
   145         );
   146       },
   147       reason => { return this._serverError(reason); }
   148     );
   149   },
   151   _getAssertion: function(aAudience) {
   152     return this._fxAccounts.getAssertion(aAudience);
   153   },
   155   _signOut: function() {
   156     if (!this._activeSession) {
   157       return Promise.resolve();
   158     }
   160     // We clear the local session cache as soon as we get the onlogout
   161     // notification triggered within FxAccounts.signOut, so we save the
   162     // session token value to be able to remove the remote server session
   163     // in case that we have network connection.
   164     let sessionToken = this._activeSession.sessionToken;
   166     return this._fxAccounts.signOut(true).then(
   167       () => {
   168         // At this point the local session should already be removed.
   170         // The client can create new sessions up to the limit (100?).
   171         // Orphaned tokens on the server will eventually be garbage collected.
   172         if (Services.io.offline) {
   173           return Promise.resolve();
   174         }
   175         // Otherwise, we try to remove the remote session.
   176         let client = this._getFxAccountsClient();
   177         return client.signOut(sessionToken).then(
   178           result => {
   179             let error = this._getError(result);
   180             if (error) {
   181               return this._error(error, result);
   182             }
   183             log.debug("Signed out");
   184             return Promise.resolve();
   185           },
   186           reason => {
   187             return this._serverError(reason);
   188           }
   189         );
   190       }
   191     );
   192   },
   194   _uiRequest: function(aRequest, aAudience, aParams) {
   195     let ui = Cc["@mozilla.org/fxaccounts/fxaccounts-ui-glue;1"]
   196                .createInstance(Ci.nsIFxAccountsUIGlue);
   197     if (!ui[aRequest]) {
   198       return this._error(ERROR_UI_REQUEST);
   199     }
   201     if (!aParams || !Array.isArray(aParams)) {
   202       aParams = [aParams];
   203     }
   205     return ui[aRequest].apply(this, aParams).then(
   206       result => {
   207         // Even if we get a successful result from the UI, the account will
   208         // most likely be unverified, so we cannot get an assertion.
   209         if (result && result.verified) {
   210           return this._getAssertion(aAudience);
   211         }
   213         return this._error(ERROR_UNVERIFIED_ACCOUNT, {
   214           user: result
   215         });
   216       },
   217       error => {
   218         return this._error(ERROR_UI_ERROR, error);
   219       }
   220     );
   221   },
   223   // -- API --
   225   signIn: function(aAccountId, aPassword) {
   226     return this._signInSignUp("signIn", aAccountId, aPassword);
   227   },
   229   signUp: function(aAccountId, aPassword) {
   230     return this._signInSignUp("signUp", aAccountId, aPassword);
   231   },
   233   signOut: function() {
   234     if (!this._activeSession) {
   235       // If there is no cached active session, we try to get it from the
   236       // account storage.
   237       return this.getAccount().then(
   238         result => {
   239           if (!result) {
   240             return Promise.resolve();
   241           }
   242           return this._signOut();
   243         }
   244       );
   245     }
   246     return this._signOut();
   247   },
   249   getAccount: function() {
   250     // We check first if we have session details cached.
   251     if (this._activeSession) {
   252       // If our cache says that the account is not yet verified,
   253       // we kick off verification before returning what we have.
   254       if (this._activeSession && !this._activeSession.verified &&
   255           !Services.io.offline) {
   256         this.verificationStatus(this._activeSession);
   257       }
   259       log.debug("Account " + JSON.stringify(this._user));
   260       return Promise.resolve(this._user);
   261     }
   263     // If no cached information, we try to get it from the persistent storage.
   264     return this._fxAccounts.getSignedInUser().then(
   265       user => {
   266         if (!user || !user.email) {
   267           log.debug("No signed in account");
   268           return Promise.resolve(null);
   269         }
   271         this._activeSession = user;
   272         // If we get a stored information of a not yet verified account,
   273         // we kick off verification before returning what we have.
   274         if (!user.verified && !Services.io.offline) {
   275           log.debug("Unverified account");
   276           this.verificationStatus(user);
   277         }
   279         log.debug("Account " + JSON.stringify(this._user));
   280         return Promise.resolve(this._user);
   281       }
   282     );
   283   },
   285   queryAccount: function(aAccountId) {
   286     log.debug("queryAccount " + aAccountId);
   287     if (Services.io.offline) {
   288       return this._error(ERROR_OFFLINE);
   289     }
   291     let deferred = Promise.defer();
   293     if (!aAccountId) {
   294       return this._error(ERROR_INVALID_ACCOUNTID);
   295     }
   297     let client = this._getFxAccountsClient();
   298     return client.accountExists(aAccountId).then(
   299       result => {
   300         log.debug("Account " + result ? "" : "does not" + " exists");
   301         let error = this._getError(result);
   302         if (error) {
   303           return this._error(error, result);
   304         }
   306         return Promise.resolve({
   307           registered: result
   308         });
   309       },
   310       reason => { this._serverError(reason); }
   311     );
   312   },
   314   verificationStatus: function() {
   315     log.debug("verificationStatus");
   316     if (!this._activeSession || !this._activeSession.sessionToken) {
   317       this._error(ERROR_NO_TOKEN_SESSION);
   318     }
   320     // There is no way to unverify an already verified account, so we just
   321     // return the account details of a verified account
   322     if (this._activeSession.verified) {
   323       log.debug("Account already verified");
   324       return;
   325     }
   327     if (Services.io.offline) {
   328       this._error(ERROR_OFFLINE);
   329     }
   331     let client = this._getFxAccountsClient();
   332     client.recoveryEmailStatus(this._activeSession.sessionToken).then(
   333       data => {
   334         let error = this._getError(data);
   335         if (error) {
   336           this._error(error, data);
   337         }
   338         // If the verification status has changed, update state.
   339         if (this._activeSession.verified != data.verified) {
   340           this._activeSession.verified = data.verified;
   341           this._fxAccounts.setSignedInUser(this._activeSession);
   342         }
   343         log.debug(JSON.stringify(this._user));
   344       },
   345       reason => { this._serverError(reason); }
   346     );
   347   },
   349   /*
   350    * Try to get an assertion for the given audience.
   351    *
   352    * aOptions can include:
   353    *
   354    *   refreshAuthentication  - (bool) Force re-auth.
   355    *
   356    *   silent                 - (bool) Prevent any UI interaction.
   357    *                            I.e., try to get an automatic assertion.
   358    *
   359    */
   360   getAssertion: function(aAudience, aOptions) {
   361     if (!aAudience) {
   362       return this._error(ERROR_INVALID_AUDIENCE);
   363     }
   365     if (Services.io.offline) {
   366       return this._error(ERROR_OFFLINE);
   367     }
   369     return this.getAccount().then(
   370       user => {
   371         if (user) {
   372           // We cannot get assertions for unverified accounts.
   373           if (!user.verified) {
   374             return this._error(ERROR_UNVERIFIED_ACCOUNT, {
   375               user: user
   376             });
   377           }
   379           // RPs might require an authentication refresh.
   380           if (aOptions &&
   381               aOptions.refreshAuthentication) {
   382             let gracePeriod = aOptions.refreshAuthentication;
   383             if (typeof gracePeriod != 'number' || isNaN(gracePeriod)) {
   384               return this._error(ERROR_INVALID_REFRESH_AUTH_VALUE);
   385             }
   387             if ((Date.now() / 1000) - this._activeSession.authAt > gracePeriod) {
   388               // Grace period expired, so we sign out and request the user to
   389               // authenticate herself again. If the authentication succeeds, we
   390               // will return the assertion. Otherwise, we will return an error.
   391               return this._signOut().then(
   392                 () => {
   393                   if (aOptions.silent) {
   394                     return Promise.resolve(null);
   395                   }
   396                   return this._uiRequest(UI_REQUEST_REFRESH_AUTH,
   397                                          aAudience, user.accountId);
   398                 }
   399               );
   400             }
   401           }
   403           return this._getAssertion(aAudience);
   404         }
   406         log.debug("No signed in user");
   408         if (aOptions && aOptions.silent) {
   409           return Promise.resolve(null);
   410         }
   412         // If there is no currently signed in user, we trigger the signIn UI
   413         // flow.
   414         return this._uiRequest(UI_REQUEST_SIGN_IN_FLOW, aAudience);
   415       }
   416     );
   417   }
   419 };
   421 FxAccountsManager.init();

mercurial