Wed, 31 Dec 2014 06:09:35 +0100
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();