toolkit/identity/Identity.jsm

changeset 0
6474c204b198
     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();

mercurial