Bumping manifests a=b2g-bump
[gecko.git] / toolkit / identity / IdentityProvider.jsm
blob761abff4d8cf2008f4de0514a7c8f089910b5699
1 /* -*- js-indent-level: 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/. */
7 "use strict";
9 const Cu = Components.utils;
10 const Ci = Components.interfaces;
11 const Cc = Components.classes;
12 const Cr = Components.results;
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");
19 this.EXPORTED_SYMBOLS = ["IdentityProvider"];
20 const FALLBACK_PROVIDER = "browserid.org";
22 XPCOMUtils.defineLazyModuleGetter(this,
23                                   "jwcrypto",
24                                   "resource://gre/modules/identity/jwcrypto.jsm");
26 function log(...aMessageArgs) {
27   Logger.log.apply(Logger, ["IDP"].concat(aMessageArgs));
29 function reportError(...aMessageArgs) {
30   Logger.reportError.apply(Logger, ["IDP"].concat(aMessageArgs));
34 function IdentityProviderService() {
35   XPCOMUtils.defineLazyModuleGetter(this,
36                                     "_store",
37                                     "resource://gre/modules/identity/IdentityStore.jsm",
38                                     "IdentityStore");
40   this.reset();
43 IdentityProviderService.prototype = {
44   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
45   _sandboxConfigured: false,
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   },
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 = {};
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   },
72   getProvisionFlow: function getProvisionFlow(aProvId, aErrBack) {
73     let provFlow = this._provisionFlows[aProvId];
74     if (provFlow) {
75       return provFlow;
76     }
78     let err = "No provisioning flow found with id " + aProvId;
79     log("ERROR:", err);
80     if (typeof aErrBack === 'function') {
81       aErrBack(err);
82     }
83   },
85   shutdown: function RP_shutdown() {
86     this.reset();
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     }
95     Services.obs.removeObserver(this, "quit-application-granted");
96   },
98   get securityLevel() {
99     return 1;
100   },
102   get certDuration() {
103     switch(this.securityLevel) {
104       default:
105         return 3600;
106     }
107   },
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);
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.
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();
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.
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         };
152         log("_provisionIdentity: Created sandbox and provisioning flow with id:", provId);
153         // XXX bug 769862 - provisioning flow should timeout after N seconds
155       }.bind(this));
156     }
157   },
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);
172     // Expect a flow for this caller already to be underway.
173     let provFlow = this.getProvisionFlow(aCaller.id, aCaller.doError);
175     // keep the caller object around
176     provFlow.caller = aCaller;
178     let identity = provFlow.identity;
179     let frame = provFlow.provisioningFrame;
181     // Determine recommended length of cert.
182     let duration = this.certDuration;
184     // Make a record that we have begun provisioning.  This is required
185     // for genKeyPair.
186     provFlow.didBeginProvisioning = true;
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   },
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);
204     // look up the provisioning caller and its callback
205     let provFlow = this.getProvisionFlow(aProvId);
207     // Sandbox is deleted in _cleanUpProvisionFlow in case we re-use it.
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.
215     // invoke the callback with an error.
216     provFlow.callback(aReason);
217   },
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);
233     if (!provFlow.didBeginProvisioning) {
234       let errStr = "ERROR: genKeyPair called before beginProvisioning";
235       log(errStr);
236       provFlow.callback(errStr);
237       return;
238     }
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       }
249       provFlow.kp = kp;
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   },
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);
276     // look up provisioning caller, make sure it's valid.
277     let provFlow = this.getProvisionFlow(aProvId);
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     }
290     // store the keypair and certificate just provided in IDStore.
291     this._store.addIdentity(provFlow.identity, provFlow.kp, aCert);
293     // Great success!
294     provFlow.callback(null);
296     // Clean up the flow.
297     this._cleanUpProvisionFlow(aProvId);
298   },
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.
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);
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);
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   },
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);
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     }
354     authFlow.caller = aCaller;
356     let identity = this._provisionFlows[authFlow.provId].identity;
358     // tell the UI to start the authentication process
359     log("beginAuthentication: authFlow:", aCaller.id, "identity:", identity);
360     return authFlow.caller.doBeginAuthenticationCallback(identity);
361   },
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);
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;
381     // delete caller
382     delete authFlow['caller'];
383     delete this._authenticationFlows[aAuthId];
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   },
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);
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;
412     // delete caller
413     delete authFlow['caller'];
414     delete this._authenticationFlows[aAuthId];
416     let provFlow = this.getProvisionFlow(provId);
417     provFlow.didAuthentication = true;
418     Services.obs.notifyObservers(null, "identity-auth-complete", aAuthId);
420     // invoke callback with ERROR.
421     let errStr = "Authentication canceled by IDP";
422     log("ERROR: cancelAuthentication:", errStr);
423     provFlow.callback(errStr);
424   },
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   },
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);
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     }
452     new Sandbox(aURL, aCallback);
453   },
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};
462     Services.obs.notifyObservers({wrappedJSObject:propBag}, "identity-auth", aURL);
463   },
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];
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     }
483     // Clean up a related authentication flow, if there is one.
484     if (this._authenticationFlows[prov.authId]) {
485       delete this._authenticationFlows[prov.authId];
486     }
488     // Finally delete the provision flow
489     delete this._provisionFlows[aProvId];
490   }
494 this.IdentityProvider = new IdentityProviderService();