1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/identity/IdentityProvider.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,494 @@ 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 +const Cu = Components.utils; 1.13 +const Ci = Components.interfaces; 1.14 +const Cc = Components.classes; 1.15 +const Cr = Components.results; 1.16 + 1.17 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.18 +Cu.import("resource://gre/modules/Services.jsm"); 1.19 +Cu.import("resource://gre/modules/identity/LogUtils.jsm"); 1.20 +Cu.import("resource://gre/modules/identity/Sandbox.jsm"); 1.21 + 1.22 +this.EXPORTED_SYMBOLS = ["IdentityProvider"]; 1.23 +const FALLBACK_PROVIDER = "browserid.org"; 1.24 + 1.25 +XPCOMUtils.defineLazyModuleGetter(this, 1.26 + "jwcrypto", 1.27 + "resource://gre/modules/identity/jwcrypto.jsm"); 1.28 + 1.29 +function log(...aMessageArgs) { 1.30 + Logger.log.apply(Logger, ["IDP"].concat(aMessageArgs)); 1.31 +} 1.32 +function reportError(...aMessageArgs) { 1.33 + Logger.reportError.apply(Logger, ["IDP"].concat(aMessageArgs)); 1.34 +} 1.35 + 1.36 + 1.37 +function IdentityProviderService() { 1.38 + XPCOMUtils.defineLazyModuleGetter(this, 1.39 + "_store", 1.40 + "resource://gre/modules/identity/IdentityStore.jsm", 1.41 + "IdentityStore"); 1.42 + 1.43 + this.reset(); 1.44 +} 1.45 + 1.46 +IdentityProviderService.prototype = { 1.47 + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]), 1.48 + _sandboxConfigured: false, 1.49 + 1.50 + observe: function observe(aSubject, aTopic, aData) { 1.51 + switch (aTopic) { 1.52 + case "quit-application-granted": 1.53 + Services.obs.removeObserver(this, "quit-application-granted"); 1.54 + this.shutdown(); 1.55 + break; 1.56 + } 1.57 + }, 1.58 + 1.59 + reset: function IDP_reset() { 1.60 + // Clear the provisioning flows. Provision flows contain an 1.61 + // identity, idpParams (how to reach the IdP to provision and 1.62 + // authenticate), a callback (a completion callback for when things 1.63 + // are done), and a provisioningFrame (which is the provisioning 1.64 + // sandbox). Additionally, two callbacks will be attached: 1.65 + // beginProvisioningCallback and genKeyPairCallback. 1.66 + this._provisionFlows = {}; 1.67 + 1.68 + // Clear the authentication flows. Authentication flows attach 1.69 + // to provision flows. In the process of provisioning an id, it 1.70 + // may be necessary to authenticate with an IdP. The authentication 1.71 + // flow maintains the state of that authentication process. 1.72 + this._authenticationFlows = {}; 1.73 + }, 1.74 + 1.75 + getProvisionFlow: function getProvisionFlow(aProvId, aErrBack) { 1.76 + let provFlow = this._provisionFlows[aProvId]; 1.77 + if (provFlow) { 1.78 + return provFlow; 1.79 + } 1.80 + 1.81 + let err = "No provisioning flow found with id " + aProvId; 1.82 + log("ERROR:", err); 1.83 + if (typeof aErrBack === 'function') { 1.84 + aErrBack(err); 1.85 + } 1.86 + }, 1.87 + 1.88 + shutdown: function RP_shutdown() { 1.89 + this.reset(); 1.90 + 1.91 + if (this._sandboxConfigured) { 1.92 + // Tear down message manager listening on the hidden window 1.93 + Cu.import("resource://gre/modules/DOMIdentity.jsm"); 1.94 + DOMIdentity._configureMessages(Services.appShell.hiddenDOMWindow, false); 1.95 + this._sandboxConfigured = false; 1.96 + } 1.97 + 1.98 + Services.obs.removeObserver(this, "quit-application-granted"); 1.99 + }, 1.100 + 1.101 + get securityLevel() { 1.102 + return 1; 1.103 + }, 1.104 + 1.105 + get certDuration() { 1.106 + switch(this.securityLevel) { 1.107 + default: 1.108 + return 3600; 1.109 + } 1.110 + }, 1.111 + 1.112 + /** 1.113 + * Provision an Identity 1.114 + * 1.115 + * @param aIdentity 1.116 + * (string) the email we're logging in with 1.117 + * 1.118 + * @param aIDPParams 1.119 + * (object) parameters of the IdP 1.120 + * 1.121 + * @param aCallback 1.122 + * (function) callback to invoke on completion 1.123 + * with first-positional parameter the error. 1.124 + */ 1.125 + _provisionIdentity: function _provisionIdentity(aIdentity, aIDPParams, aProvId, aCallback) { 1.126 + let provPath = aIDPParams.idpParams.provisioning; 1.127 + let url = Services.io.newURI("https://" + aIDPParams.domain, null, null).resolve(provPath); 1.128 + log("_provisionIdentity: identity:", aIdentity, "url:", url); 1.129 + 1.130 + // If aProvId is not null, then we already have a flow 1.131 + // with a sandbox. Otherwise, get a sandbox and create a 1.132 + // new provision flow. 1.133 + 1.134 + if (aProvId) { 1.135 + // Re-use an existing sandbox 1.136 + log("_provisionIdentity: re-using sandbox in provisioning flow with id:", aProvId); 1.137 + this._provisionFlows[aProvId].provisioningSandbox.reload(); 1.138 + 1.139 + } else { 1.140 + this._createProvisioningSandbox(url, function createdSandbox(aSandbox) { 1.141 + // create a provisioning flow, using the sandbox id, and 1.142 + // stash callback associated with this provisioning workflow. 1.143 + 1.144 + let provId = aSandbox.id; 1.145 + this._provisionFlows[provId] = { 1.146 + identity: aIdentity, 1.147 + idpParams: aIDPParams, 1.148 + securityLevel: this.securityLevel, 1.149 + provisioningSandbox: aSandbox, 1.150 + callback: function doCallback(aErr) { 1.151 + aCallback(aErr, provId); 1.152 + }, 1.153 + }; 1.154 + 1.155 + log("_provisionIdentity: Created sandbox and provisioning flow with id:", provId); 1.156 + // XXX bug 769862 - provisioning flow should timeout after N seconds 1.157 + 1.158 + }.bind(this)); 1.159 + } 1.160 + }, 1.161 + 1.162 + // DOM Methods 1.163 + /** 1.164 + * the provisioning iframe sandbox has called navigator.id.beginProvisioning() 1.165 + * 1.166 + * @param aCaller 1.167 + * (object) the iframe sandbox caller with all callbacks and 1.168 + * other information. Callbacks include: 1.169 + * - doBeginProvisioningCallback(id, duration_s) 1.170 + * - doGenKeyPairCallback(pk) 1.171 + */ 1.172 + beginProvisioning: function beginProvisioning(aCaller) { 1.173 + log("beginProvisioning:", aCaller.id); 1.174 + 1.175 + // Expect a flow for this caller already to be underway. 1.176 + let provFlow = this.getProvisionFlow(aCaller.id, aCaller.doError); 1.177 + 1.178 + // keep the caller object around 1.179 + provFlow.caller = aCaller; 1.180 + 1.181 + let identity = provFlow.identity; 1.182 + let frame = provFlow.provisioningFrame; 1.183 + 1.184 + // Determine recommended length of cert. 1.185 + let duration = this.certDuration; 1.186 + 1.187 + // Make a record that we have begun provisioning. This is required 1.188 + // for genKeyPair. 1.189 + provFlow.didBeginProvisioning = true; 1.190 + 1.191 + // Let the sandbox know to invoke the callback to beginProvisioning with 1.192 + // the identity and cert length. 1.193 + return aCaller.doBeginProvisioningCallback(identity, duration); 1.194 + }, 1.195 + 1.196 + /** 1.197 + * the provisioning iframe sandbox has called 1.198 + * navigator.id.raiseProvisioningFailure() 1.199 + * 1.200 + * @param aProvId 1.201 + * (int) the identifier of the provisioning flow tied to that sandbox 1.202 + * @param aReason 1.203 + */ 1.204 + raiseProvisioningFailure: function raiseProvisioningFailure(aProvId, aReason) { 1.205 + reportError("Provisioning failure", aReason); 1.206 + 1.207 + // look up the provisioning caller and its callback 1.208 + let provFlow = this.getProvisionFlow(aProvId); 1.209 + 1.210 + // Sandbox is deleted in _cleanUpProvisionFlow in case we re-use it. 1.211 + 1.212 + // This may be either a "soft" or "hard" fail. If it's a 1.213 + // soft fail, we'll flow through setAuthenticationFlow, where 1.214 + // the provision flow data will be copied into a new auth 1.215 + // flow. If it's a hard fail, then the callback will be 1.216 + // responsible for cleaning up the now defunct provision flow. 1.217 + 1.218 + // invoke the callback with an error. 1.219 + provFlow.callback(aReason); 1.220 + }, 1.221 + 1.222 + /** 1.223 + * When navigator.id.genKeyPair is called from provisioning iframe sandbox. 1.224 + * Generates a keypair for the current user being provisioned. 1.225 + * 1.226 + * @param aProvId 1.227 + * (int) the identifier of the provisioning caller tied to that sandbox 1.228 + * 1.229 + * It is an error to call genKeypair without receiving the callback for 1.230 + * the beginProvisioning() call first. 1.231 + */ 1.232 + genKeyPair: function genKeyPair(aProvId) { 1.233 + // Look up the provisioning caller and make sure it's valid. 1.234 + let provFlow = this.getProvisionFlow(aProvId); 1.235 + 1.236 + if (!provFlow.didBeginProvisioning) { 1.237 + let errStr = "ERROR: genKeyPair called before beginProvisioning"; 1.238 + log(errStr); 1.239 + provFlow.callback(errStr); 1.240 + return; 1.241 + } 1.242 + 1.243 + // Ok generate a keypair 1.244 + jwcrypto.generateKeyPair(jwcrypto.ALGORITHMS.DS160, function gkpCb(err, kp) { 1.245 + log("in gkp callback"); 1.246 + if (err) { 1.247 + log("ERROR: genKeyPair:", err); 1.248 + provFlow.callback(err); 1.249 + return; 1.250 + } 1.251 + 1.252 + provFlow.kp = kp; 1.253 + 1.254 + // Serialize the publicKey of the keypair and send it back to the 1.255 + // sandbox. 1.256 + log("genKeyPair: generated keypair for provisioning flow with id:", aProvId); 1.257 + provFlow.caller.doGenKeyPairCallback(provFlow.kp.serializedPublicKey); 1.258 + }.bind(this)); 1.259 + }, 1.260 + 1.261 + /** 1.262 + * When navigator.id.registerCertificate is called from provisioning iframe 1.263 + * sandbox. 1.264 + * 1.265 + * Sets the certificate for the user for which a certificate was requested 1.266 + * via a preceding call to beginProvisioning (and genKeypair). 1.267 + * 1.268 + * @param aProvId 1.269 + * (integer) the identifier of the provisioning caller tied to that 1.270 + * sandbox 1.271 + * 1.272 + * @param aCert 1.273 + * (String) A JWT representing the signed certificate for the user 1.274 + * being provisioned, provided by the IdP. 1.275 + */ 1.276 + registerCertificate: function registerCertificate(aProvId, aCert) { 1.277 + log("registerCertificate:", aProvId, aCert); 1.278 + 1.279 + // look up provisioning caller, make sure it's valid. 1.280 + let provFlow = this.getProvisionFlow(aProvId); 1.281 + 1.282 + if (!provFlow.caller) { 1.283 + reportError("registerCertificate", "No provision flow or caller"); 1.284 + return; 1.285 + } 1.286 + if (!provFlow.kp) { 1.287 + let errStr = "Cannot register a certificate without a keypair"; 1.288 + reportError("registerCertificate", errStr); 1.289 + provFlow.callback(errStr); 1.290 + return; 1.291 + } 1.292 + 1.293 + // store the keypair and certificate just provided in IDStore. 1.294 + this._store.addIdentity(provFlow.identity, provFlow.kp, aCert); 1.295 + 1.296 + // Great success! 1.297 + provFlow.callback(null); 1.298 + 1.299 + // Clean up the flow. 1.300 + this._cleanUpProvisionFlow(aProvId); 1.301 + }, 1.302 + 1.303 + /** 1.304 + * Begin the authentication process with an IdP 1.305 + * 1.306 + * @param aProvId 1.307 + * (int) the identifier of the provisioning flow which failed 1.308 + * 1.309 + * @param aCallback 1.310 + * (function) to invoke upon completion, with 1.311 + * first-positional-param error. 1.312 + */ 1.313 + _doAuthentication: function _doAuthentication(aProvId, aIDPParams) { 1.314 + log("_doAuthentication: provId:", aProvId, "idpParams:", aIDPParams); 1.315 + // create an authentication caller and its identifier AuthId 1.316 + // stash aIdentity, idpparams, and callback in it. 1.317 + 1.318 + // extract authentication URL from idpParams 1.319 + let authPath = aIDPParams.idpParams.authentication; 1.320 + let authURI = Services.io.newURI("https://" + aIDPParams.domain, null, null).resolve(authPath); 1.321 + 1.322 + // beginAuthenticationFlow causes the "identity-auth" topic to be 1.323 + // observed. Since it's sending a notification to the DOM, there's 1.324 + // no callback. We wait for the DOM to trigger the next phase of 1.325 + // provisioning. 1.326 + this._beginAuthenticationFlow(aProvId, authURI); 1.327 + 1.328 + // either we bind the AuthID to the sandbox ourselves, or UX does that, 1.329 + // in which case we need to tell UX the AuthId. 1.330 + // Currently, the UX creates the UI and gets the AuthId from the window 1.331 + // and sets is with setAuthenticationFlow 1.332 + }, 1.333 + 1.334 + /** 1.335 + * The authentication frame has called navigator.id.beginAuthentication 1.336 + * 1.337 + * IMPORTANT: the aCaller is *always* non-null, even if this is called from 1.338 + * a regular content page. We have to make sure, on every DOM call, that 1.339 + * aCaller is an expected authentication-flow identifier. If not, we throw 1.340 + * an error or something. 1.341 + * 1.342 + * @param aCaller 1.343 + * (object) the authentication caller 1.344 + * 1.345 + */ 1.346 + beginAuthentication: function beginAuthentication(aCaller) { 1.347 + log("beginAuthentication: caller id:", aCaller.id); 1.348 + 1.349 + // Begin the authentication flow after having concluded a provisioning 1.350 + // flow. The aCaller that the DOM gives us will have the same ID as 1.351 + // the provisioning flow we just concluded. (see setAuthenticationFlow) 1.352 + let authFlow = this._authenticationFlows[aCaller.id]; 1.353 + if (!authFlow) { 1.354 + return aCaller.doError("beginAuthentication: no flow for caller id", aCaller.id); 1.355 + } 1.356 + 1.357 + authFlow.caller = aCaller; 1.358 + 1.359 + let identity = this._provisionFlows[authFlow.provId].identity; 1.360 + 1.361 + // tell the UI to start the authentication process 1.362 + log("beginAuthentication: authFlow:", aCaller.id, "identity:", identity); 1.363 + return authFlow.caller.doBeginAuthenticationCallback(identity); 1.364 + }, 1.365 + 1.366 + /** 1.367 + * The auth frame has called navigator.id.completeAuthentication 1.368 + * 1.369 + * @param aAuthId 1.370 + * (int) the identifier of the authentication caller tied to that sandbox 1.371 + * 1.372 + */ 1.373 + completeAuthentication: function completeAuthentication(aAuthId) { 1.374 + log("completeAuthentication:", aAuthId); 1.375 + 1.376 + // look up the AuthId caller, and get its callback. 1.377 + let authFlow = this._authenticationFlows[aAuthId]; 1.378 + if (!authFlow) { 1.379 + reportError("completeAuthentication", "No auth flow with id", aAuthId); 1.380 + return; 1.381 + } 1.382 + let provId = authFlow.provId; 1.383 + 1.384 + // delete caller 1.385 + delete authFlow['caller']; 1.386 + delete this._authenticationFlows[aAuthId]; 1.387 + 1.388 + let provFlow = this.getProvisionFlow(provId); 1.389 + provFlow.didAuthentication = true; 1.390 + let subject = { 1.391 + rpId: provFlow.rpId, 1.392 + identity: provFlow.identity, 1.393 + }; 1.394 + Services.obs.notifyObservers({ wrappedJSObject: subject }, "identity-auth-complete", aAuthId); 1.395 + }, 1.396 + 1.397 + /** 1.398 + * The auth frame has called navigator.id.cancelAuthentication 1.399 + * 1.400 + * @param aAuthId 1.401 + * (int) the identifier of the authentication caller 1.402 + * 1.403 + */ 1.404 + cancelAuthentication: function cancelAuthentication(aAuthId) { 1.405 + log("cancelAuthentication:", aAuthId); 1.406 + 1.407 + // look up the AuthId caller, and get its callback. 1.408 + let authFlow = this._authenticationFlows[aAuthId]; 1.409 + if (!authFlow) { 1.410 + reportError("cancelAuthentication", "No auth flow with id:", aAuthId); 1.411 + return; 1.412 + } 1.413 + let provId = authFlow.provId; 1.414 + 1.415 + // delete caller 1.416 + delete authFlow['caller']; 1.417 + delete this._authenticationFlows[aAuthId]; 1.418 + 1.419 + let provFlow = this.getProvisionFlow(provId); 1.420 + provFlow.didAuthentication = true; 1.421 + Services.obs.notifyObservers(null, "identity-auth-complete", aAuthId); 1.422 + 1.423 + // invoke callback with ERROR. 1.424 + let errStr = "Authentication canceled by IDP"; 1.425 + log("ERROR: cancelAuthentication:", errStr); 1.426 + provFlow.callback(errStr); 1.427 + }, 1.428 + 1.429 + /** 1.430 + * Called by the UI to set the ID and caller for the authentication flow after it gets its ID 1.431 + */ 1.432 + setAuthenticationFlow: function(aAuthId, aProvId) { 1.433 + // this is the transition point between the two flows, 1.434 + // provision and authenticate. We tell the auth flow which 1.435 + // provisioning flow it is started from. 1.436 + log("setAuthenticationFlow: authId:", aAuthId, "provId:", aProvId); 1.437 + this._authenticationFlows[aAuthId] = { provId: aProvId }; 1.438 + this._provisionFlows[aProvId].authId = aAuthId; 1.439 + }, 1.440 + 1.441 + /** 1.442 + * Load the provisioning URL in a hidden frame to start the provisioning 1.443 + * process. 1.444 + */ 1.445 + _createProvisioningSandbox: function _createProvisioningSandbox(aURL, aCallback) { 1.446 + log("_createProvisioningSandbox:", aURL); 1.447 + 1.448 + if (!this._sandboxConfigured) { 1.449 + // Configure message manager listening on the hidden window 1.450 + Cu.import("resource://gre/modules/DOMIdentity.jsm"); 1.451 + DOMIdentity._configureMessages(Services.appShell.hiddenDOMWindow, true); 1.452 + this._sandboxConfigured = true; 1.453 + } 1.454 + 1.455 + new Sandbox(aURL, aCallback); 1.456 + }, 1.457 + 1.458 + /** 1.459 + * Load the authentication UI to start the authentication process. 1.460 + */ 1.461 + _beginAuthenticationFlow: function _beginAuthenticationFlow(aProvId, aURL) { 1.462 + log("_beginAuthenticationFlow:", aProvId, aURL); 1.463 + let propBag = {provId: aProvId}; 1.464 + 1.465 + Services.obs.notifyObservers({wrappedJSObject:propBag}, "identity-auth", aURL); 1.466 + }, 1.467 + 1.468 + /** 1.469 + * Clean up a provision flow and the authentication flow and sandbox 1.470 + * that may be attached to it. 1.471 + */ 1.472 + _cleanUpProvisionFlow: function _cleanUpProvisionFlow(aProvId) { 1.473 + log('_cleanUpProvisionFlow:', aProvId); 1.474 + let prov = this._provisionFlows[aProvId]; 1.475 + 1.476 + // Clean up the sandbox, if there is one. 1.477 + if (prov.provisioningSandbox) { 1.478 + let sandbox = this._provisionFlows[aProvId]['provisioningSandbox']; 1.479 + if (sandbox.free) { 1.480 + log('_cleanUpProvisionFlow: freeing sandbox'); 1.481 + sandbox.free(); 1.482 + } 1.483 + delete this._provisionFlows[aProvId]['provisioningSandbox']; 1.484 + } 1.485 + 1.486 + // Clean up a related authentication flow, if there is one. 1.487 + if (this._authenticationFlows[prov.authId]) { 1.488 + delete this._authenticationFlows[prov.authId]; 1.489 + } 1.490 + 1.491 + // Finally delete the provision flow 1.492 + delete this._provisionFlows[aProvId]; 1.493 + } 1.494 + 1.495 +}; 1.496 + 1.497 +this.IdentityProvider = new IdentityProviderService();