1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/identity/Identity.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,305 @@ 1.4 +/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */ 1.5 +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.8 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +"use strict"; 1.11 + 1.12 +this.EXPORTED_SYMBOLS = ["IdentityService"]; 1.13 + 1.14 +const Cu = Components.utils; 1.15 +const Ci = Components.interfaces; 1.16 +const Cc = Components.classes; 1.17 +const Cr = Components.results; 1.18 + 1.19 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.20 +Cu.import("resource://gre/modules/Services.jsm"); 1.21 +Cu.import("resource://gre/modules/identity/LogUtils.jsm"); 1.22 +Cu.import("resource://gre/modules/identity/IdentityStore.jsm"); 1.23 +Cu.import("resource://gre/modules/identity/RelyingParty.jsm"); 1.24 +Cu.import("resource://gre/modules/identity/IdentityProvider.jsm"); 1.25 + 1.26 +XPCOMUtils.defineLazyModuleGetter(this, 1.27 + "jwcrypto", 1.28 + "resource://gre/modules/identity/jwcrypto.jsm"); 1.29 + 1.30 +function log(...aMessageArgs) { 1.31 + Logger.log.apply(Logger, ["core"].concat(aMessageArgs)); 1.32 +} 1.33 +function reportError(...aMessageArgs) { 1.34 + Logger.reportError.apply(Logger, ["core"].concat(aMessageArgs)); 1.35 +} 1.36 + 1.37 +function IDService() { 1.38 + Services.obs.addObserver(this, "quit-application-granted", false); 1.39 + Services.obs.addObserver(this, "identity-auth-complete", false); 1.40 + 1.41 + this._store = IdentityStore; 1.42 + this.RP = RelyingParty; 1.43 + this.IDP = IdentityProvider; 1.44 +} 1.45 + 1.46 +IDService.prototype = { 1.47 + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]), 1.48 + 1.49 + observe: function observe(aSubject, aTopic, aData) { 1.50 + switch (aTopic) { 1.51 + case "quit-application-granted": 1.52 + Services.obs.removeObserver(this, "quit-application-granted"); 1.53 + this.shutdown(); 1.54 + break; 1.55 + case "identity-auth-complete": 1.56 + if (!aSubject || !aSubject.wrappedJSObject) 1.57 + break; 1.58 + let subject = aSubject.wrappedJSObject; 1.59 + log("Auth complete:", aSubject.wrappedJSObject); 1.60 + // We have authenticated in order to provision an identity. 1.61 + // So try again. 1.62 + this.selectIdentity(subject.rpId, subject.identity); 1.63 + break; 1.64 + } 1.65 + }, 1.66 + 1.67 + reset: function reset() { 1.68 + // Explicitly call reset() on our RP and IDP classes. 1.69 + // This is here to make testing easier. When the 1.70 + // quit-application-granted signal is emitted, reset() will be 1.71 + // called here, on RP, on IDP, and on the store. So you don't 1.72 + // need to use this :) 1.73 + this._store.reset(); 1.74 + this.RP.reset(); 1.75 + this.IDP.reset(); 1.76 + }, 1.77 + 1.78 + shutdown: function shutdown() { 1.79 + log("shutdown"); 1.80 + Services.obs.removeObserver(this, "identity-auth-complete"); 1.81 + Services.obs.removeObserver(this, "quit-application-granted"); 1.82 + }, 1.83 + 1.84 + /** 1.85 + * Parse an email into username and domain if it is valid, else return null 1.86 + */ 1.87 + parseEmail: function parseEmail(email) { 1.88 + var match = email.match(/^([^@]+)@([^@^/]+.[a-z]+)$/); 1.89 + if (match) { 1.90 + return { 1.91 + username: match[1], 1.92 + domain: match[2] 1.93 + }; 1.94 + } 1.95 + return null; 1.96 + }, 1.97 + 1.98 + /** 1.99 + * The UX wants to add a new identity 1.100 + * often followed by selectIdentity() 1.101 + * 1.102 + * @param aIdentity 1.103 + * (string) the email chosen for login 1.104 + */ 1.105 + addIdentity: function addIdentity(aIdentity) { 1.106 + if (this._store.fetchIdentity(aIdentity) === null) { 1.107 + this._store.addIdentity(aIdentity, null, null); 1.108 + } 1.109 + }, 1.110 + 1.111 + /** 1.112 + * The UX comes back and calls selectIdentity once the user has picked 1.113 + * an identity. 1.114 + * 1.115 + * @param aRPId 1.116 + * (integer) the id of the doc object obtained in .watch() and 1.117 + * passed to the UX component. 1.118 + * 1.119 + * @param aIdentity 1.120 + * (string) the email chosen for login 1.121 + */ 1.122 + selectIdentity: function selectIdentity(aRPId, aIdentity) { 1.123 + log("selectIdentity: RP id:", aRPId, "identity:", aIdentity); 1.124 + 1.125 + // Get the RP that was stored when watch() was invoked. 1.126 + let rp = this.RP._rpFlows[aRPId]; 1.127 + if (!rp) { 1.128 + reportError("selectIdentity", "Invalid RP id: ", aRPId); 1.129 + return; 1.130 + } 1.131 + 1.132 + // It's possible that we are in the process of provisioning an 1.133 + // identity. 1.134 + let provId = rp.provId; 1.135 + 1.136 + let rpLoginOptions = { 1.137 + loggedInUser: aIdentity, 1.138 + origin: rp.origin 1.139 + }; 1.140 + log("selectIdentity: provId:", provId, "origin:", rp.origin); 1.141 + 1.142 + // Once we have a cert, and once the user is authenticated with the 1.143 + // IdP, we can generate an assertion and deliver it to the doc. 1.144 + let self = this; 1.145 + this.RP._generateAssertion(rp.origin, aIdentity, function hadReadyAssertion(err, assertion) { 1.146 + if (!err && assertion) { 1.147 + self.RP._doLogin(rp, rpLoginOptions, assertion); 1.148 + return; 1.149 + 1.150 + } 1.151 + // Need to provision an identity first. Begin by discovering 1.152 + // the user's IdP. 1.153 + self._discoverIdentityProvider(aIdentity, function gotIDP(err, idpParams) { 1.154 + if (err) { 1.155 + rp.doError(err); 1.156 + return; 1.157 + } 1.158 + 1.159 + // The idpParams tell us where to go to provision and authenticate 1.160 + // the identity. 1.161 + self.IDP._provisionIdentity(aIdentity, idpParams, provId, function gotID(err, aProvId) { 1.162 + 1.163 + // Provision identity may have created a new provision flow 1.164 + // for us. To make it easier to relate provision flows with 1.165 + // RP callers, we cross index the two here. 1.166 + rp.provId = aProvId; 1.167 + self.IDP._provisionFlows[aProvId].rpId = aRPId; 1.168 + 1.169 + // At this point, we already have a cert. If the user is also 1.170 + // already authenticated with the IdP, then we can try again 1.171 + // to generate an assertion and login. 1.172 + if (err) { 1.173 + // We are not authenticated. If we have already tried to 1.174 + // authenticate and failed, then this is a "hard fail" and 1.175 + // we give up. Otherwise we try to authenticate with the 1.176 + // IdP. 1.177 + 1.178 + if (self.IDP._provisionFlows[aProvId].didAuthentication) { 1.179 + self.IDP._cleanUpProvisionFlow(aProvId); 1.180 + self.RP._cleanUpProvisionFlow(aRPId, aProvId); 1.181 + log("ERROR: selectIdentity: authentication hard fail"); 1.182 + rp.doError("Authentication fail."); 1.183 + return; 1.184 + } 1.185 + // Try to authenticate with the IdP. Note that we do 1.186 + // not clean up the provision flow here. We will continue 1.187 + // to use it. 1.188 + self.IDP._doAuthentication(aProvId, idpParams); 1.189 + return; 1.190 + } 1.191 + 1.192 + // Provisioning flows end when a certificate has been registered. 1.193 + // Thus IdentityProvider's registerCertificate() cleans up the 1.194 + // current provisioning flow. We only do this here on error. 1.195 + self.RP._generateAssertion(rp.origin, aIdentity, function gotAssertion(err, assertion) { 1.196 + if (err) { 1.197 + rp.doError(err); 1.198 + return; 1.199 + } 1.200 + self.RP._doLogin(rp, rpLoginOptions, assertion); 1.201 + self.RP._cleanUpProvisionFlow(aRPId, aProvId); 1.202 + return; 1.203 + }); 1.204 + }); 1.205 + }); 1.206 + }); 1.207 + }, 1.208 + 1.209 + // methods for chrome and add-ons 1.210 + 1.211 + /** 1.212 + * Discover the IdP for an identity 1.213 + * 1.214 + * @param aIdentity 1.215 + * (string) the email we're logging in with 1.216 + * 1.217 + * @param aCallback 1.218 + * (function) callback to invoke on completion 1.219 + * with first-positional parameter the error. 1.220 + */ 1.221 + _discoverIdentityProvider: function _discoverIdentityProvider(aIdentity, aCallback) { 1.222 + // XXX bug 767610 - validate email address call 1.223 + // When that is available, we can remove this custom parser 1.224 + var parsedEmail = this.parseEmail(aIdentity); 1.225 + if (parsedEmail === null) { 1.226 + return aCallback("Could not parse email: " + aIdentity); 1.227 + } 1.228 + log("_discoverIdentityProvider: identity:", aIdentity, "domain:", parsedEmail.domain); 1.229 + 1.230 + this._fetchWellKnownFile(parsedEmail.domain, function fetchedWellKnown(err, idpParams) { 1.231 + // idpParams includes the pk, authorization url, and 1.232 + // provisioning url. 1.233 + 1.234 + // XXX bug 769861 follow any authority delegations 1.235 + // if no well-known at any point in the delegation 1.236 + // fall back to browserid.org as IdP 1.237 + return aCallback(err, idpParams); 1.238 + }); 1.239 + }, 1.240 + 1.241 + /** 1.242 + * Fetch the well-known file from the domain. 1.243 + * 1.244 + * @param aDomain 1.245 + * 1.246 + * @param aScheme 1.247 + * (string) (optional) Protocol to use. Default is https. 1.248 + * This is necessary because we are unable to test 1.249 + * https. 1.250 + * 1.251 + * @param aCallback 1.252 + * 1.253 + */ 1.254 + _fetchWellKnownFile: function _fetchWellKnownFile(aDomain, aCallback, aScheme='https') { 1.255 + // XXX bug 769854 make tests https and remove aScheme option 1.256 + let url = aScheme + '://' + aDomain + "/.well-known/browserid"; 1.257 + log("_fetchWellKnownFile:", url); 1.258 + 1.259 + // this appears to be a more successful way to get at xmlhttprequest (which supposedly will close with a window 1.260 + let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] 1.261 + .createInstance(Ci.nsIXMLHttpRequest); 1.262 + 1.263 + // XXX bug 769865 gracefully handle being off-line 1.264 + // XXX bug 769866 decide on how to handle redirects 1.265 + req.open("GET", url, true); 1.266 + req.responseType = "json"; 1.267 + req.mozBackgroundRequest = true; 1.268 + req.onload = function _fetchWellKnownFile_onload() { 1.269 + if (req.status < 200 || req.status >= 400) { 1.270 + log("_fetchWellKnownFile", url, ": server returned status:", req.status); 1.271 + return aCallback("Error"); 1.272 + } 1.273 + try { 1.274 + let idpParams = req.response; 1.275 + 1.276 + // Verify that the IdP returned a valid configuration 1.277 + if (! (idpParams.provisioning && 1.278 + idpParams.authentication && 1.279 + idpParams['public-key'])) { 1.280 + let errStr= "Invalid well-known file from: " + aDomain; 1.281 + log("_fetchWellKnownFile:", errStr); 1.282 + return aCallback(errStr); 1.283 + } 1.284 + 1.285 + let callbackObj = { 1.286 + domain: aDomain, 1.287 + idpParams: idpParams, 1.288 + }; 1.289 + log("_fetchWellKnownFile result: ", callbackObj); 1.290 + // Yay. Valid IdP configuration for the domain. 1.291 + return aCallback(null, callbackObj); 1.292 + 1.293 + } catch (err) { 1.294 + reportError("_fetchWellKnownFile", "Bad configuration from", aDomain, err); 1.295 + return aCallback(err.toString()); 1.296 + } 1.297 + }; 1.298 + req.onerror = function _fetchWellKnownFile_onerror() { 1.299 + log("_fetchWellKnownFile", "ERROR:", req.status, req.statusText); 1.300 + log("ERROR: _fetchWellKnownFile:", err); 1.301 + return aCallback("Error"); 1.302 + }; 1.303 + req.send(null); 1.304 + }, 1.305 + 1.306 +}; 1.307 + 1.308 +this.IdentityService = new IDService();