toolkit/identity/Identity.jsm

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:e883db8bbc95
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 this.EXPORTED_SYMBOLS = ["IdentityService"];
10
11 const Cu = Components.utils;
12 const Ci = Components.interfaces;
13 const Cc = Components.classes;
14 const Cr = Components.results;
15
16 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
17 Cu.import("resource://gre/modules/Services.jsm");
18 Cu.import("resource://gre/modules/identity/LogUtils.jsm");
19 Cu.import("resource://gre/modules/identity/IdentityStore.jsm");
20 Cu.import("resource://gre/modules/identity/RelyingParty.jsm");
21 Cu.import("resource://gre/modules/identity/IdentityProvider.jsm");
22
23 XPCOMUtils.defineLazyModuleGetter(this,
24 "jwcrypto",
25 "resource://gre/modules/identity/jwcrypto.jsm");
26
27 function log(...aMessageArgs) {
28 Logger.log.apply(Logger, ["core"].concat(aMessageArgs));
29 }
30 function reportError(...aMessageArgs) {
31 Logger.reportError.apply(Logger, ["core"].concat(aMessageArgs));
32 }
33
34 function IDService() {
35 Services.obs.addObserver(this, "quit-application-granted", false);
36 Services.obs.addObserver(this, "identity-auth-complete", false);
37
38 this._store = IdentityStore;
39 this.RP = RelyingParty;
40 this.IDP = IdentityProvider;
41 }
42
43 IDService.prototype = {
44 QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
45
46 observe: function observe(aSubject, aTopic, aData) {
47 switch (aTopic) {
48 case "quit-application-granted":
49 Services.obs.removeObserver(this, "quit-application-granted");
50 this.shutdown();
51 break;
52 case "identity-auth-complete":
53 if (!aSubject || !aSubject.wrappedJSObject)
54 break;
55 let subject = aSubject.wrappedJSObject;
56 log("Auth complete:", aSubject.wrappedJSObject);
57 // We have authenticated in order to provision an identity.
58 // So try again.
59 this.selectIdentity(subject.rpId, subject.identity);
60 break;
61 }
62 },
63
64 reset: function reset() {
65 // Explicitly call reset() on our RP and IDP classes.
66 // This is here to make testing easier. When the
67 // quit-application-granted signal is emitted, reset() will be
68 // called here, on RP, on IDP, and on the store. So you don't
69 // need to use this :)
70 this._store.reset();
71 this.RP.reset();
72 this.IDP.reset();
73 },
74
75 shutdown: function shutdown() {
76 log("shutdown");
77 Services.obs.removeObserver(this, "identity-auth-complete");
78 Services.obs.removeObserver(this, "quit-application-granted");
79 },
80
81 /**
82 * Parse an email into username and domain if it is valid, else return null
83 */
84 parseEmail: function parseEmail(email) {
85 var match = email.match(/^([^@]+)@([^@^/]+.[a-z]+)$/);
86 if (match) {
87 return {
88 username: match[1],
89 domain: match[2]
90 };
91 }
92 return null;
93 },
94
95 /**
96 * The UX wants to add a new identity
97 * often followed by selectIdentity()
98 *
99 * @param aIdentity
100 * (string) the email chosen for login
101 */
102 addIdentity: function addIdentity(aIdentity) {
103 if (this._store.fetchIdentity(aIdentity) === null) {
104 this._store.addIdentity(aIdentity, null, null);
105 }
106 },
107
108 /**
109 * The UX comes back and calls selectIdentity once the user has picked
110 * an identity.
111 *
112 * @param aRPId
113 * (integer) the id of the doc object obtained in .watch() and
114 * passed to the UX component.
115 *
116 * @param aIdentity
117 * (string) the email chosen for login
118 */
119 selectIdentity: function selectIdentity(aRPId, aIdentity) {
120 log("selectIdentity: RP id:", aRPId, "identity:", aIdentity);
121
122 // Get the RP that was stored when watch() was invoked.
123 let rp = this.RP._rpFlows[aRPId];
124 if (!rp) {
125 reportError("selectIdentity", "Invalid RP id: ", aRPId);
126 return;
127 }
128
129 // It's possible that we are in the process of provisioning an
130 // identity.
131 let provId = rp.provId;
132
133 let rpLoginOptions = {
134 loggedInUser: aIdentity,
135 origin: rp.origin
136 };
137 log("selectIdentity: provId:", provId, "origin:", rp.origin);
138
139 // Once we have a cert, and once the user is authenticated with the
140 // IdP, we can generate an assertion and deliver it to the doc.
141 let self = this;
142 this.RP._generateAssertion(rp.origin, aIdentity, function hadReadyAssertion(err, assertion) {
143 if (!err && assertion) {
144 self.RP._doLogin(rp, rpLoginOptions, assertion);
145 return;
146
147 }
148 // Need to provision an identity first. Begin by discovering
149 // the user's IdP.
150 self._discoverIdentityProvider(aIdentity, function gotIDP(err, idpParams) {
151 if (err) {
152 rp.doError(err);
153 return;
154 }
155
156 // The idpParams tell us where to go to provision and authenticate
157 // the identity.
158 self.IDP._provisionIdentity(aIdentity, idpParams, provId, function gotID(err, aProvId) {
159
160 // Provision identity may have created a new provision flow
161 // for us. To make it easier to relate provision flows with
162 // RP callers, we cross index the two here.
163 rp.provId = aProvId;
164 self.IDP._provisionFlows[aProvId].rpId = aRPId;
165
166 // At this point, we already have a cert. If the user is also
167 // already authenticated with the IdP, then we can try again
168 // to generate an assertion and login.
169 if (err) {
170 // We are not authenticated. If we have already tried to
171 // authenticate and failed, then this is a "hard fail" and
172 // we give up. Otherwise we try to authenticate with the
173 // IdP.
174
175 if (self.IDP._provisionFlows[aProvId].didAuthentication) {
176 self.IDP._cleanUpProvisionFlow(aProvId);
177 self.RP._cleanUpProvisionFlow(aRPId, aProvId);
178 log("ERROR: selectIdentity: authentication hard fail");
179 rp.doError("Authentication fail.");
180 return;
181 }
182 // Try to authenticate with the IdP. Note that we do
183 // not clean up the provision flow here. We will continue
184 // to use it.
185 self.IDP._doAuthentication(aProvId, idpParams);
186 return;
187 }
188
189 // Provisioning flows end when a certificate has been registered.
190 // Thus IdentityProvider's registerCertificate() cleans up the
191 // current provisioning flow. We only do this here on error.
192 self.RP._generateAssertion(rp.origin, aIdentity, function gotAssertion(err, assertion) {
193 if (err) {
194 rp.doError(err);
195 return;
196 }
197 self.RP._doLogin(rp, rpLoginOptions, assertion);
198 self.RP._cleanUpProvisionFlow(aRPId, aProvId);
199 return;
200 });
201 });
202 });
203 });
204 },
205
206 // methods for chrome and add-ons
207
208 /**
209 * Discover the IdP for an identity
210 *
211 * @param aIdentity
212 * (string) the email we're logging in with
213 *
214 * @param aCallback
215 * (function) callback to invoke on completion
216 * with first-positional parameter the error.
217 */
218 _discoverIdentityProvider: function _discoverIdentityProvider(aIdentity, aCallback) {
219 // XXX bug 767610 - validate email address call
220 // When that is available, we can remove this custom parser
221 var parsedEmail = this.parseEmail(aIdentity);
222 if (parsedEmail === null) {
223 return aCallback("Could not parse email: " + aIdentity);
224 }
225 log("_discoverIdentityProvider: identity:", aIdentity, "domain:", parsedEmail.domain);
226
227 this._fetchWellKnownFile(parsedEmail.domain, function fetchedWellKnown(err, idpParams) {
228 // idpParams includes the pk, authorization url, and
229 // provisioning url.
230
231 // XXX bug 769861 follow any authority delegations
232 // if no well-known at any point in the delegation
233 // fall back to browserid.org as IdP
234 return aCallback(err, idpParams);
235 });
236 },
237
238 /**
239 * Fetch the well-known file from the domain.
240 *
241 * @param aDomain
242 *
243 * @param aScheme
244 * (string) (optional) Protocol to use. Default is https.
245 * This is necessary because we are unable to test
246 * https.
247 *
248 * @param aCallback
249 *
250 */
251 _fetchWellKnownFile: function _fetchWellKnownFile(aDomain, aCallback, aScheme='https') {
252 // XXX bug 769854 make tests https and remove aScheme option
253 let url = aScheme + '://' + aDomain + "/.well-known/browserid";
254 log("_fetchWellKnownFile:", url);
255
256 // this appears to be a more successful way to get at xmlhttprequest (which supposedly will close with a window
257 let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
258 .createInstance(Ci.nsIXMLHttpRequest);
259
260 // XXX bug 769865 gracefully handle being off-line
261 // XXX bug 769866 decide on how to handle redirects
262 req.open("GET", url, true);
263 req.responseType = "json";
264 req.mozBackgroundRequest = true;
265 req.onload = function _fetchWellKnownFile_onload() {
266 if (req.status < 200 || req.status >= 400) {
267 log("_fetchWellKnownFile", url, ": server returned status:", req.status);
268 return aCallback("Error");
269 }
270 try {
271 let idpParams = req.response;
272
273 // Verify that the IdP returned a valid configuration
274 if (! (idpParams.provisioning &&
275 idpParams.authentication &&
276 idpParams['public-key'])) {
277 let errStr= "Invalid well-known file from: " + aDomain;
278 log("_fetchWellKnownFile:", errStr);
279 return aCallback(errStr);
280 }
281
282 let callbackObj = {
283 domain: aDomain,
284 idpParams: idpParams,
285 };
286 log("_fetchWellKnownFile result: ", callbackObj);
287 // Yay. Valid IdP configuration for the domain.
288 return aCallback(null, callbackObj);
289
290 } catch (err) {
291 reportError("_fetchWellKnownFile", "Bad configuration from", aDomain, err);
292 return aCallback(err.toString());
293 }
294 };
295 req.onerror = function _fetchWellKnownFile_onerror() {
296 log("_fetchWellKnownFile", "ERROR:", req.status, req.statusText);
297 log("ERROR: _fetchWellKnownFile:", err);
298 return aCallback("Error");
299 };
300 req.send(null);
301 },
302
303 };
304
305 this.IdentityService = new IDService();

mercurial