michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /** michael@0: * Temporary abstraction layer for common Fx Accounts operations. michael@0: * For now, we will be using this module only from B2G but in the end we might michael@0: * want this to be merged with FxAccounts.jsm and let other products also use michael@0: * it. michael@0: */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["FxAccountsManager"]; michael@0: michael@0: const { classes: Cc, interfaces: Ci, utils: Cu } = Components; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/FxAccounts.jsm"); michael@0: Cu.import("resource://gre/modules/Promise.jsm"); michael@0: Cu.import("resource://gre/modules/FxAccountsCommon.js"); michael@0: michael@0: this.FxAccountsManager = { michael@0: michael@0: init: function() { michael@0: Services.obs.addObserver(this, ONLOGOUT_NOTIFICATION, false); michael@0: }, michael@0: michael@0: observe: function(aSubject, aTopic, aData) { michael@0: if (aTopic !== ONLOGOUT_NOTIFICATION) { michael@0: return; michael@0: } michael@0: michael@0: // Remove the cached session if we get a logout notification. michael@0: this._activeSession = null; michael@0: }, michael@0: michael@0: // We don't really need to save fxAccounts instance but this way we allow michael@0: // to mock FxAccounts from tests. michael@0: _fxAccounts: fxAccounts, michael@0: michael@0: // We keep the session details here so consumers don't need to deal with michael@0: // session tokens and are only required to handle the email. michael@0: _activeSession: null, michael@0: michael@0: // We only expose the email and the verified status so far. michael@0: get _user() { michael@0: if (!this._activeSession || !this._activeSession.email) { michael@0: return null; michael@0: } michael@0: michael@0: return { michael@0: accountId: this._activeSession.email, michael@0: verified: this._activeSession.verified michael@0: } michael@0: }, michael@0: michael@0: _error: function(aError, aDetails) { michael@0: log.error(aError); michael@0: let reason = { michael@0: error: aError michael@0: }; michael@0: if (aDetails) { michael@0: reason.details = aDetails; michael@0: } michael@0: return Promise.reject(reason); michael@0: }, michael@0: michael@0: _getError: function(aServerResponse) { michael@0: if (!aServerResponse || !aServerResponse.error || !aServerResponse.error.errno) { michael@0: return; michael@0: } michael@0: let error = SERVER_ERRNO_TO_ERROR[aServerResponse.error.errno]; michael@0: return error; michael@0: }, michael@0: michael@0: _serverError: function(aServerResponse) { michael@0: let error = this._getError({ error: aServerResponse }); michael@0: return this._error(error ? error : ERROR_SERVER_ERROR, aServerResponse); michael@0: }, michael@0: michael@0: // As with _fxAccounts, we don't really need this method, but this way we michael@0: // allow tests to mock FxAccountsClient. By default, we want to return the michael@0: // client used by the fxAccounts object because deep down they should have michael@0: // access to the same hawk request object which will enable them to share michael@0: // local clock skeq data. michael@0: _getFxAccountsClient: function() { michael@0: return this._fxAccounts.getAccountsClient(); michael@0: }, michael@0: michael@0: _signInSignUp: function(aMethod, aAccountId, aPassword) { michael@0: if (Services.io.offline) { michael@0: return this._error(ERROR_OFFLINE); michael@0: } michael@0: michael@0: if (!aAccountId) { michael@0: return this._error(ERROR_INVALID_ACCOUNTID); michael@0: } michael@0: michael@0: if (!aPassword) { michael@0: return this._error(ERROR_INVALID_PASSWORD); michael@0: } michael@0: michael@0: // Check that there is no signed in account first. michael@0: if (this._activeSession) { michael@0: return this._error(ERROR_ALREADY_SIGNED_IN_USER, { michael@0: user: this._user michael@0: }); michael@0: } michael@0: michael@0: let client = this._getFxAccountsClient(); michael@0: return this._fxAccounts.getSignedInUser().then( michael@0: user => { michael@0: if (user) { michael@0: return this._error(ERROR_ALREADY_SIGNED_IN_USER, { michael@0: user: this._user michael@0: }); michael@0: } michael@0: return client[aMethod](aAccountId, aPassword); michael@0: } michael@0: ).then( michael@0: user => { michael@0: let error = this._getError(user); michael@0: if (!user || !user.uid || !user.sessionToken || error) { michael@0: return this._error(error ? error : ERROR_INTERNAL_INVALID_USER, { michael@0: user: user michael@0: }); michael@0: } michael@0: michael@0: // If the user object includes an email field, it may differ in michael@0: // capitalization from what we sent down. This is the server's michael@0: // canonical capitalization and should be used instead. michael@0: user.email = user.email || aAccountId; michael@0: return this._fxAccounts.setSignedInUser(user).then( michael@0: () => { michael@0: this._activeSession = user; michael@0: log.debug("User signed in: " + JSON.stringify(this._user) + michael@0: " - Account created " + (aMethod == "signUp")); michael@0: return Promise.resolve({ michael@0: accountCreated: aMethod === "signUp", michael@0: user: this._user michael@0: }); michael@0: } michael@0: ); michael@0: }, michael@0: reason => { return this._serverError(reason); } michael@0: ); michael@0: }, michael@0: michael@0: _getAssertion: function(aAudience) { michael@0: return this._fxAccounts.getAssertion(aAudience); michael@0: }, michael@0: michael@0: _signOut: function() { michael@0: if (!this._activeSession) { michael@0: return Promise.resolve(); michael@0: } michael@0: michael@0: // We clear the local session cache as soon as we get the onlogout michael@0: // notification triggered within FxAccounts.signOut, so we save the michael@0: // session token value to be able to remove the remote server session michael@0: // in case that we have network connection. michael@0: let sessionToken = this._activeSession.sessionToken; michael@0: michael@0: return this._fxAccounts.signOut(true).then( michael@0: () => { michael@0: // At this point the local session should already be removed. michael@0: michael@0: // The client can create new sessions up to the limit (100?). michael@0: // Orphaned tokens on the server will eventually be garbage collected. michael@0: if (Services.io.offline) { michael@0: return Promise.resolve(); michael@0: } michael@0: // Otherwise, we try to remove the remote session. michael@0: let client = this._getFxAccountsClient(); michael@0: return client.signOut(sessionToken).then( michael@0: result => { michael@0: let error = this._getError(result); michael@0: if (error) { michael@0: return this._error(error, result); michael@0: } michael@0: log.debug("Signed out"); michael@0: return Promise.resolve(); michael@0: }, michael@0: reason => { michael@0: return this._serverError(reason); michael@0: } michael@0: ); michael@0: } michael@0: ); michael@0: }, michael@0: michael@0: _uiRequest: function(aRequest, aAudience, aParams) { michael@0: let ui = Cc["@mozilla.org/fxaccounts/fxaccounts-ui-glue;1"] michael@0: .createInstance(Ci.nsIFxAccountsUIGlue); michael@0: if (!ui[aRequest]) { michael@0: return this._error(ERROR_UI_REQUEST); michael@0: } michael@0: michael@0: if (!aParams || !Array.isArray(aParams)) { michael@0: aParams = [aParams]; michael@0: } michael@0: michael@0: return ui[aRequest].apply(this, aParams).then( michael@0: result => { michael@0: // Even if we get a successful result from the UI, the account will michael@0: // most likely be unverified, so we cannot get an assertion. michael@0: if (result && result.verified) { michael@0: return this._getAssertion(aAudience); michael@0: } michael@0: michael@0: return this._error(ERROR_UNVERIFIED_ACCOUNT, { michael@0: user: result michael@0: }); michael@0: }, michael@0: error => { michael@0: return this._error(ERROR_UI_ERROR, error); michael@0: } michael@0: ); michael@0: }, michael@0: michael@0: // -- API -- michael@0: michael@0: signIn: function(aAccountId, aPassword) { michael@0: return this._signInSignUp("signIn", aAccountId, aPassword); michael@0: }, michael@0: michael@0: signUp: function(aAccountId, aPassword) { michael@0: return this._signInSignUp("signUp", aAccountId, aPassword); michael@0: }, michael@0: michael@0: signOut: function() { michael@0: if (!this._activeSession) { michael@0: // If there is no cached active session, we try to get it from the michael@0: // account storage. michael@0: return this.getAccount().then( michael@0: result => { michael@0: if (!result) { michael@0: return Promise.resolve(); michael@0: } michael@0: return this._signOut(); michael@0: } michael@0: ); michael@0: } michael@0: return this._signOut(); michael@0: }, michael@0: michael@0: getAccount: function() { michael@0: // We check first if we have session details cached. michael@0: if (this._activeSession) { michael@0: // If our cache says that the account is not yet verified, michael@0: // we kick off verification before returning what we have. michael@0: if (this._activeSession && !this._activeSession.verified && michael@0: !Services.io.offline) { michael@0: this.verificationStatus(this._activeSession); michael@0: } michael@0: michael@0: log.debug("Account " + JSON.stringify(this._user)); michael@0: return Promise.resolve(this._user); michael@0: } michael@0: michael@0: // If no cached information, we try to get it from the persistent storage. michael@0: return this._fxAccounts.getSignedInUser().then( michael@0: user => { michael@0: if (!user || !user.email) { michael@0: log.debug("No signed in account"); michael@0: return Promise.resolve(null); michael@0: } michael@0: michael@0: this._activeSession = user; michael@0: // If we get a stored information of a not yet verified account, michael@0: // we kick off verification before returning what we have. michael@0: if (!user.verified && !Services.io.offline) { michael@0: log.debug("Unverified account"); michael@0: this.verificationStatus(user); michael@0: } michael@0: michael@0: log.debug("Account " + JSON.stringify(this._user)); michael@0: return Promise.resolve(this._user); michael@0: } michael@0: ); michael@0: }, michael@0: michael@0: queryAccount: function(aAccountId) { michael@0: log.debug("queryAccount " + aAccountId); michael@0: if (Services.io.offline) { michael@0: return this._error(ERROR_OFFLINE); michael@0: } michael@0: michael@0: let deferred = Promise.defer(); michael@0: michael@0: if (!aAccountId) { michael@0: return this._error(ERROR_INVALID_ACCOUNTID); michael@0: } michael@0: michael@0: let client = this._getFxAccountsClient(); michael@0: return client.accountExists(aAccountId).then( michael@0: result => { michael@0: log.debug("Account " + result ? "" : "does not" + " exists"); michael@0: let error = this._getError(result); michael@0: if (error) { michael@0: return this._error(error, result); michael@0: } michael@0: michael@0: return Promise.resolve({ michael@0: registered: result michael@0: }); michael@0: }, michael@0: reason => { this._serverError(reason); } michael@0: ); michael@0: }, michael@0: michael@0: verificationStatus: function() { michael@0: log.debug("verificationStatus"); michael@0: if (!this._activeSession || !this._activeSession.sessionToken) { michael@0: this._error(ERROR_NO_TOKEN_SESSION); michael@0: } michael@0: michael@0: // There is no way to unverify an already verified account, so we just michael@0: // return the account details of a verified account michael@0: if (this._activeSession.verified) { michael@0: log.debug("Account already verified"); michael@0: return; michael@0: } michael@0: michael@0: if (Services.io.offline) { michael@0: this._error(ERROR_OFFLINE); michael@0: } michael@0: michael@0: let client = this._getFxAccountsClient(); michael@0: client.recoveryEmailStatus(this._activeSession.sessionToken).then( michael@0: data => { michael@0: let error = this._getError(data); michael@0: if (error) { michael@0: this._error(error, data); michael@0: } michael@0: // If the verification status has changed, update state. michael@0: if (this._activeSession.verified != data.verified) { michael@0: this._activeSession.verified = data.verified; michael@0: this._fxAccounts.setSignedInUser(this._activeSession); michael@0: } michael@0: log.debug(JSON.stringify(this._user)); michael@0: }, michael@0: reason => { this._serverError(reason); } michael@0: ); michael@0: }, michael@0: michael@0: /* michael@0: * Try to get an assertion for the given audience. michael@0: * michael@0: * aOptions can include: michael@0: * michael@0: * refreshAuthentication - (bool) Force re-auth. michael@0: * michael@0: * silent - (bool) Prevent any UI interaction. michael@0: * I.e., try to get an automatic assertion. michael@0: * michael@0: */ michael@0: getAssertion: function(aAudience, aOptions) { michael@0: if (!aAudience) { michael@0: return this._error(ERROR_INVALID_AUDIENCE); michael@0: } michael@0: michael@0: if (Services.io.offline) { michael@0: return this._error(ERROR_OFFLINE); michael@0: } michael@0: michael@0: return this.getAccount().then( michael@0: user => { michael@0: if (user) { michael@0: // We cannot get assertions for unverified accounts. michael@0: if (!user.verified) { michael@0: return this._error(ERROR_UNVERIFIED_ACCOUNT, { michael@0: user: user michael@0: }); michael@0: } michael@0: michael@0: // RPs might require an authentication refresh. michael@0: if (aOptions && michael@0: aOptions.refreshAuthentication) { michael@0: let gracePeriod = aOptions.refreshAuthentication; michael@0: if (typeof gracePeriod != 'number' || isNaN(gracePeriod)) { michael@0: return this._error(ERROR_INVALID_REFRESH_AUTH_VALUE); michael@0: } michael@0: michael@0: if ((Date.now() / 1000) - this._activeSession.authAt > gracePeriod) { michael@0: // Grace period expired, so we sign out and request the user to michael@0: // authenticate herself again. If the authentication succeeds, we michael@0: // will return the assertion. Otherwise, we will return an error. michael@0: return this._signOut().then( michael@0: () => { michael@0: if (aOptions.silent) { michael@0: return Promise.resolve(null); michael@0: } michael@0: return this._uiRequest(UI_REQUEST_REFRESH_AUTH, michael@0: aAudience, user.accountId); michael@0: } michael@0: ); michael@0: } michael@0: } michael@0: michael@0: return this._getAssertion(aAudience); michael@0: } michael@0: michael@0: log.debug("No signed in user"); michael@0: michael@0: if (aOptions && aOptions.silent) { michael@0: return Promise.resolve(null); michael@0: } michael@0: michael@0: // If there is no currently signed in user, we trigger the signIn UI michael@0: // flow. michael@0: return this._uiRequest(UI_REQUEST_SIGN_IN_FLOW, aAudience); michael@0: } michael@0: ); michael@0: } michael@0: michael@0: }; michael@0: michael@0: FxAccountsManager.init();