|
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 |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 this.EXPORTED_SYMBOLS = ["fxAccounts", "FxAccounts"]; |
|
6 |
|
7 const {classes: Cc, interfaces: Ci, utils: Cu} = Components; |
|
8 |
|
9 Cu.import("resource://gre/modules/Log.jsm"); |
|
10 Cu.import("resource://gre/modules/Promise.jsm"); |
|
11 Cu.import("resource://gre/modules/osfile.jsm"); |
|
12 Cu.import("resource://services-common/utils.js"); |
|
13 Cu.import("resource://services-crypto/utils.js"); |
|
14 Cu.import("resource://gre/modules/Services.jsm"); |
|
15 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
16 Cu.import("resource://gre/modules/Timer.jsm"); |
|
17 Cu.import("resource://gre/modules/Task.jsm"); |
|
18 Cu.import("resource://gre/modules/FxAccountsCommon.js"); |
|
19 |
|
20 XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsClient", |
|
21 "resource://gre/modules/FxAccountsClient.jsm"); |
|
22 |
|
23 XPCOMUtils.defineLazyModuleGetter(this, "jwcrypto", |
|
24 "resource://gre/modules/identity/jwcrypto.jsm"); |
|
25 |
|
26 // All properties exposed by the public FxAccounts API. |
|
27 let publicProperties = [ |
|
28 "getAccountsClient", |
|
29 "getAccountsSignInURI", |
|
30 "getAccountsSignUpURI", |
|
31 "getAssertion", |
|
32 "getKeys", |
|
33 "getSignedInUser", |
|
34 "loadAndPoll", |
|
35 "localtimeOffsetMsec", |
|
36 "now", |
|
37 "promiseAccountsForceSigninURI", |
|
38 "resendVerificationEmail", |
|
39 "setSignedInUser", |
|
40 "signOut", |
|
41 "version", |
|
42 "whenVerified" |
|
43 ]; |
|
44 |
|
45 // An AccountState object holds all state related to one specific account. |
|
46 // Only one AccountState is ever "current" in the FxAccountsInternal object - |
|
47 // whenever a user logs out or logs in, the current AccountState is discarded, |
|
48 // making it impossible for the wrong state or state data to be accidentally |
|
49 // used. |
|
50 // In addition, it has some promise-related helpers to ensure that if an |
|
51 // attempt is made to resolve a promise on a "stale" state (eg, if an |
|
52 // operation starts, but a different user logs in before the operation |
|
53 // completes), the promise will be rejected. |
|
54 // It is intended to be used thusly: |
|
55 // somePromiseBasedFunction: function() { |
|
56 // let currentState = this.currentAccountState; |
|
57 // return someOtherPromiseFunction().then( |
|
58 // data => currentState.resolve(data) |
|
59 // ); |
|
60 // } |
|
61 // If the state has changed between the function being called and the promise |
|
62 // being resolved, the .resolve() call will actually be rejected. |
|
63 AccountState = function(fxaInternal) { |
|
64 this.fxaInternal = fxaInternal; |
|
65 }; |
|
66 |
|
67 AccountState.prototype = { |
|
68 cert: null, |
|
69 keyPair: null, |
|
70 signedInUser: null, |
|
71 whenVerifiedDeferred: null, |
|
72 whenKeysReadyDeferred: null, |
|
73 |
|
74 get isCurrent() this.fxaInternal && this.fxaInternal.currentAccountState === this, |
|
75 |
|
76 abort: function() { |
|
77 if (this.whenVerifiedDeferred) { |
|
78 this.whenVerifiedDeferred.reject( |
|
79 new Error("Verification aborted; Another user signing in")); |
|
80 this.whenVerifiedDeferred = null; |
|
81 } |
|
82 |
|
83 if (this.whenKeysReadyDeferred) { |
|
84 this.whenKeysReadyDeferred.reject( |
|
85 new Error("Verification aborted; Another user signing in")); |
|
86 this.whenKeysReadyDeferred = null; |
|
87 } |
|
88 this.cert = null; |
|
89 this.keyPair = null; |
|
90 this.signedInUser = null; |
|
91 this.fxaInternal = null; |
|
92 }, |
|
93 |
|
94 getUserAccountData: function() { |
|
95 // Skip disk if user is cached. |
|
96 if (this.signedInUser) { |
|
97 return this.resolve(this.signedInUser.accountData); |
|
98 } |
|
99 |
|
100 return this.fxaInternal.signedInUserStorage.get().then( |
|
101 user => { |
|
102 if (logPII) { |
|
103 // don't stringify unless it will be written. We should replace this |
|
104 // check with param substitutions added in bug 966674 |
|
105 log.debug("getUserAccountData -> " + JSON.stringify(user)); |
|
106 } |
|
107 if (user && user.version == this.version) { |
|
108 log.debug("setting signed in user"); |
|
109 this.signedInUser = user; |
|
110 } |
|
111 return this.resolve(user ? user.accountData : null); |
|
112 }, |
|
113 err => { |
|
114 if (err instanceof OS.File.Error && err.becauseNoSuchFile) { |
|
115 // File hasn't been created yet. That will be done |
|
116 // on the first call to getSignedInUser |
|
117 return this.resolve(null); |
|
118 } |
|
119 return this.reject(err); |
|
120 } |
|
121 ); |
|
122 }, |
|
123 |
|
124 setUserAccountData: function(accountData) { |
|
125 return this.fxaInternal.signedInUserStorage.get().then(record => { |
|
126 if (!this.isCurrent) { |
|
127 return this.reject(new Error("Another user has signed in")); |
|
128 } |
|
129 record.accountData = accountData; |
|
130 this.signedInUser = record; |
|
131 return this.fxaInternal.signedInUserStorage.set(record) |
|
132 .then(() => this.resolve(accountData)); |
|
133 }); |
|
134 }, |
|
135 |
|
136 |
|
137 getCertificate: function(data, keyPair, mustBeValidUntil) { |
|
138 if (logPII) { |
|
139 // don't stringify unless it will be written. We should replace this |
|
140 // check with param substitutions added in bug 966674 |
|
141 log.debug("getCertificate" + JSON.stringify(this.signedInUser)); |
|
142 } |
|
143 // TODO: get the lifetime from the cert's .exp field |
|
144 if (this.cert && this.cert.validUntil > mustBeValidUntil) { |
|
145 log.debug(" getCertificate already had one"); |
|
146 return this.resolve(this.cert.cert); |
|
147 } |
|
148 // else get our cert signed |
|
149 let willBeValidUntil = this.fxaInternal.now() + CERT_LIFETIME; |
|
150 return this.fxaInternal.getCertificateSigned(data.sessionToken, |
|
151 keyPair.serializedPublicKey, |
|
152 CERT_LIFETIME).then( |
|
153 cert => { |
|
154 log.debug("getCertificate got a new one: " + !!cert); |
|
155 this.cert = { |
|
156 cert: cert, |
|
157 validUntil: willBeValidUntil |
|
158 }; |
|
159 return cert; |
|
160 } |
|
161 ).then(result => this.resolve(result)); |
|
162 }, |
|
163 |
|
164 getKeyPair: function(mustBeValidUntil) { |
|
165 if (this.keyPair && (this.keyPair.validUntil > mustBeValidUntil)) { |
|
166 log.debug("getKeyPair: already have a keyPair"); |
|
167 return this.resolve(this.keyPair.keyPair); |
|
168 } |
|
169 // Otherwse, create a keypair and set validity limit. |
|
170 let willBeValidUntil = this.fxaInternal.now() + KEY_LIFETIME; |
|
171 let d = Promise.defer(); |
|
172 jwcrypto.generateKeyPair("DS160", (err, kp) => { |
|
173 if (err) { |
|
174 return this.reject(err); |
|
175 } |
|
176 this.keyPair = { |
|
177 keyPair: kp, |
|
178 validUntil: willBeValidUntil |
|
179 }; |
|
180 log.debug("got keyPair"); |
|
181 delete this.cert; |
|
182 d.resolve(this.keyPair.keyPair); |
|
183 }); |
|
184 return d.promise.then(result => this.resolve(result)); |
|
185 }, |
|
186 |
|
187 resolve: function(result) { |
|
188 if (!this.isCurrent) { |
|
189 log.info("An accountState promise was resolved, but was actually rejected" + |
|
190 " due to a different user being signed in. Originally resolved" + |
|
191 " with: " + result); |
|
192 return Promise.reject(new Error("A different user signed in")); |
|
193 } |
|
194 return Promise.resolve(result); |
|
195 }, |
|
196 |
|
197 reject: function(error) { |
|
198 // It could be argued that we should just let it reject with the original |
|
199 // error - but this runs the risk of the error being (eg) a 401, which |
|
200 // might cause the consumer to attempt some remediation and cause other |
|
201 // problems. |
|
202 if (!this.isCurrent) { |
|
203 log.info("An accountState promise was rejected, but we are ignoring that" + |
|
204 "reason and rejecting it due to a different user being signed in." + |
|
205 "Originally rejected with: " + reason); |
|
206 return Promise.reject(new Error("A different user signed in")); |
|
207 } |
|
208 return Promise.reject(error); |
|
209 }, |
|
210 |
|
211 } |
|
212 |
|
213 /** |
|
214 * Copies properties from a given object to another object. |
|
215 * |
|
216 * @param from (object) |
|
217 * The object we read property descriptors from. |
|
218 * @param to (object) |
|
219 * The object that we set property descriptors on. |
|
220 * @param options (object) (optional) |
|
221 * {keys: [...]} |
|
222 * Lets the caller pass the names of all properties they want to be |
|
223 * copied. Will copy all properties of the given source object by |
|
224 * default. |
|
225 * {bind: object} |
|
226 * Lets the caller specify the object that will be used to .bind() |
|
227 * all function properties we find to. Will bind to the given target |
|
228 * object by default. |
|
229 */ |
|
230 function copyObjectProperties(from, to, opts = {}) { |
|
231 let keys = (opts && opts.keys) || Object.keys(from); |
|
232 let thisArg = (opts && opts.bind) || to; |
|
233 |
|
234 for (let prop of keys) { |
|
235 let desc = Object.getOwnPropertyDescriptor(from, prop); |
|
236 |
|
237 if (typeof(desc.value) == "function") { |
|
238 desc.value = desc.value.bind(thisArg); |
|
239 } |
|
240 |
|
241 if (desc.get) { |
|
242 desc.get = desc.get.bind(thisArg); |
|
243 } |
|
244 |
|
245 if (desc.set) { |
|
246 desc.set = desc.set.bind(thisArg); |
|
247 } |
|
248 |
|
249 Object.defineProperty(to, prop, desc); |
|
250 } |
|
251 } |
|
252 |
|
253 /** |
|
254 * The public API's constructor. |
|
255 */ |
|
256 this.FxAccounts = function (mockInternal) { |
|
257 let internal = new FxAccountsInternal(); |
|
258 let external = {}; |
|
259 |
|
260 // Copy all public properties to the 'external' object. |
|
261 let prototype = FxAccountsInternal.prototype; |
|
262 let options = {keys: publicProperties, bind: internal}; |
|
263 copyObjectProperties(prototype, external, options); |
|
264 |
|
265 // Copy all of the mock's properties to the internal object. |
|
266 if (mockInternal && !mockInternal.onlySetInternal) { |
|
267 copyObjectProperties(mockInternal, internal); |
|
268 } |
|
269 |
|
270 if (mockInternal) { |
|
271 // Exposes the internal object for testing only. |
|
272 external.internal = internal; |
|
273 } |
|
274 |
|
275 return Object.freeze(external); |
|
276 } |
|
277 |
|
278 /** |
|
279 * The internal API's constructor. |
|
280 */ |
|
281 function FxAccountsInternal() { |
|
282 this.version = DATA_FORMAT_VERSION; |
|
283 |
|
284 // Make a local copy of these constants so we can mock it in testing |
|
285 this.POLL_STEP = POLL_STEP; |
|
286 this.POLL_SESSION = POLL_SESSION; |
|
287 // We will create this.pollTimeRemaining below; it will initially be |
|
288 // set to the value of POLL_SESSION. |
|
289 |
|
290 // We interact with the Firefox Accounts auth server in order to confirm that |
|
291 // a user's email has been verified and also to fetch the user's keys from |
|
292 // the server. We manage these processes in possibly long-lived promises |
|
293 // that are internal to this object (never exposed to callers). Because |
|
294 // Firefox Accounts allows for only one logged-in user, and because it's |
|
295 // conceivable that while we are waiting to verify one identity, a caller |
|
296 // could start verification on a second, different identity, we need to be |
|
297 // able to abort all work on the first sign-in process. The currentTimer and |
|
298 // currentAccountState are used for this purpose. |
|
299 // (XXX - should the timer be directly on the currentAccountState?) |
|
300 this.currentTimer = null; |
|
301 this.currentAccountState = new AccountState(this); |
|
302 |
|
303 // We don't reference |profileDir| in the top-level module scope |
|
304 // as we may be imported before we know where it is. |
|
305 this.signedInUserStorage = new JSONStorage({ |
|
306 filename: DEFAULT_STORAGE_FILENAME, |
|
307 baseDir: OS.Constants.Path.profileDir, |
|
308 }); |
|
309 } |
|
310 |
|
311 /** |
|
312 * The internal API's prototype. |
|
313 */ |
|
314 FxAccountsInternal.prototype = { |
|
315 |
|
316 /** |
|
317 * The current data format's version number. |
|
318 */ |
|
319 version: DATA_FORMAT_VERSION, |
|
320 |
|
321 _fxAccountsClient: null, |
|
322 |
|
323 get fxAccountsClient() { |
|
324 if (!this._fxAccountsClient) { |
|
325 this._fxAccountsClient = new FxAccountsClient(); |
|
326 } |
|
327 return this._fxAccountsClient; |
|
328 }, |
|
329 |
|
330 /** |
|
331 * Return the current time in milliseconds as an integer. Allows tests to |
|
332 * manipulate the date to simulate certificate expiration. |
|
333 */ |
|
334 now: function() { |
|
335 return this.fxAccountsClient.now(); |
|
336 }, |
|
337 |
|
338 getAccountsClient: function() { |
|
339 return this.fxAccountsClient; |
|
340 }, |
|
341 |
|
342 /** |
|
343 * Return clock offset in milliseconds, as reported by the fxAccountsClient. |
|
344 * This can be overridden for testing. |
|
345 * |
|
346 * The offset is the number of milliseconds that must be added to the client |
|
347 * clock to make it equal to the server clock. For example, if the client is |
|
348 * five minutes ahead of the server, the localtimeOffsetMsec will be -300000. |
|
349 */ |
|
350 get localtimeOffsetMsec() { |
|
351 return this.fxAccountsClient.localtimeOffsetMsec; |
|
352 }, |
|
353 |
|
354 /** |
|
355 * Ask the server whether the user's email has been verified |
|
356 */ |
|
357 checkEmailStatus: function checkEmailStatus(sessionToken) { |
|
358 return this.fxAccountsClient.recoveryEmailStatus(sessionToken); |
|
359 }, |
|
360 |
|
361 /** |
|
362 * Once the user's email is verified, we can request the keys |
|
363 */ |
|
364 fetchKeys: function fetchKeys(keyFetchToken) { |
|
365 log.debug("fetchKeys: " + !!keyFetchToken); |
|
366 if (logPII) { |
|
367 log.debug("fetchKeys - the token is " + keyFetchToken); |
|
368 } |
|
369 return this.fxAccountsClient.accountKeys(keyFetchToken); |
|
370 }, |
|
371 |
|
372 // set() makes sure that polling is happening, if necessary. |
|
373 // get() does not wait for verification, and returns an object even if |
|
374 // unverified. The caller of get() must check .verified . |
|
375 // The "fxaccounts:onverified" event will fire only when the verified |
|
376 // state goes from false to true, so callers must register their observer |
|
377 // and then call get(). In particular, it will not fire when the account |
|
378 // was found to be verified in a previous boot: if our stored state says |
|
379 // the account is verified, the event will never fire. So callers must do: |
|
380 // register notification observer (go) |
|
381 // userdata = get() |
|
382 // if (userdata.verified()) {go()} |
|
383 |
|
384 /** |
|
385 * Get the user currently signed in to Firefox Accounts. |
|
386 * |
|
387 * @return Promise |
|
388 * The promise resolves to the credentials object of the signed-in user: |
|
389 * { |
|
390 * email: The user's email address |
|
391 * uid: The user's unique id |
|
392 * sessionToken: Session for the FxA server |
|
393 * kA: An encryption key from the FxA server |
|
394 * kB: An encryption key derived from the user's FxA password |
|
395 * verified: email verification status |
|
396 * authAt: The time (seconds since epoch) that this record was |
|
397 * authenticated |
|
398 * } |
|
399 * or null if no user is signed in. |
|
400 */ |
|
401 getSignedInUser: function getSignedInUser() { |
|
402 let currentState = this.currentAccountState; |
|
403 return currentState.getUserAccountData().then(data => { |
|
404 if (!data) { |
|
405 return null; |
|
406 } |
|
407 if (!this.isUserEmailVerified(data)) { |
|
408 // If the email is not verified, start polling for verification, |
|
409 // but return null right away. We don't want to return a promise |
|
410 // that might not be fulfilled for a long time. |
|
411 this.startVerifiedCheck(data); |
|
412 } |
|
413 return data; |
|
414 }).then(result => currentState.resolve(result)); |
|
415 }, |
|
416 |
|
417 /** |
|
418 * Set the current user signed in to Firefox Accounts. |
|
419 * |
|
420 * @param credentials |
|
421 * The credentials object obtained by logging in or creating |
|
422 * an account on the FxA server: |
|
423 * { |
|
424 * authAt: The time (seconds since epoch) that this record was |
|
425 * authenticated |
|
426 * email: The users email address |
|
427 * keyFetchToken: a keyFetchToken which has not yet been used |
|
428 * sessionToken: Session for the FxA server |
|
429 * uid: The user's unique id |
|
430 * unwrapBKey: used to unwrap kB, derived locally from the |
|
431 * password (not revealed to the FxA server) |
|
432 * verified: true/false |
|
433 * } |
|
434 * @return Promise |
|
435 * The promise resolves to null when the data is saved |
|
436 * successfully and is rejected on error. |
|
437 */ |
|
438 setSignedInUser: function setSignedInUser(credentials) { |
|
439 log.debug("setSignedInUser - aborting any existing flows"); |
|
440 this.abortExistingFlow(); |
|
441 |
|
442 let record = {version: this.version, accountData: credentials}; |
|
443 let currentState = this.currentAccountState; |
|
444 // Cache a clone of the credentials object. |
|
445 currentState.signedInUser = JSON.parse(JSON.stringify(record)); |
|
446 |
|
447 // This promise waits for storage, but not for verification. |
|
448 // We're telling the caller that this is durable now. |
|
449 return this.signedInUserStorage.set(record).then(() => { |
|
450 this.notifyObservers(ONLOGIN_NOTIFICATION); |
|
451 if (!this.isUserEmailVerified(credentials)) { |
|
452 this.startVerifiedCheck(credentials); |
|
453 } |
|
454 }).then(result => currentState.resolve(result)); |
|
455 }, |
|
456 |
|
457 /** |
|
458 * returns a promise that fires with the assertion. If there is no verified |
|
459 * signed-in user, fires with null. |
|
460 */ |
|
461 getAssertion: function getAssertion(audience) { |
|
462 log.debug("enter getAssertion()"); |
|
463 let currentState = this.currentAccountState; |
|
464 let mustBeValidUntil = this.now() + ASSERTION_USE_PERIOD; |
|
465 return currentState.getUserAccountData().then(data => { |
|
466 if (!data) { |
|
467 // No signed-in user |
|
468 return null; |
|
469 } |
|
470 if (!this.isUserEmailVerified(data)) { |
|
471 // Signed-in user has not verified email |
|
472 return null; |
|
473 } |
|
474 return currentState.getKeyPair(mustBeValidUntil).then(keyPair => { |
|
475 return currentState.getCertificate(data, keyPair, mustBeValidUntil) |
|
476 .then(cert => { |
|
477 return this.getAssertionFromCert(data, keyPair, cert, audience); |
|
478 }); |
|
479 }); |
|
480 }).then(result => currentState.resolve(result)); |
|
481 }, |
|
482 |
|
483 /** |
|
484 * Resend the verification email fot the currently signed-in user. |
|
485 * |
|
486 */ |
|
487 resendVerificationEmail: function resendVerificationEmail() { |
|
488 let currentState = this.currentAccountState; |
|
489 return this.getSignedInUser().then(data => { |
|
490 // If the caller is asking for verification to be re-sent, and there is |
|
491 // no signed-in user to begin with, this is probably best regarded as an |
|
492 // error. |
|
493 if (data) { |
|
494 this.pollEmailStatus(currentState, data.sessionToken, "start"); |
|
495 return this.fxAccountsClient.resendVerificationEmail(data.sessionToken); |
|
496 } |
|
497 throw new Error("Cannot resend verification email; no signed-in user"); |
|
498 }); |
|
499 }, |
|
500 |
|
501 /* |
|
502 * Reset state such that any previous flow is canceled. |
|
503 */ |
|
504 abortExistingFlow: function abortExistingFlow() { |
|
505 if (this.currentTimer) { |
|
506 log.debug("Polling aborted; Another user signing in"); |
|
507 clearTimeout(this.currentTimer); |
|
508 this.currentTimer = 0; |
|
509 } |
|
510 this.currentAccountState.abort(); |
|
511 this.currentAccountState = new AccountState(this); |
|
512 }, |
|
513 |
|
514 signOut: function signOut(localOnly) { |
|
515 let currentState = this.currentAccountState; |
|
516 let sessionToken; |
|
517 return currentState.getUserAccountData().then(data => { |
|
518 // Save the session token for use in the call to signOut below. |
|
519 sessionToken = data && data.sessionToken; |
|
520 return this._signOutLocal(); |
|
521 }).then(() => { |
|
522 // FxAccountsManager calls here, then does its own call |
|
523 // to FxAccountsClient.signOut(). |
|
524 if (!localOnly) { |
|
525 // Wrap this in a promise so *any* errors in signOut won't |
|
526 // block the local sign out. This is *not* returned. |
|
527 Promise.resolve().then(() => { |
|
528 // This can happen in the background and shouldn't block |
|
529 // the user from signing out. The server must tolerate |
|
530 // clients just disappearing, so this call should be best effort. |
|
531 return this._signOutServer(sessionToken); |
|
532 }).then(null, err => { |
|
533 log.error("Error during remote sign out of Firefox Accounts: " + err); |
|
534 }); |
|
535 } |
|
536 }).then(() => { |
|
537 this.notifyObservers(ONLOGOUT_NOTIFICATION); |
|
538 }); |
|
539 }, |
|
540 |
|
541 /** |
|
542 * This function should be called in conjunction with a server-side |
|
543 * signOut via FxAccountsClient. |
|
544 */ |
|
545 _signOutLocal: function signOutLocal() { |
|
546 this.abortExistingFlow(); |
|
547 this.currentAccountState.signedInUser = null; // clear in-memory cache |
|
548 return this.signedInUserStorage.set(null); |
|
549 }, |
|
550 |
|
551 _signOutServer: function signOutServer(sessionToken) { |
|
552 return this.fxAccountsClient.signOut(sessionToken); |
|
553 }, |
|
554 |
|
555 /** |
|
556 * Fetch encryption keys for the signed-in-user from the FxA API server. |
|
557 * |
|
558 * Not for user consumption. Exists to cause the keys to be fetch. |
|
559 * |
|
560 * Returns user data so that it can be chained with other methods. |
|
561 * |
|
562 * @return Promise |
|
563 * The promise resolves to the credentials object of the signed-in user: |
|
564 * { |
|
565 * email: The user's email address |
|
566 * uid: The user's unique id |
|
567 * sessionToken: Session for the FxA server |
|
568 * kA: An encryption key from the FxA server |
|
569 * kB: An encryption key derived from the user's FxA password |
|
570 * verified: email verification status |
|
571 * } |
|
572 * or null if no user is signed in |
|
573 */ |
|
574 getKeys: function() { |
|
575 let currentState = this.currentAccountState; |
|
576 return currentState.getUserAccountData().then((userData) => { |
|
577 if (!userData) { |
|
578 throw new Error("Can't get keys; User is not signed in"); |
|
579 } |
|
580 if (userData.kA && userData.kB) { |
|
581 return userData; |
|
582 } |
|
583 if (!currentState.whenKeysReadyDeferred) { |
|
584 currentState.whenKeysReadyDeferred = Promise.defer(); |
|
585 if (userData.keyFetchToken) { |
|
586 this.fetchAndUnwrapKeys(userData.keyFetchToken).then( |
|
587 (dataWithKeys) => { |
|
588 if (!dataWithKeys.kA || !dataWithKeys.kB) { |
|
589 currentState.whenKeysReadyDeferred.reject( |
|
590 new Error("user data missing kA or kB") |
|
591 ); |
|
592 return; |
|
593 } |
|
594 currentState.whenKeysReadyDeferred.resolve(dataWithKeys); |
|
595 }, |
|
596 (err) => { |
|
597 currentState.whenKeysReadyDeferred.reject(err); |
|
598 } |
|
599 ); |
|
600 } else { |
|
601 currentState.whenKeysReadyDeferred.reject('No keyFetchToken'); |
|
602 } |
|
603 } |
|
604 return currentState.whenKeysReadyDeferred.promise; |
|
605 }).then(result => currentState.resolve(result)); |
|
606 }, |
|
607 |
|
608 fetchAndUnwrapKeys: function(keyFetchToken) { |
|
609 if (logPII) { |
|
610 log.debug("fetchAndUnwrapKeys: token: " + keyFetchToken); |
|
611 } |
|
612 let currentState = this.currentAccountState; |
|
613 return Task.spawn(function* task() { |
|
614 // Sign out if we don't have a key fetch token. |
|
615 if (!keyFetchToken) { |
|
616 log.warn("improper fetchAndUnwrapKeys() call: token missing"); |
|
617 yield this.signOut(); |
|
618 return null; |
|
619 } |
|
620 |
|
621 let {kA, wrapKB} = yield this.fetchKeys(keyFetchToken); |
|
622 |
|
623 let data = yield currentState.getUserAccountData(); |
|
624 |
|
625 // Sanity check that the user hasn't changed out from under us |
|
626 if (data.keyFetchToken !== keyFetchToken) { |
|
627 throw new Error("Signed in user changed while fetching keys!"); |
|
628 } |
|
629 |
|
630 // Next statements must be synchronous until we setUserAccountData |
|
631 // so that we don't risk getting into a weird state. |
|
632 let kB_hex = CryptoUtils.xor(CommonUtils.hexToBytes(data.unwrapBKey), |
|
633 wrapKB); |
|
634 |
|
635 if (logPII) { |
|
636 log.debug("kB_hex: " + kB_hex); |
|
637 } |
|
638 data.kA = CommonUtils.bytesAsHex(kA); |
|
639 data.kB = CommonUtils.bytesAsHex(kB_hex); |
|
640 |
|
641 delete data.keyFetchToken; |
|
642 |
|
643 log.debug("Keys Obtained: kA=" + !!data.kA + ", kB=" + !!data.kB); |
|
644 if (logPII) { |
|
645 log.debug("Keys Obtained: kA=" + data.kA + ", kB=" + data.kB); |
|
646 } |
|
647 |
|
648 yield currentState.setUserAccountData(data); |
|
649 // We are now ready for business. This should only be invoked once |
|
650 // per setSignedInUser(), regardless of whether we've rebooted since |
|
651 // setSignedInUser() was called. |
|
652 this.notifyObservers(ONVERIFIED_NOTIFICATION); |
|
653 return data; |
|
654 }.bind(this)).then(result => currentState.resolve(result)); |
|
655 }, |
|
656 |
|
657 getAssertionFromCert: function(data, keyPair, cert, audience) { |
|
658 log.debug("getAssertionFromCert"); |
|
659 let payload = {}; |
|
660 let d = Promise.defer(); |
|
661 let options = { |
|
662 duration: ASSERTION_LIFETIME, |
|
663 localtimeOffsetMsec: this.localtimeOffsetMsec, |
|
664 now: this.now() |
|
665 }; |
|
666 let currentState = this.currentAccountState; |
|
667 // "audience" should look like "http://123done.org". |
|
668 // The generated assertion will expire in two minutes. |
|
669 jwcrypto.generateAssertion(cert, keyPair, audience, options, (err, signed) => { |
|
670 if (err) { |
|
671 log.error("getAssertionFromCert: " + err); |
|
672 d.reject(err); |
|
673 } else { |
|
674 log.debug("getAssertionFromCert returning signed: " + !!signed); |
|
675 if (logPII) { |
|
676 log.debug("getAssertionFromCert returning signed: " + signed); |
|
677 } |
|
678 d.resolve(signed); |
|
679 } |
|
680 }); |
|
681 return d.promise.then(result => currentState.resolve(result)); |
|
682 }, |
|
683 |
|
684 getCertificateSigned: function(sessionToken, serializedPublicKey, lifetime) { |
|
685 log.debug("getCertificateSigned: " + !!sessionToken + " " + !!serializedPublicKey); |
|
686 if (logPII) { |
|
687 log.debug("getCertificateSigned: " + sessionToken + " " + serializedPublicKey); |
|
688 } |
|
689 return this.fxAccountsClient.signCertificate( |
|
690 sessionToken, |
|
691 JSON.parse(serializedPublicKey), |
|
692 lifetime |
|
693 ); |
|
694 }, |
|
695 |
|
696 getUserAccountData: function() { |
|
697 return this.currentAccountState.getUserAccountData(); |
|
698 }, |
|
699 |
|
700 isUserEmailVerified: function isUserEmailVerified(data) { |
|
701 return !!(data && data.verified); |
|
702 }, |
|
703 |
|
704 /** |
|
705 * Setup for and if necessary do email verification polling. |
|
706 */ |
|
707 loadAndPoll: function() { |
|
708 let currentState = this.currentAccountState; |
|
709 return currentState.getUserAccountData() |
|
710 .then(data => { |
|
711 if (data && !this.isUserEmailVerified(data)) { |
|
712 this.pollEmailStatus(currentState, data.sessionToken, "start"); |
|
713 } |
|
714 return data; |
|
715 }); |
|
716 }, |
|
717 |
|
718 startVerifiedCheck: function(data) { |
|
719 log.debug("startVerifiedCheck " + JSON.stringify(data)); |
|
720 // Get us to the verified state, then get the keys. This returns a promise |
|
721 // that will fire when we are completely ready. |
|
722 // |
|
723 // Login is truly complete once keys have been fetched, so once getKeys() |
|
724 // obtains and stores kA and kB, it will fire the onverified observer |
|
725 // notification. |
|
726 return this.whenVerified(data) |
|
727 .then(() => this.getKeys()); |
|
728 }, |
|
729 |
|
730 whenVerified: function(data) { |
|
731 let currentState = this.currentAccountState; |
|
732 if (data.verified) { |
|
733 log.debug("already verified"); |
|
734 return currentState.resolve(data); |
|
735 } |
|
736 if (!currentState.whenVerifiedDeferred) { |
|
737 log.debug("whenVerified promise starts polling for verified email"); |
|
738 this.pollEmailStatus(currentState, data.sessionToken, "start"); |
|
739 } |
|
740 return currentState.whenVerifiedDeferred.promise.then( |
|
741 result => currentState.resolve(result) |
|
742 ); |
|
743 }, |
|
744 |
|
745 notifyObservers: function(topic) { |
|
746 log.debug("Notifying observers of " + topic); |
|
747 Services.obs.notifyObservers(null, topic, null); |
|
748 }, |
|
749 |
|
750 // XXX - pollEmailStatus should maybe be on the AccountState object? |
|
751 pollEmailStatus: function pollEmailStatus(currentState, sessionToken, why) { |
|
752 log.debug("entering pollEmailStatus: " + why); |
|
753 if (why == "start") { |
|
754 // If we were already polling, stop and start again. This could happen |
|
755 // if the user requested the verification email to be resent while we |
|
756 // were already polling for receipt of an earlier email. |
|
757 this.pollTimeRemaining = this.POLL_SESSION; |
|
758 if (!currentState.whenVerifiedDeferred) { |
|
759 currentState.whenVerifiedDeferred = Promise.defer(); |
|
760 // This deferred might not end up with any handlers (eg, if sync |
|
761 // is yet to start up.) This might cause "A promise chain failed to |
|
762 // handle a rejection" messages, so add an error handler directly |
|
763 // on the promise to log the error. |
|
764 currentState.whenVerifiedDeferred.promise.then(null, err => { |
|
765 log.info("the wait for user verification was stopped: " + err); |
|
766 }); |
|
767 } |
|
768 } |
|
769 |
|
770 this.checkEmailStatus(sessionToken) |
|
771 .then((response) => { |
|
772 log.debug("checkEmailStatus -> " + JSON.stringify(response)); |
|
773 if (response && response.verified) { |
|
774 // Bug 947056 - Server should be able to tell FxAccounts.jsm to back |
|
775 // off or stop polling altogether |
|
776 currentState.getUserAccountData() |
|
777 .then((data) => { |
|
778 data.verified = true; |
|
779 return currentState.setUserAccountData(data); |
|
780 }) |
|
781 .then((data) => { |
|
782 // Now that the user is verified, we can proceed to fetch keys |
|
783 if (currentState.whenVerifiedDeferred) { |
|
784 currentState.whenVerifiedDeferred.resolve(data); |
|
785 delete currentState.whenVerifiedDeferred; |
|
786 } |
|
787 }); |
|
788 } else { |
|
789 log.debug("polling with step = " + this.POLL_STEP); |
|
790 this.pollTimeRemaining -= this.POLL_STEP; |
|
791 log.debug("time remaining: " + this.pollTimeRemaining); |
|
792 if (this.pollTimeRemaining > 0) { |
|
793 this.currentTimer = setTimeout(() => { |
|
794 this.pollEmailStatus(currentState, sessionToken, "timer")}, this.POLL_STEP); |
|
795 log.debug("started timer " + this.currentTimer); |
|
796 } else { |
|
797 if (currentState.whenVerifiedDeferred) { |
|
798 currentState.whenVerifiedDeferred.reject( |
|
799 new Error("User email verification timed out.") |
|
800 ); |
|
801 delete currentState.whenVerifiedDeferred; |
|
802 } |
|
803 } |
|
804 } |
|
805 }); |
|
806 }, |
|
807 |
|
808 // Return the URI of the remote UI flows. |
|
809 getAccountsSignUpURI: function() { |
|
810 let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.signup.uri"); |
|
811 if (!/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting |
|
812 throw new Error("Firefox Accounts server must use HTTPS"); |
|
813 } |
|
814 return url; |
|
815 }, |
|
816 |
|
817 // Return the URI of the remote UI flows. |
|
818 getAccountsSignInURI: function() { |
|
819 let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.signin.uri"); |
|
820 if (!/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting |
|
821 throw new Error("Firefox Accounts server must use HTTPS"); |
|
822 } |
|
823 return url; |
|
824 }, |
|
825 |
|
826 // Returns a promise that resolves with the URL to use to force a re-signin |
|
827 // of the current account. |
|
828 promiseAccountsForceSigninURI: function() { |
|
829 let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.force_auth.uri"); |
|
830 if (!/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting |
|
831 throw new Error("Firefox Accounts server must use HTTPS"); |
|
832 } |
|
833 let currentState = this.currentAccountState; |
|
834 // but we need to append the email address onto a query string. |
|
835 return this.getSignedInUser().then(accountData => { |
|
836 if (!accountData) { |
|
837 return null; |
|
838 } |
|
839 let newQueryPortion = url.indexOf("?") == -1 ? "?" : "&"; |
|
840 newQueryPortion += "email=" + encodeURIComponent(accountData.email); |
|
841 return url + newQueryPortion; |
|
842 }).then(result => currentState.resolve(result)); |
|
843 } |
|
844 }; |
|
845 |
|
846 /** |
|
847 * JSONStorage constructor that creates instances that may set/get |
|
848 * to a specified file, in a directory that will be created if it |
|
849 * doesn't exist. |
|
850 * |
|
851 * @param options { |
|
852 * filename: of the file to write to |
|
853 * baseDir: directory where the file resides |
|
854 * } |
|
855 * @return instance |
|
856 */ |
|
857 function JSONStorage(options) { |
|
858 this.baseDir = options.baseDir; |
|
859 this.path = OS.Path.join(options.baseDir, options.filename); |
|
860 }; |
|
861 |
|
862 JSONStorage.prototype = { |
|
863 set: function(contents) { |
|
864 return OS.File.makeDir(this.baseDir, {ignoreExisting: true}) |
|
865 .then(CommonUtils.writeJSON.bind(null, contents, this.path)); |
|
866 }, |
|
867 |
|
868 get: function() { |
|
869 return CommonUtils.readJSON(this.path); |
|
870 } |
|
871 }; |
|
872 |
|
873 // A getter for the instance to export |
|
874 XPCOMUtils.defineLazyGetter(this, "fxAccounts", function() { |
|
875 let a = new FxAccounts(); |
|
876 |
|
877 // XXX Bug 947061 - We need a strategy for resuming email verification after |
|
878 // browser restart |
|
879 a.loadAndPoll(); |
|
880 |
|
881 return a; |
|
882 }); |