|
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/. */ |
|
4 |
|
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 */ |
|
11 |
|
12 "use strict"; |
|
13 |
|
14 this.EXPORTED_SYMBOLS = ["FxAccountsManager"]; |
|
15 |
|
16 const { classes: Cc, interfaces: Ci, utils: Cu } = Components; |
|
17 |
|
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"); |
|
23 |
|
24 this.FxAccountsManager = { |
|
25 |
|
26 init: function() { |
|
27 Services.obs.addObserver(this, ONLOGOUT_NOTIFICATION, false); |
|
28 }, |
|
29 |
|
30 observe: function(aSubject, aTopic, aData) { |
|
31 if (aTopic !== ONLOGOUT_NOTIFICATION) { |
|
32 return; |
|
33 } |
|
34 |
|
35 // Remove the cached session if we get a logout notification. |
|
36 this._activeSession = null; |
|
37 }, |
|
38 |
|
39 // We don't really need to save fxAccounts instance but this way we allow |
|
40 // to mock FxAccounts from tests. |
|
41 _fxAccounts: fxAccounts, |
|
42 |
|
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, |
|
46 |
|
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 } |
|
52 |
|
53 return { |
|
54 accountId: this._activeSession.email, |
|
55 verified: this._activeSession.verified |
|
56 } |
|
57 }, |
|
58 |
|
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 }, |
|
69 |
|
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 }, |
|
77 |
|
78 _serverError: function(aServerResponse) { |
|
79 let error = this._getError({ error: aServerResponse }); |
|
80 return this._error(error ? error : ERROR_SERVER_ERROR, aServerResponse); |
|
81 }, |
|
82 |
|
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 }, |
|
91 |
|
92 _signInSignUp: function(aMethod, aAccountId, aPassword) { |
|
93 if (Services.io.offline) { |
|
94 return this._error(ERROR_OFFLINE); |
|
95 } |
|
96 |
|
97 if (!aAccountId) { |
|
98 return this._error(ERROR_INVALID_ACCOUNTID); |
|
99 } |
|
100 |
|
101 if (!aPassword) { |
|
102 return this._error(ERROR_INVALID_PASSWORD); |
|
103 } |
|
104 |
|
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 } |
|
111 |
|
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 } |
|
130 |
|
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 }, |
|
150 |
|
151 _getAssertion: function(aAudience) { |
|
152 return this._fxAccounts.getAssertion(aAudience); |
|
153 }, |
|
154 |
|
155 _signOut: function() { |
|
156 if (!this._activeSession) { |
|
157 return Promise.resolve(); |
|
158 } |
|
159 |
|
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; |
|
165 |
|
166 return this._fxAccounts.signOut(true).then( |
|
167 () => { |
|
168 // At this point the local session should already be removed. |
|
169 |
|
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 }, |
|
193 |
|
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 } |
|
200 |
|
201 if (!aParams || !Array.isArray(aParams)) { |
|
202 aParams = [aParams]; |
|
203 } |
|
204 |
|
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 } |
|
212 |
|
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 }, |
|
222 |
|
223 // -- API -- |
|
224 |
|
225 signIn: function(aAccountId, aPassword) { |
|
226 return this._signInSignUp("signIn", aAccountId, aPassword); |
|
227 }, |
|
228 |
|
229 signUp: function(aAccountId, aPassword) { |
|
230 return this._signInSignUp("signUp", aAccountId, aPassword); |
|
231 }, |
|
232 |
|
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 }, |
|
248 |
|
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 } |
|
258 |
|
259 log.debug("Account " + JSON.stringify(this._user)); |
|
260 return Promise.resolve(this._user); |
|
261 } |
|
262 |
|
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 } |
|
270 |
|
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 } |
|
278 |
|
279 log.debug("Account " + JSON.stringify(this._user)); |
|
280 return Promise.resolve(this._user); |
|
281 } |
|
282 ); |
|
283 }, |
|
284 |
|
285 queryAccount: function(aAccountId) { |
|
286 log.debug("queryAccount " + aAccountId); |
|
287 if (Services.io.offline) { |
|
288 return this._error(ERROR_OFFLINE); |
|
289 } |
|
290 |
|
291 let deferred = Promise.defer(); |
|
292 |
|
293 if (!aAccountId) { |
|
294 return this._error(ERROR_INVALID_ACCOUNTID); |
|
295 } |
|
296 |
|
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 } |
|
305 |
|
306 return Promise.resolve({ |
|
307 registered: result |
|
308 }); |
|
309 }, |
|
310 reason => { this._serverError(reason); } |
|
311 ); |
|
312 }, |
|
313 |
|
314 verificationStatus: function() { |
|
315 log.debug("verificationStatus"); |
|
316 if (!this._activeSession || !this._activeSession.sessionToken) { |
|
317 this._error(ERROR_NO_TOKEN_SESSION); |
|
318 } |
|
319 |
|
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 } |
|
326 |
|
327 if (Services.io.offline) { |
|
328 this._error(ERROR_OFFLINE); |
|
329 } |
|
330 |
|
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 }, |
|
348 |
|
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 } |
|
364 |
|
365 if (Services.io.offline) { |
|
366 return this._error(ERROR_OFFLINE); |
|
367 } |
|
368 |
|
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 } |
|
378 |
|
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 } |
|
386 |
|
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 } |
|
402 |
|
403 return this._getAssertion(aAudience); |
|
404 } |
|
405 |
|
406 log.debug("No signed in user"); |
|
407 |
|
408 if (aOptions && aOptions.silent) { |
|
409 return Promise.resolve(null); |
|
410 } |
|
411 |
|
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 } |
|
418 |
|
419 }; |
|
420 |
|
421 FxAccountsManager.init(); |