|
1 /* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */ |
|
2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
5 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 |
|
7 "use strict"; |
|
8 |
|
9 const Cu = Components.utils; |
|
10 const Ci = Components.interfaces; |
|
11 const Cc = Components.classes; |
|
12 const Cr = Components.results; |
|
13 |
|
14 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
15 Cu.import("resource://gre/modules/Services.jsm"); |
|
16 Cu.import("resource://gre/modules/identity/LogUtils.jsm"); |
|
17 Cu.import("resource://gre/modules/identity/Sandbox.jsm"); |
|
18 |
|
19 this.EXPORTED_SYMBOLS = ["IdentityProvider"]; |
|
20 const FALLBACK_PROVIDER = "browserid.org"; |
|
21 |
|
22 XPCOMUtils.defineLazyModuleGetter(this, |
|
23 "jwcrypto", |
|
24 "resource://gre/modules/identity/jwcrypto.jsm"); |
|
25 |
|
26 function log(...aMessageArgs) { |
|
27 Logger.log.apply(Logger, ["IDP"].concat(aMessageArgs)); |
|
28 } |
|
29 function reportError(...aMessageArgs) { |
|
30 Logger.reportError.apply(Logger, ["IDP"].concat(aMessageArgs)); |
|
31 } |
|
32 |
|
33 |
|
34 function IdentityProviderService() { |
|
35 XPCOMUtils.defineLazyModuleGetter(this, |
|
36 "_store", |
|
37 "resource://gre/modules/identity/IdentityStore.jsm", |
|
38 "IdentityStore"); |
|
39 |
|
40 this.reset(); |
|
41 } |
|
42 |
|
43 IdentityProviderService.prototype = { |
|
44 QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]), |
|
45 _sandboxConfigured: false, |
|
46 |
|
47 observe: function observe(aSubject, aTopic, aData) { |
|
48 switch (aTopic) { |
|
49 case "quit-application-granted": |
|
50 Services.obs.removeObserver(this, "quit-application-granted"); |
|
51 this.shutdown(); |
|
52 break; |
|
53 } |
|
54 }, |
|
55 |
|
56 reset: function IDP_reset() { |
|
57 // Clear the provisioning flows. Provision flows contain an |
|
58 // identity, idpParams (how to reach the IdP to provision and |
|
59 // authenticate), a callback (a completion callback for when things |
|
60 // are done), and a provisioningFrame (which is the provisioning |
|
61 // sandbox). Additionally, two callbacks will be attached: |
|
62 // beginProvisioningCallback and genKeyPairCallback. |
|
63 this._provisionFlows = {}; |
|
64 |
|
65 // Clear the authentication flows. Authentication flows attach |
|
66 // to provision flows. In the process of provisioning an id, it |
|
67 // may be necessary to authenticate with an IdP. The authentication |
|
68 // flow maintains the state of that authentication process. |
|
69 this._authenticationFlows = {}; |
|
70 }, |
|
71 |
|
72 getProvisionFlow: function getProvisionFlow(aProvId, aErrBack) { |
|
73 let provFlow = this._provisionFlows[aProvId]; |
|
74 if (provFlow) { |
|
75 return provFlow; |
|
76 } |
|
77 |
|
78 let err = "No provisioning flow found with id " + aProvId; |
|
79 log("ERROR:", err); |
|
80 if (typeof aErrBack === 'function') { |
|
81 aErrBack(err); |
|
82 } |
|
83 }, |
|
84 |
|
85 shutdown: function RP_shutdown() { |
|
86 this.reset(); |
|
87 |
|
88 if (this._sandboxConfigured) { |
|
89 // Tear down message manager listening on the hidden window |
|
90 Cu.import("resource://gre/modules/DOMIdentity.jsm"); |
|
91 DOMIdentity._configureMessages(Services.appShell.hiddenDOMWindow, false); |
|
92 this._sandboxConfigured = false; |
|
93 } |
|
94 |
|
95 Services.obs.removeObserver(this, "quit-application-granted"); |
|
96 }, |
|
97 |
|
98 get securityLevel() { |
|
99 return 1; |
|
100 }, |
|
101 |
|
102 get certDuration() { |
|
103 switch(this.securityLevel) { |
|
104 default: |
|
105 return 3600; |
|
106 } |
|
107 }, |
|
108 |
|
109 /** |
|
110 * Provision an Identity |
|
111 * |
|
112 * @param aIdentity |
|
113 * (string) the email we're logging in with |
|
114 * |
|
115 * @param aIDPParams |
|
116 * (object) parameters of the IdP |
|
117 * |
|
118 * @param aCallback |
|
119 * (function) callback to invoke on completion |
|
120 * with first-positional parameter the error. |
|
121 */ |
|
122 _provisionIdentity: function _provisionIdentity(aIdentity, aIDPParams, aProvId, aCallback) { |
|
123 let provPath = aIDPParams.idpParams.provisioning; |
|
124 let url = Services.io.newURI("https://" + aIDPParams.domain, null, null).resolve(provPath); |
|
125 log("_provisionIdentity: identity:", aIdentity, "url:", url); |
|
126 |
|
127 // If aProvId is not null, then we already have a flow |
|
128 // with a sandbox. Otherwise, get a sandbox and create a |
|
129 // new provision flow. |
|
130 |
|
131 if (aProvId) { |
|
132 // Re-use an existing sandbox |
|
133 log("_provisionIdentity: re-using sandbox in provisioning flow with id:", aProvId); |
|
134 this._provisionFlows[aProvId].provisioningSandbox.reload(); |
|
135 |
|
136 } else { |
|
137 this._createProvisioningSandbox(url, function createdSandbox(aSandbox) { |
|
138 // create a provisioning flow, using the sandbox id, and |
|
139 // stash callback associated with this provisioning workflow. |
|
140 |
|
141 let provId = aSandbox.id; |
|
142 this._provisionFlows[provId] = { |
|
143 identity: aIdentity, |
|
144 idpParams: aIDPParams, |
|
145 securityLevel: this.securityLevel, |
|
146 provisioningSandbox: aSandbox, |
|
147 callback: function doCallback(aErr) { |
|
148 aCallback(aErr, provId); |
|
149 }, |
|
150 }; |
|
151 |
|
152 log("_provisionIdentity: Created sandbox and provisioning flow with id:", provId); |
|
153 // XXX bug 769862 - provisioning flow should timeout after N seconds |
|
154 |
|
155 }.bind(this)); |
|
156 } |
|
157 }, |
|
158 |
|
159 // DOM Methods |
|
160 /** |
|
161 * the provisioning iframe sandbox has called navigator.id.beginProvisioning() |
|
162 * |
|
163 * @param aCaller |
|
164 * (object) the iframe sandbox caller with all callbacks and |
|
165 * other information. Callbacks include: |
|
166 * - doBeginProvisioningCallback(id, duration_s) |
|
167 * - doGenKeyPairCallback(pk) |
|
168 */ |
|
169 beginProvisioning: function beginProvisioning(aCaller) { |
|
170 log("beginProvisioning:", aCaller.id); |
|
171 |
|
172 // Expect a flow for this caller already to be underway. |
|
173 let provFlow = this.getProvisionFlow(aCaller.id, aCaller.doError); |
|
174 |
|
175 // keep the caller object around |
|
176 provFlow.caller = aCaller; |
|
177 |
|
178 let identity = provFlow.identity; |
|
179 let frame = provFlow.provisioningFrame; |
|
180 |
|
181 // Determine recommended length of cert. |
|
182 let duration = this.certDuration; |
|
183 |
|
184 // Make a record that we have begun provisioning. This is required |
|
185 // for genKeyPair. |
|
186 provFlow.didBeginProvisioning = true; |
|
187 |
|
188 // Let the sandbox know to invoke the callback to beginProvisioning with |
|
189 // the identity and cert length. |
|
190 return aCaller.doBeginProvisioningCallback(identity, duration); |
|
191 }, |
|
192 |
|
193 /** |
|
194 * the provisioning iframe sandbox has called |
|
195 * navigator.id.raiseProvisioningFailure() |
|
196 * |
|
197 * @param aProvId |
|
198 * (int) the identifier of the provisioning flow tied to that sandbox |
|
199 * @param aReason |
|
200 */ |
|
201 raiseProvisioningFailure: function raiseProvisioningFailure(aProvId, aReason) { |
|
202 reportError("Provisioning failure", aReason); |
|
203 |
|
204 // look up the provisioning caller and its callback |
|
205 let provFlow = this.getProvisionFlow(aProvId); |
|
206 |
|
207 // Sandbox is deleted in _cleanUpProvisionFlow in case we re-use it. |
|
208 |
|
209 // This may be either a "soft" or "hard" fail. If it's a |
|
210 // soft fail, we'll flow through setAuthenticationFlow, where |
|
211 // the provision flow data will be copied into a new auth |
|
212 // flow. If it's a hard fail, then the callback will be |
|
213 // responsible for cleaning up the now defunct provision flow. |
|
214 |
|
215 // invoke the callback with an error. |
|
216 provFlow.callback(aReason); |
|
217 }, |
|
218 |
|
219 /** |
|
220 * When navigator.id.genKeyPair is called from provisioning iframe sandbox. |
|
221 * Generates a keypair for the current user being provisioned. |
|
222 * |
|
223 * @param aProvId |
|
224 * (int) the identifier of the provisioning caller tied to that sandbox |
|
225 * |
|
226 * It is an error to call genKeypair without receiving the callback for |
|
227 * the beginProvisioning() call first. |
|
228 */ |
|
229 genKeyPair: function genKeyPair(aProvId) { |
|
230 // Look up the provisioning caller and make sure it's valid. |
|
231 let provFlow = this.getProvisionFlow(aProvId); |
|
232 |
|
233 if (!provFlow.didBeginProvisioning) { |
|
234 let errStr = "ERROR: genKeyPair called before beginProvisioning"; |
|
235 log(errStr); |
|
236 provFlow.callback(errStr); |
|
237 return; |
|
238 } |
|
239 |
|
240 // Ok generate a keypair |
|
241 jwcrypto.generateKeyPair(jwcrypto.ALGORITHMS.DS160, function gkpCb(err, kp) { |
|
242 log("in gkp callback"); |
|
243 if (err) { |
|
244 log("ERROR: genKeyPair:", err); |
|
245 provFlow.callback(err); |
|
246 return; |
|
247 } |
|
248 |
|
249 provFlow.kp = kp; |
|
250 |
|
251 // Serialize the publicKey of the keypair and send it back to the |
|
252 // sandbox. |
|
253 log("genKeyPair: generated keypair for provisioning flow with id:", aProvId); |
|
254 provFlow.caller.doGenKeyPairCallback(provFlow.kp.serializedPublicKey); |
|
255 }.bind(this)); |
|
256 }, |
|
257 |
|
258 /** |
|
259 * When navigator.id.registerCertificate is called from provisioning iframe |
|
260 * sandbox. |
|
261 * |
|
262 * Sets the certificate for the user for which a certificate was requested |
|
263 * via a preceding call to beginProvisioning (and genKeypair). |
|
264 * |
|
265 * @param aProvId |
|
266 * (integer) the identifier of the provisioning caller tied to that |
|
267 * sandbox |
|
268 * |
|
269 * @param aCert |
|
270 * (String) A JWT representing the signed certificate for the user |
|
271 * being provisioned, provided by the IdP. |
|
272 */ |
|
273 registerCertificate: function registerCertificate(aProvId, aCert) { |
|
274 log("registerCertificate:", aProvId, aCert); |
|
275 |
|
276 // look up provisioning caller, make sure it's valid. |
|
277 let provFlow = this.getProvisionFlow(aProvId); |
|
278 |
|
279 if (!provFlow.caller) { |
|
280 reportError("registerCertificate", "No provision flow or caller"); |
|
281 return; |
|
282 } |
|
283 if (!provFlow.kp) { |
|
284 let errStr = "Cannot register a certificate without a keypair"; |
|
285 reportError("registerCertificate", errStr); |
|
286 provFlow.callback(errStr); |
|
287 return; |
|
288 } |
|
289 |
|
290 // store the keypair and certificate just provided in IDStore. |
|
291 this._store.addIdentity(provFlow.identity, provFlow.kp, aCert); |
|
292 |
|
293 // Great success! |
|
294 provFlow.callback(null); |
|
295 |
|
296 // Clean up the flow. |
|
297 this._cleanUpProvisionFlow(aProvId); |
|
298 }, |
|
299 |
|
300 /** |
|
301 * Begin the authentication process with an IdP |
|
302 * |
|
303 * @param aProvId |
|
304 * (int) the identifier of the provisioning flow which failed |
|
305 * |
|
306 * @param aCallback |
|
307 * (function) to invoke upon completion, with |
|
308 * first-positional-param error. |
|
309 */ |
|
310 _doAuthentication: function _doAuthentication(aProvId, aIDPParams) { |
|
311 log("_doAuthentication: provId:", aProvId, "idpParams:", aIDPParams); |
|
312 // create an authentication caller and its identifier AuthId |
|
313 // stash aIdentity, idpparams, and callback in it. |
|
314 |
|
315 // extract authentication URL from idpParams |
|
316 let authPath = aIDPParams.idpParams.authentication; |
|
317 let authURI = Services.io.newURI("https://" + aIDPParams.domain, null, null).resolve(authPath); |
|
318 |
|
319 // beginAuthenticationFlow causes the "identity-auth" topic to be |
|
320 // observed. Since it's sending a notification to the DOM, there's |
|
321 // no callback. We wait for the DOM to trigger the next phase of |
|
322 // provisioning. |
|
323 this._beginAuthenticationFlow(aProvId, authURI); |
|
324 |
|
325 // either we bind the AuthID to the sandbox ourselves, or UX does that, |
|
326 // in which case we need to tell UX the AuthId. |
|
327 // Currently, the UX creates the UI and gets the AuthId from the window |
|
328 // and sets is with setAuthenticationFlow |
|
329 }, |
|
330 |
|
331 /** |
|
332 * The authentication frame has called navigator.id.beginAuthentication |
|
333 * |
|
334 * IMPORTANT: the aCaller is *always* non-null, even if this is called from |
|
335 * a regular content page. We have to make sure, on every DOM call, that |
|
336 * aCaller is an expected authentication-flow identifier. If not, we throw |
|
337 * an error or something. |
|
338 * |
|
339 * @param aCaller |
|
340 * (object) the authentication caller |
|
341 * |
|
342 */ |
|
343 beginAuthentication: function beginAuthentication(aCaller) { |
|
344 log("beginAuthentication: caller id:", aCaller.id); |
|
345 |
|
346 // Begin the authentication flow after having concluded a provisioning |
|
347 // flow. The aCaller that the DOM gives us will have the same ID as |
|
348 // the provisioning flow we just concluded. (see setAuthenticationFlow) |
|
349 let authFlow = this._authenticationFlows[aCaller.id]; |
|
350 if (!authFlow) { |
|
351 return aCaller.doError("beginAuthentication: no flow for caller id", aCaller.id); |
|
352 } |
|
353 |
|
354 authFlow.caller = aCaller; |
|
355 |
|
356 let identity = this._provisionFlows[authFlow.provId].identity; |
|
357 |
|
358 // tell the UI to start the authentication process |
|
359 log("beginAuthentication: authFlow:", aCaller.id, "identity:", identity); |
|
360 return authFlow.caller.doBeginAuthenticationCallback(identity); |
|
361 }, |
|
362 |
|
363 /** |
|
364 * The auth frame has called navigator.id.completeAuthentication |
|
365 * |
|
366 * @param aAuthId |
|
367 * (int) the identifier of the authentication caller tied to that sandbox |
|
368 * |
|
369 */ |
|
370 completeAuthentication: function completeAuthentication(aAuthId) { |
|
371 log("completeAuthentication:", aAuthId); |
|
372 |
|
373 // look up the AuthId caller, and get its callback. |
|
374 let authFlow = this._authenticationFlows[aAuthId]; |
|
375 if (!authFlow) { |
|
376 reportError("completeAuthentication", "No auth flow with id", aAuthId); |
|
377 return; |
|
378 } |
|
379 let provId = authFlow.provId; |
|
380 |
|
381 // delete caller |
|
382 delete authFlow['caller']; |
|
383 delete this._authenticationFlows[aAuthId]; |
|
384 |
|
385 let provFlow = this.getProvisionFlow(provId); |
|
386 provFlow.didAuthentication = true; |
|
387 let subject = { |
|
388 rpId: provFlow.rpId, |
|
389 identity: provFlow.identity, |
|
390 }; |
|
391 Services.obs.notifyObservers({ wrappedJSObject: subject }, "identity-auth-complete", aAuthId); |
|
392 }, |
|
393 |
|
394 /** |
|
395 * The auth frame has called navigator.id.cancelAuthentication |
|
396 * |
|
397 * @param aAuthId |
|
398 * (int) the identifier of the authentication caller |
|
399 * |
|
400 */ |
|
401 cancelAuthentication: function cancelAuthentication(aAuthId) { |
|
402 log("cancelAuthentication:", aAuthId); |
|
403 |
|
404 // look up the AuthId caller, and get its callback. |
|
405 let authFlow = this._authenticationFlows[aAuthId]; |
|
406 if (!authFlow) { |
|
407 reportError("cancelAuthentication", "No auth flow with id:", aAuthId); |
|
408 return; |
|
409 } |
|
410 let provId = authFlow.provId; |
|
411 |
|
412 // delete caller |
|
413 delete authFlow['caller']; |
|
414 delete this._authenticationFlows[aAuthId]; |
|
415 |
|
416 let provFlow = this.getProvisionFlow(provId); |
|
417 provFlow.didAuthentication = true; |
|
418 Services.obs.notifyObservers(null, "identity-auth-complete", aAuthId); |
|
419 |
|
420 // invoke callback with ERROR. |
|
421 let errStr = "Authentication canceled by IDP"; |
|
422 log("ERROR: cancelAuthentication:", errStr); |
|
423 provFlow.callback(errStr); |
|
424 }, |
|
425 |
|
426 /** |
|
427 * Called by the UI to set the ID and caller for the authentication flow after it gets its ID |
|
428 */ |
|
429 setAuthenticationFlow: function(aAuthId, aProvId) { |
|
430 // this is the transition point between the two flows, |
|
431 // provision and authenticate. We tell the auth flow which |
|
432 // provisioning flow it is started from. |
|
433 log("setAuthenticationFlow: authId:", aAuthId, "provId:", aProvId); |
|
434 this._authenticationFlows[aAuthId] = { provId: aProvId }; |
|
435 this._provisionFlows[aProvId].authId = aAuthId; |
|
436 }, |
|
437 |
|
438 /** |
|
439 * Load the provisioning URL in a hidden frame to start the provisioning |
|
440 * process. |
|
441 */ |
|
442 _createProvisioningSandbox: function _createProvisioningSandbox(aURL, aCallback) { |
|
443 log("_createProvisioningSandbox:", aURL); |
|
444 |
|
445 if (!this._sandboxConfigured) { |
|
446 // Configure message manager listening on the hidden window |
|
447 Cu.import("resource://gre/modules/DOMIdentity.jsm"); |
|
448 DOMIdentity._configureMessages(Services.appShell.hiddenDOMWindow, true); |
|
449 this._sandboxConfigured = true; |
|
450 } |
|
451 |
|
452 new Sandbox(aURL, aCallback); |
|
453 }, |
|
454 |
|
455 /** |
|
456 * Load the authentication UI to start the authentication process. |
|
457 */ |
|
458 _beginAuthenticationFlow: function _beginAuthenticationFlow(aProvId, aURL) { |
|
459 log("_beginAuthenticationFlow:", aProvId, aURL); |
|
460 let propBag = {provId: aProvId}; |
|
461 |
|
462 Services.obs.notifyObservers({wrappedJSObject:propBag}, "identity-auth", aURL); |
|
463 }, |
|
464 |
|
465 /** |
|
466 * Clean up a provision flow and the authentication flow and sandbox |
|
467 * that may be attached to it. |
|
468 */ |
|
469 _cleanUpProvisionFlow: function _cleanUpProvisionFlow(aProvId) { |
|
470 log('_cleanUpProvisionFlow:', aProvId); |
|
471 let prov = this._provisionFlows[aProvId]; |
|
472 |
|
473 // Clean up the sandbox, if there is one. |
|
474 if (prov.provisioningSandbox) { |
|
475 let sandbox = this._provisionFlows[aProvId]['provisioningSandbox']; |
|
476 if (sandbox.free) { |
|
477 log('_cleanUpProvisionFlow: freeing sandbox'); |
|
478 sandbox.free(); |
|
479 } |
|
480 delete this._provisionFlows[aProvId]['provisioningSandbox']; |
|
481 } |
|
482 |
|
483 // Clean up a related authentication flow, if there is one. |
|
484 if (this._authenticationFlows[prov.authId]) { |
|
485 delete this._authenticationFlows[prov.authId]; |
|
486 } |
|
487 |
|
488 // Finally delete the provision flow |
|
489 delete this._provisionFlows[aProvId]; |
|
490 } |
|
491 |
|
492 }; |
|
493 |
|
494 this.IdentityProvider = new IdentityProviderService(); |