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