1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/services/fxaccounts/FxAccountsManager.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,421 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +/** 1.9 + * Temporary abstraction layer for common Fx Accounts operations. 1.10 + * For now, we will be using this module only from B2G but in the end we might 1.11 + * want this to be merged with FxAccounts.jsm and let other products also use 1.12 + * it. 1.13 + */ 1.14 + 1.15 +"use strict"; 1.16 + 1.17 +this.EXPORTED_SYMBOLS = ["FxAccountsManager"]; 1.18 + 1.19 +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; 1.20 + 1.21 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.22 +Cu.import("resource://gre/modules/Services.jsm"); 1.23 +Cu.import("resource://gre/modules/FxAccounts.jsm"); 1.24 +Cu.import("resource://gre/modules/Promise.jsm"); 1.25 +Cu.import("resource://gre/modules/FxAccountsCommon.js"); 1.26 + 1.27 +this.FxAccountsManager = { 1.28 + 1.29 + init: function() { 1.30 + Services.obs.addObserver(this, ONLOGOUT_NOTIFICATION, false); 1.31 + }, 1.32 + 1.33 + observe: function(aSubject, aTopic, aData) { 1.34 + if (aTopic !== ONLOGOUT_NOTIFICATION) { 1.35 + return; 1.36 + } 1.37 + 1.38 + // Remove the cached session if we get a logout notification. 1.39 + this._activeSession = null; 1.40 + }, 1.41 + 1.42 + // We don't really need to save fxAccounts instance but this way we allow 1.43 + // to mock FxAccounts from tests. 1.44 + _fxAccounts: fxAccounts, 1.45 + 1.46 + // We keep the session details here so consumers don't need to deal with 1.47 + // session tokens and are only required to handle the email. 1.48 + _activeSession: null, 1.49 + 1.50 + // We only expose the email and the verified status so far. 1.51 + get _user() { 1.52 + if (!this._activeSession || !this._activeSession.email) { 1.53 + return null; 1.54 + } 1.55 + 1.56 + return { 1.57 + accountId: this._activeSession.email, 1.58 + verified: this._activeSession.verified 1.59 + } 1.60 + }, 1.61 + 1.62 + _error: function(aError, aDetails) { 1.63 + log.error(aError); 1.64 + let reason = { 1.65 + error: aError 1.66 + }; 1.67 + if (aDetails) { 1.68 + reason.details = aDetails; 1.69 + } 1.70 + return Promise.reject(reason); 1.71 + }, 1.72 + 1.73 + _getError: function(aServerResponse) { 1.74 + if (!aServerResponse || !aServerResponse.error || !aServerResponse.error.errno) { 1.75 + return; 1.76 + } 1.77 + let error = SERVER_ERRNO_TO_ERROR[aServerResponse.error.errno]; 1.78 + return error; 1.79 + }, 1.80 + 1.81 + _serverError: function(aServerResponse) { 1.82 + let error = this._getError({ error: aServerResponse }); 1.83 + return this._error(error ? error : ERROR_SERVER_ERROR, aServerResponse); 1.84 + }, 1.85 + 1.86 + // As with _fxAccounts, we don't really need this method, but this way we 1.87 + // allow tests to mock FxAccountsClient. By default, we want to return the 1.88 + // client used by the fxAccounts object because deep down they should have 1.89 + // access to the same hawk request object which will enable them to share 1.90 + // local clock skeq data. 1.91 + _getFxAccountsClient: function() { 1.92 + return this._fxAccounts.getAccountsClient(); 1.93 + }, 1.94 + 1.95 + _signInSignUp: function(aMethod, aAccountId, aPassword) { 1.96 + if (Services.io.offline) { 1.97 + return this._error(ERROR_OFFLINE); 1.98 + } 1.99 + 1.100 + if (!aAccountId) { 1.101 + return this._error(ERROR_INVALID_ACCOUNTID); 1.102 + } 1.103 + 1.104 + if (!aPassword) { 1.105 + return this._error(ERROR_INVALID_PASSWORD); 1.106 + } 1.107 + 1.108 + // Check that there is no signed in account first. 1.109 + if (this._activeSession) { 1.110 + return this._error(ERROR_ALREADY_SIGNED_IN_USER, { 1.111 + user: this._user 1.112 + }); 1.113 + } 1.114 + 1.115 + let client = this._getFxAccountsClient(); 1.116 + return this._fxAccounts.getSignedInUser().then( 1.117 + user => { 1.118 + if (user) { 1.119 + return this._error(ERROR_ALREADY_SIGNED_IN_USER, { 1.120 + user: this._user 1.121 + }); 1.122 + } 1.123 + return client[aMethod](aAccountId, aPassword); 1.124 + } 1.125 + ).then( 1.126 + user => { 1.127 + let error = this._getError(user); 1.128 + if (!user || !user.uid || !user.sessionToken || error) { 1.129 + return this._error(error ? error : ERROR_INTERNAL_INVALID_USER, { 1.130 + user: user 1.131 + }); 1.132 + } 1.133 + 1.134 + // If the user object includes an email field, it may differ in 1.135 + // capitalization from what we sent down. This is the server's 1.136 + // canonical capitalization and should be used instead. 1.137 + user.email = user.email || aAccountId; 1.138 + return this._fxAccounts.setSignedInUser(user).then( 1.139 + () => { 1.140 + this._activeSession = user; 1.141 + log.debug("User signed in: " + JSON.stringify(this._user) + 1.142 + " - Account created " + (aMethod == "signUp")); 1.143 + return Promise.resolve({ 1.144 + accountCreated: aMethod === "signUp", 1.145 + user: this._user 1.146 + }); 1.147 + } 1.148 + ); 1.149 + }, 1.150 + reason => { return this._serverError(reason); } 1.151 + ); 1.152 + }, 1.153 + 1.154 + _getAssertion: function(aAudience) { 1.155 + return this._fxAccounts.getAssertion(aAudience); 1.156 + }, 1.157 + 1.158 + _signOut: function() { 1.159 + if (!this._activeSession) { 1.160 + return Promise.resolve(); 1.161 + } 1.162 + 1.163 + // We clear the local session cache as soon as we get the onlogout 1.164 + // notification triggered within FxAccounts.signOut, so we save the 1.165 + // session token value to be able to remove the remote server session 1.166 + // in case that we have network connection. 1.167 + let sessionToken = this._activeSession.sessionToken; 1.168 + 1.169 + return this._fxAccounts.signOut(true).then( 1.170 + () => { 1.171 + // At this point the local session should already be removed. 1.172 + 1.173 + // The client can create new sessions up to the limit (100?). 1.174 + // Orphaned tokens on the server will eventually be garbage collected. 1.175 + if (Services.io.offline) { 1.176 + return Promise.resolve(); 1.177 + } 1.178 + // Otherwise, we try to remove the remote session. 1.179 + let client = this._getFxAccountsClient(); 1.180 + return client.signOut(sessionToken).then( 1.181 + result => { 1.182 + let error = this._getError(result); 1.183 + if (error) { 1.184 + return this._error(error, result); 1.185 + } 1.186 + log.debug("Signed out"); 1.187 + return Promise.resolve(); 1.188 + }, 1.189 + reason => { 1.190 + return this._serverError(reason); 1.191 + } 1.192 + ); 1.193 + } 1.194 + ); 1.195 + }, 1.196 + 1.197 + _uiRequest: function(aRequest, aAudience, aParams) { 1.198 + let ui = Cc["@mozilla.org/fxaccounts/fxaccounts-ui-glue;1"] 1.199 + .createInstance(Ci.nsIFxAccountsUIGlue); 1.200 + if (!ui[aRequest]) { 1.201 + return this._error(ERROR_UI_REQUEST); 1.202 + } 1.203 + 1.204 + if (!aParams || !Array.isArray(aParams)) { 1.205 + aParams = [aParams]; 1.206 + } 1.207 + 1.208 + return ui[aRequest].apply(this, aParams).then( 1.209 + result => { 1.210 + // Even if we get a successful result from the UI, the account will 1.211 + // most likely be unverified, so we cannot get an assertion. 1.212 + if (result && result.verified) { 1.213 + return this._getAssertion(aAudience); 1.214 + } 1.215 + 1.216 + return this._error(ERROR_UNVERIFIED_ACCOUNT, { 1.217 + user: result 1.218 + }); 1.219 + }, 1.220 + error => { 1.221 + return this._error(ERROR_UI_ERROR, error); 1.222 + } 1.223 + ); 1.224 + }, 1.225 + 1.226 + // -- API -- 1.227 + 1.228 + signIn: function(aAccountId, aPassword) { 1.229 + return this._signInSignUp("signIn", aAccountId, aPassword); 1.230 + }, 1.231 + 1.232 + signUp: function(aAccountId, aPassword) { 1.233 + return this._signInSignUp("signUp", aAccountId, aPassword); 1.234 + }, 1.235 + 1.236 + signOut: function() { 1.237 + if (!this._activeSession) { 1.238 + // If there is no cached active session, we try to get it from the 1.239 + // account storage. 1.240 + return this.getAccount().then( 1.241 + result => { 1.242 + if (!result) { 1.243 + return Promise.resolve(); 1.244 + } 1.245 + return this._signOut(); 1.246 + } 1.247 + ); 1.248 + } 1.249 + return this._signOut(); 1.250 + }, 1.251 + 1.252 + getAccount: function() { 1.253 + // We check first if we have session details cached. 1.254 + if (this._activeSession) { 1.255 + // If our cache says that the account is not yet verified, 1.256 + // we kick off verification before returning what we have. 1.257 + if (this._activeSession && !this._activeSession.verified && 1.258 + !Services.io.offline) { 1.259 + this.verificationStatus(this._activeSession); 1.260 + } 1.261 + 1.262 + log.debug("Account " + JSON.stringify(this._user)); 1.263 + return Promise.resolve(this._user); 1.264 + } 1.265 + 1.266 + // If no cached information, we try to get it from the persistent storage. 1.267 + return this._fxAccounts.getSignedInUser().then( 1.268 + user => { 1.269 + if (!user || !user.email) { 1.270 + log.debug("No signed in account"); 1.271 + return Promise.resolve(null); 1.272 + } 1.273 + 1.274 + this._activeSession = user; 1.275 + // If we get a stored information of a not yet verified account, 1.276 + // we kick off verification before returning what we have. 1.277 + if (!user.verified && !Services.io.offline) { 1.278 + log.debug("Unverified account"); 1.279 + this.verificationStatus(user); 1.280 + } 1.281 + 1.282 + log.debug("Account " + JSON.stringify(this._user)); 1.283 + return Promise.resolve(this._user); 1.284 + } 1.285 + ); 1.286 + }, 1.287 + 1.288 + queryAccount: function(aAccountId) { 1.289 + log.debug("queryAccount " + aAccountId); 1.290 + if (Services.io.offline) { 1.291 + return this._error(ERROR_OFFLINE); 1.292 + } 1.293 + 1.294 + let deferred = Promise.defer(); 1.295 + 1.296 + if (!aAccountId) { 1.297 + return this._error(ERROR_INVALID_ACCOUNTID); 1.298 + } 1.299 + 1.300 + let client = this._getFxAccountsClient(); 1.301 + return client.accountExists(aAccountId).then( 1.302 + result => { 1.303 + log.debug("Account " + result ? "" : "does not" + " exists"); 1.304 + let error = this._getError(result); 1.305 + if (error) { 1.306 + return this._error(error, result); 1.307 + } 1.308 + 1.309 + return Promise.resolve({ 1.310 + registered: result 1.311 + }); 1.312 + }, 1.313 + reason => { this._serverError(reason); } 1.314 + ); 1.315 + }, 1.316 + 1.317 + verificationStatus: function() { 1.318 + log.debug("verificationStatus"); 1.319 + if (!this._activeSession || !this._activeSession.sessionToken) { 1.320 + this._error(ERROR_NO_TOKEN_SESSION); 1.321 + } 1.322 + 1.323 + // There is no way to unverify an already verified account, so we just 1.324 + // return the account details of a verified account 1.325 + if (this._activeSession.verified) { 1.326 + log.debug("Account already verified"); 1.327 + return; 1.328 + } 1.329 + 1.330 + if (Services.io.offline) { 1.331 + this._error(ERROR_OFFLINE); 1.332 + } 1.333 + 1.334 + let client = this._getFxAccountsClient(); 1.335 + client.recoveryEmailStatus(this._activeSession.sessionToken).then( 1.336 + data => { 1.337 + let error = this._getError(data); 1.338 + if (error) { 1.339 + this._error(error, data); 1.340 + } 1.341 + // If the verification status has changed, update state. 1.342 + if (this._activeSession.verified != data.verified) { 1.343 + this._activeSession.verified = data.verified; 1.344 + this._fxAccounts.setSignedInUser(this._activeSession); 1.345 + } 1.346 + log.debug(JSON.stringify(this._user)); 1.347 + }, 1.348 + reason => { this._serverError(reason); } 1.349 + ); 1.350 + }, 1.351 + 1.352 + /* 1.353 + * Try to get an assertion for the given audience. 1.354 + * 1.355 + * aOptions can include: 1.356 + * 1.357 + * refreshAuthentication - (bool) Force re-auth. 1.358 + * 1.359 + * silent - (bool) Prevent any UI interaction. 1.360 + * I.e., try to get an automatic assertion. 1.361 + * 1.362 + */ 1.363 + getAssertion: function(aAudience, aOptions) { 1.364 + if (!aAudience) { 1.365 + return this._error(ERROR_INVALID_AUDIENCE); 1.366 + } 1.367 + 1.368 + if (Services.io.offline) { 1.369 + return this._error(ERROR_OFFLINE); 1.370 + } 1.371 + 1.372 + return this.getAccount().then( 1.373 + user => { 1.374 + if (user) { 1.375 + // We cannot get assertions for unverified accounts. 1.376 + if (!user.verified) { 1.377 + return this._error(ERROR_UNVERIFIED_ACCOUNT, { 1.378 + user: user 1.379 + }); 1.380 + } 1.381 + 1.382 + // RPs might require an authentication refresh. 1.383 + if (aOptions && 1.384 + aOptions.refreshAuthentication) { 1.385 + let gracePeriod = aOptions.refreshAuthentication; 1.386 + if (typeof gracePeriod != 'number' || isNaN(gracePeriod)) { 1.387 + return this._error(ERROR_INVALID_REFRESH_AUTH_VALUE); 1.388 + } 1.389 + 1.390 + if ((Date.now() / 1000) - this._activeSession.authAt > gracePeriod) { 1.391 + // Grace period expired, so we sign out and request the user to 1.392 + // authenticate herself again. If the authentication succeeds, we 1.393 + // will return the assertion. Otherwise, we will return an error. 1.394 + return this._signOut().then( 1.395 + () => { 1.396 + if (aOptions.silent) { 1.397 + return Promise.resolve(null); 1.398 + } 1.399 + return this._uiRequest(UI_REQUEST_REFRESH_AUTH, 1.400 + aAudience, user.accountId); 1.401 + } 1.402 + ); 1.403 + } 1.404 + } 1.405 + 1.406 + return this._getAssertion(aAudience); 1.407 + } 1.408 + 1.409 + log.debug("No signed in user"); 1.410 + 1.411 + if (aOptions && aOptions.silent) { 1.412 + return Promise.resolve(null); 1.413 + } 1.414 + 1.415 + // If there is no currently signed in user, we trigger the signIn UI 1.416 + // flow. 1.417 + return this._uiRequest(UI_REQUEST_SIGN_IN_FLOW, aAudience); 1.418 + } 1.419 + ); 1.420 + } 1.421 + 1.422 +}; 1.423 + 1.424 +FxAccountsManager.init();