Bug 794680 - Connect gecko to b2g identity ui. r=benadida
☠☠ backed out by 7a8f277d19df ☠ ☠
authorJed Parsons <jparsons@mozilla.com>
Tue, 16 Oct 2012 21:34:02 -0400
changeset 110624 4f8830d30f623af5b0f3d10ab6ca7d9b44204759
parent 110623 6c1d11cdda0addcef124c9cdcea2ded667d249a1
child 110625 70e45c7aa3ba26db59b91e24d204788b1281b5e4
push id93
push usernmatsakis@mozilla.com
push dateWed, 31 Oct 2012 21:26:57 +0000
reviewersbenadida
bugs794680
milestone19.0a1
Bug 794680 - Connect gecko to b2g identity ui. r=benadida
b2g/components/Makefile.in
b2g/components/SignInToWebsite.jsm
b2g/components/test/unit/head_identity.js
b2g/components/test/unit/test_signintowebsite.js
b2g/components/test/unit/xpcshell.ini
dom/identity/DOMIdentity.jsm
dom/identity/nsDOMIdentity.js
toolkit/identity/Makefile.in
toolkit/identity/MinimalIdentity.jsm
toolkit/identity/tests/Makefile.in
toolkit/identity/tests/unit/test_identity.js
toolkit/identity/tests/unit/test_minimalidentity.js
toolkit/identity/tests/unit/xpcshell.ini
--- a/b2g/components/Makefile.in
+++ b/b2g/components/Makefile.in
@@ -30,16 +30,17 @@ EXTRA_PP_COMPONENTS = \
         SmsProtocolHandler.js \
         TelProtocolHandler.js \
         YoutubeProtocolHandler.js \
         RecoveryService.js \
         $(NULL)
 
 EXTRA_JS_MODULES = \
 	TelURIParser.jsm \
+	SignInToWebsite.jsm \
 	$(NULL)
 
 TEST_DIRS = \
 	test \
 	$(NULL)
 
 ifdef MOZ_UPDATER
 EXTRA_PP_COMPONENTS += UpdatePrompt.js
new file mode 100644
--- /dev/null
+++ b/b2g/components/SignInToWebsite.jsm
@@ -0,0 +1,168 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//
+// the B2G version of Identity UX
+//
+
+"use strict";
+
+const EXPORTED_SYMBOLS = ["SignInToWebsiteController"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "IdentityService",
+                                  "resource://gre/modules/identity/MinimalIdentity.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Logger",
+                                  "resource://gre/modules/identity/LogUtils.jsm");
+
+// JS shim that contains the callback functions that
+// live within the identity UI provisioning frame.
+// (NOT THE SAME THING AS IdP)
+const kIdentityShimFile = "chrome://browser/content/identity.js";
+
+// Type of MozChromEvents to handle payment dialogs.
+const kOpenIdentityDialog = "open-id-dialog";
+const kCloseIdentityDialog = "close-id-dialog";
+
+
+XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
+                                   "@mozilla.org/uuid-generator;1",
+                                   "nsIUUIDGenerator");
+
+function log(...aMessageArgs) {
+  Logger.log.apply(Logger, ["SignInToWebsiteController"].concat(aMessageArgs));
+}
+
+let SignInToWebsiteController = {
+
+  init: function SignInToWebsiteController_init() {
+    log("SignInToWebsiteController: init");
+    Services.obs.addObserver(this, "identity-controller-watch", false);
+    Services.obs.addObserver(this, "identity-controller-request", false);
+    Services.obs.addObserver(this, "identity-controller-logout", false);
+  },
+
+  uninit: function SignInToWebsiteController_uninit() {
+    Services.obs.removeObserver(this, "identity-controller-watch");
+    Services.obs.removeObserver(this, "identity-controller-request");
+    Services.obs.removeObserver(this, "identity-controller-logout");
+  },
+
+  observe: function SignInToWebsiteController_observe(aSubject, aTopic, aData) {
+    log("observe: received", aTopic, "with", aData, "for", aSubject);
+    let options = null;
+    if (aSubject) {
+      options = aSubject.wrappedJSObject;
+    }
+    switch(aTopic) {
+      case "identity-controller-watch":
+        this.doWatch(options);
+        break;
+      case "identity-controller-request":
+        this.doRequest(options);
+        break;
+      case "identity-controller-logout":
+        this.doLogout(options);
+        break;
+      default:
+        Logger.reportError("SignInToWebsiteController", "Unknown observer notification:", aTopic);
+        break;
+    }
+  },
+
+  getRandomId: function getRandomId() {
+    return uuidgen.generateUUID().toString();
+  },
+
+  doWatch: function SignInToWebsiteController_doWatch(aOptions) {
+    // for now, just say we're ready
+    // this._openDialogAndSendMessage(aOptions.rpId, "identity-delegate-watch", aOptions);
+    IdentityService.doReady(aOptions.rpId);
+  },
+
+  /**
+   * The website is requesting login so the user must choose an identity to use.
+   */
+  doRequest: function SignInToWebsiteController_doRequest(aOptions) {
+    log("request options", aOptions);
+    this._openDialogAndSendMessage(aOptions.rpId, "identity-delegate-request", aOptions);
+  },
+
+  /*
+   *
+   */
+  doLogout: function SignInToWebsiteController_doLogout(aOptions) {
+    log("logout options", aOptions);
+    IdentityService.doLogout(aOptions.rpId);
+  },
+
+  // FIXME: add a callback when the response from dialog is received
+  // so watch can call doReady, while request doesn't.
+  _openDialogAndSendMessage: function SignInToWebsiteController_openDialogAndSendMessage(aRpId, aMessage, aOptions) {
+    let browser = Services.wm.getMostRecentWindow("navigator:browser");
+    let content = browser.getContentWindow();
+    if (!content) {
+      // aErrorCb.onresult("NO_CONTENT_WINDOW");
+      return;
+    }
+
+    // prepare the message to be sent to gaia
+    let id = kOpenIdentityDialog + "-" + this.getRandomId();
+    let detail = {
+      type: kOpenIdentityDialog,
+      id: id
+    };
+
+    log("id before is ", id);
+    // wait for Gaia to send us a message back
+    content.addEventListener("mozContentEvent", function getAssertion(evt) {
+      log("id after is ", id);
+      let msg = evt.detail;
+      if (msg.id != id) {
+        log("mozContentEvent. evt.detail.id != ", id, msg);
+        content.removeEventListener("mozContentEvent", getAssertion);
+        return;
+      }
+
+      // Try to load the identity shim file containing the callbacks
+      // in the content script.
+      let frame = evt.detail.frame;
+      let frameLoader = frame.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
+      let mm = frameLoader.messageManager;
+      try {
+        log("about to load frame script");
+        mm.loadFrameScript(kIdentityShimFile, true);
+      } catch (e) {
+        log("Error loading ", kIdentityShimFile, " as a frame script: ", e);
+      }
+
+      mm.addMessageListener("identity-delegate-return-assertion", function(message) {
+        log("back with assertion", message.json);
+        if (message.json.assertion)
+          IdentityService.doLogin(aRpId, message.json.assertion);
+
+        IdentityService.doReady(aRpId);
+      });
+
+      // send the options down
+      log("sending message" , aMessage, aOptions);
+      mm.sendAsyncMessage(aMessage, aOptions);
+
+      log("done load frame script");
+
+      content.removeEventListener("mozContentEvent", getAssertion);
+    });
+
+    browser.shell.sendChromeEvent(detail);
+    log("sent", detail, "to gaia");
+  }
+
+};
new file mode 100644
--- /dev/null
+++ b/b2g/components/test/unit/head_identity.js
@@ -0,0 +1,118 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+// The following boilerplate makes sure that XPCom calls
+// that use the profile directory work.
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Identity",
+                                  "resource://gre/modules/identity/Identity.jsm",
+                                  "Identity");
+
+XPCOMUtils.defineLazyModuleGetter(this,
+                                  "Logger",
+                                  "resource://gre/modules/identity/LogUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this,
+                                   "uuidGenerator",
+                                   "@mozilla.org/uuid-generator;1",
+                                   "nsIUUIDGenerator");
+
+const TEST_URL = "https://myfavoriteflan.com";
+const TEST_USER = "uumellmahaye1969@hotmail.com";
+const TEST_PRIVKEY = "i-am-a-secret";
+const TEST_CERT = "i-am-a-cert";
+
+// The following are utility functions for Identity testing
+
+function log(...aMessageArgs) {
+  Logger.log.apply(Logger, ["test"].concat(aMessageArgs));
+}
+
+function partial(fn) {
+  let args = Array.prototype.slice.call(arguments, 1);
+  return function() {
+    return fn.apply(this, args.concat(Array.prototype.slice.call(arguments)));
+  };
+}
+
+function uuid() {
+  return uuidGenerator.generateUUID().toString();
+}
+
+// create a mock "doc" object, which the Identity Service
+// uses as a pointer back into the doc object
+function mock_doc(aIdentity, aOrigin, aDoFunc) {
+  let mockedDoc = {};
+  mockedDoc.id = uuid();
+  mockedDoc.loggedInEmail = aIdentity;
+  mockedDoc.origin = aOrigin;
+  mockedDoc['do'] = aDoFunc;
+  mockedDoc.doReady = partial(aDoFunc, 'ready');
+  mockedDoc.doLogin = partial(aDoFunc, 'login');
+  mockedDoc.doLogout = partial(aDoFunc, 'logout');
+  mockedDoc.doError = partial(aDoFunc, 'error');
+  mockedDoc.doCancel = partial(aDoFunc, 'cancel');
+  mockedDoc.doCoffee = partial(aDoFunc, 'coffee');
+
+  return mockedDoc;
+}
+
+// mimicking callback funtionality for ease of testing
+// this observer auto-removes itself after the observe function
+// is called, so this is meant to observe only ONE event.
+function makeObserver(aObserveTopic, aObserveFunc) {
+  let observer = {
+    // nsISupports provides type management in C++
+    // nsIObserver is to be an observer
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
+
+    observe: function (aSubject, aTopic, aData) {
+      if (aTopic == aObserveTopic) {
+        aObserveFunc(aSubject, aTopic, aData);
+        Services.obs.removeObserver(observer, aObserveTopic);
+      }
+    }
+  };
+
+  Services.obs.addObserver(observer, aObserveTopic, false);
+}
+
+// set up the ID service with an identity with keypair and all
+// when ready, invoke callback with the identity
+function setup_test_identity(identity, cert, cb) {
+  // set up the store so that we're supposed to be logged in
+  let store = get_idstore();
+
+  function keyGenerated(err, kpo) {
+    store.addIdentity(identity, kpo, cert);
+    cb();
+  };
+
+  jwcrypto.generateKeyPair("DS160", keyGenerated);
+}
+
+// takes a list of functions and returns a function that
+// when called the first time, calls the first func,
+// then the next time the second, etc.
+function call_sequentially() {
+  let numCalls = 0;
+  let funcs = arguments;
+
+  return function() {
+    if (!funcs[numCalls]) {
+      let argString = Array.prototype.slice.call(arguments).join(",");
+      do_throw("Too many calls: " + argString);
+      return;
+    }
+    funcs[numCalls].apply(funcs[numCalls],arguments);
+    numCalls += 1;
+  };
+}
new file mode 100644
--- /dev/null
+++ b/b2g/components/test/unit/test_signintowebsite.js
@@ -0,0 +1,45 @@
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "MinimalIDService",
+                                  "resource://gre/modules/identity/MinimalIdentity.jsm",
+                                  "IdentityService");
+
+XPCOMUtils.defineLazyModuleGetter(this, "SignInToWebsite",
+                                  "resource://gre/b2g/components/SignInToWebsite.jsm",
+                                  "SignInToWebsite");
+
+Cu.import("resource://gre/modules/identity/LogUtils.jsm");
+
+function log(...aMessageArgs) {
+  Logger.log.apply(Logger, ["test_signintowebsite"].concat(aMessageArgs));
+}
+
+function test_overall() {
+  do_check_neq(MinimalIDService, null);
+  run_next_test();
+}
+
+function test_mock_doc() {
+  do_test_pending();
+  let mockedDoc = mock_doc(null, TEST_URL, function(action, params) {
+    do_check_eq(action, 'coffee');
+    do_test_finished();
+    run_next_test();
+  });
+
+  mockedDoc.doCoffee();
+}
+
+// XXX bug 800085 complete these tests - mock the gaia content object
+// so we can round trip through the SignInToWebsiteController
+
+let TESTS = [
+  test_overall,
+  test_mock_doc,
+];
+
+TESTS.forEach(add_test);
+
+function run_test() {
+  run_next_test();
+}
--- a/b2g/components/test/unit/xpcshell.ini
+++ b/b2g/components/test/unit/xpcshell.ini
@@ -1,5 +1,11 @@
 [DEFAULT]
 head =
 tail =
 
 [test_bug793310.js]
+
+[test_signintowebsite.js]
+head = head_identity.js
+tail =
+
+
--- a/dom/identity/DOMIdentity.jsm
+++ b/dom/identity/DOMIdentity.jsm
@@ -7,23 +7,33 @@
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 // This is the parent process corresponding to nsDOMIdentity.
 let EXPORTED_SYMBOLS = ["DOMIdentity"];
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
+/*
 XPCOMUtils.defineLazyModuleGetter(this, "IdentityService",
                                   "resource://gre/modules/identity/Identity.jsm");
+*/
+
+// minimal Identity.jsm
+XPCOMUtils.defineLazyModuleGetter(this, "IdentityService",
+                                  "resource://gre/modules/identity/MinimalIdentity.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this,
                                   "Logger",
                                   "resource://gre/modules/identity/LogUtils.jsm");
 
+XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
+                                   "@mozilla.org/parentprocessmessagemanager;1",
+                                   "nsIMessageListenerManager");
+
 function log(...aMessageArgs) {
   Logger.log.apply(Logger, ["DOMIdentity"].concat(aMessageArgs));
 }
 
 function IDDOMMessage(aID) {
   this.id = aID;
 }
 
@@ -113,23 +123,25 @@ RPWatchContext.prototype = {
   doError: function RPWatchContext_onerror(aMessage) {
     log("doError: " + aMessage);
   }
 };
 
 let DOMIdentity = {
   // nsIMessageListener
   receiveMessage: function DOMIdentity_receiveMessage(aMessage) {
+      log("**received message", aMessage);
     let msg = aMessage.json;
 
     // Target is the frame message manager that called us and is
     // used to send replies back to the proper window.
-    let targetMM = aMessage.target
+    /* let targetMM = aMessage.target
                            .QueryInterface(Ci.nsIFrameLoaderOwner)
-                           .frameLoader.messageManager;
+                           .frameLoader.messageManager;*/
+    let targetMM = aMessage.target;
 
     switch (aMessage.name) {
       // RP
       case "Identity:RP:Watch":
         this._watch(msg, targetMM);
         break;
       case "Identity:RP:Request":
         this._request(msg);
@@ -194,18 +206,21 @@ let DOMIdentity = {
 
   _configureMessages: function DOMIdentity__configureMessages(aWindow, aRegister) {
     if (!aWindow.messageManager)
       return;
 
     let func = aWindow.messageManager[aRegister ? "addMessageListener"
                                                 : "removeMessageListener"];
 
+      log("in _configureMessages *****", aRegister, this.messages);
+
     for (let message of this.messages) {
-      func.call(aWindow.messageManager, message, this);
+      // func.call(aWindow.messageManager, message, this);
+	    ppmm.addMessageListener(message, this);
     }
   },
 
   _resetFrameState: function(aContext) {
     log("_resetFrameState: ", aContext.id);
     if (!aContext._mm) {
       throw new Error("ERROR: Trying to reset an invalid context");
     }
--- a/dom/identity/nsDOMIdentity.js
+++ b/dom/identity/nsDOMIdentity.js
@@ -12,16 +12,20 @@ const PREF_ENABLED = "dom.identity.enabl
 // Maximum length of a string that will go through IPC
 const MAX_STRING_LENGTH = 2048;
 // Maximum number of times navigator.id.request can be called for a document
 const MAX_RP_CALLS = 100;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
+XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
+                                   "@mozilla.org/childprocessmessagemanager;1",
+                                   "nsIMessageSender");
+
 // This is the child process corresponding to nsIDOMIdentity.
 
 
 function nsDOMIdentity(aIdentityInternal) {
   this._identityInternal = aIdentityInternal;
 }
 nsDOMIdentity.prototype = {
   __exposedProps__: {
@@ -478,20 +482,22 @@ nsDOMIdentityInternal.prototype = {
 
     let util = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                       .getInterface(Ci.nsIDOMWindowUtils);
     this._id = util.outerWindowID;
     this._innerWindowID = util.currentInnerWindowID;
 
     this._log("init was called from " + aWindow.document.location);
 
-    this._mm = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+    /*this._mm = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                       .getInterface(Ci.nsIWebNavigation)
                       .QueryInterface(Ci.nsIInterfaceRequestor)
                       .getInterface(Ci.nsIContentFrameMessageManager);
+    */
+    this._mm = cpmm;
 
     // Setup listeners for messages from parent process.
     this._messages = [
       "Identity:ResetState",
       "Identity:RP:Watch:OnLogin",
       "Identity:RP:Watch:OnLogout",
       "Identity:RP:Watch:OnReady",
       "Identity:RP:Request:OnCancel",
--- a/toolkit/identity/Makefile.in
+++ b/toolkit/identity/Makefile.in
@@ -30,16 +30,17 @@ CPPSRCS = \
   $(NULL)
 
 EXTRA_JS_MODULES = \
 	Identity.jsm \
 	IdentityProvider.jsm \
 	IdentityStore.jsm \
 	jwcrypto.jsm \
 	LogUtils.jsm \
+	MinimalIdentity.jsm \
 	RelyingParty.jsm \
 	Sandbox.jsm \
 	$(NULL)
 
 ifdef ENABLE_TESTS
     DIRS += tests
 endif
 
new file mode 100644
--- /dev/null
+++ b/toolkit/identity/MinimalIdentity.jsm
@@ -0,0 +1,370 @@
+/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * this alternate implementation of IdentityService
+ * provides just the channels for navigator.id,
+ * leaving the certificate storage to a server-provided app
+ */
+
+"use strict";
+
+const EXPORTED_SYMBOLS = ["IdentityService"];
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/identity/LogUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this,
+                                  "jwcrypto",
+                                  "resource://gre/modules/identity/jwcrypto.jsm");
+
+function log(...aMessageArgs) {
+  Logger.log.apply(Logger, ["minimal core"].concat(aMessageArgs));
+}
+function reportError(...aMessageArgs) {
+  Logger.reportError.apply(Logger, ["core"].concat(aMessageArgs));
+}
+
+function IDService() {
+  Services.obs.addObserver(this, "quit-application-granted", false);
+  Services.obs.addObserver(this, "identity-auth-complete", false);
+
+  // simplify, it's one object
+  this.RP = this;
+  this.IDP = this;
+
+  // keep track of flows
+  this._rpFlows = {};
+  this._authFlows = {};
+  this._provFlows = {};
+}
+
+IDService.prototype = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
+
+  observe: function observe(aSubject, aTopic, aData) {
+    switch (aTopic) {
+      case "quit-application-granted":
+        Services.obs.removeObserver(this, "quit-application-granted");
+        this.shutdown();
+        break;
+    }
+  },
+
+  shutdown: function shutdown() {
+    log("shutdown");
+    Services.obs.removeObserver(this, "quit-application-granted");
+  },
+
+  /**
+   * Parse an email into username and domain if it is valid, else return null
+   */
+  parseEmail: function parseEmail(email) {
+    var match = email.match(/^([^@]+)@([^@^/]+.[a-z]+)$/);
+    if (match) {
+      return {
+        username: match[1],
+        domain: match[2]
+      };
+    }
+    return null;
+  },
+
+  // RP stuff
+  /**
+   * Register a listener for a given windowID as a result of a call to
+   * navigator.id.watch().
+   *
+   * @param aCaller
+   *        (Object)  an object that represents the caller document, and
+   *                  is expected to have properties:
+   *                  - id (unique, e.g. uuid)
+   *                  - loggedInEmail (string or null)
+   *                  - origin (string)
+   *
+   *                  and a bunch of callbacks
+   *                  - doReady()
+   *                  - doLogin()
+   *                  - doLogout()
+   *                  - doError()
+   *                  - doCancel()
+   *
+   */
+  watch: function watch(aRpCaller) {
+    log("watch: ", aRpCaller);
+
+    // store the caller structure and notify the UI observers
+    this._rpFlows[aRpCaller.id] = aRpCaller;
+    let options = {rpId: aRpCaller.id, origin: aRpCaller.origin};
+    Services.obs.notifyObservers({wrappedJSObject: options},"identity-controller-watch", null);
+  },
+
+  /**
+   * Initiate a login with user interaction as a result of a call to
+   * navigator.id.request().
+   *
+   * @param aRPId
+   *        (integer)  the id of the doc object obtained in .watch()
+   *
+   * @param aOptions
+   *        (Object)  options including privacyPolicy, termsOfService
+   */
+  request: function request(aRPId, aOptions) {
+    log("request: rpId:", aRPId);
+    let rp = this._rpFlows[aRPId];
+
+    // Notify UX to display identity picker.
+    // Pass the doc id to UX so it can pass it back to us later.
+    let options = {rpId: aRPId, origin: rp.origin};
+
+    Services.obs.notifyObservers({wrappedJSObject: options}, "identity-controller-request", null);
+  },
+
+  /**
+   * Invoked when a user wishes to logout of a site (for instance, when clicking
+   * on an in-content logout button).
+   *
+   * @param aRpCallerId
+   *        (integer)  the id of the doc object obtained in .watch()
+   *
+   */
+  logout: function logout(aRpCallerId) {
+    log("logout: RP caller id:", aRpCallerId);
+    Services.obs.notifyObservers({wrappedJSObject: {rpId: aRpCallerId}},
+                                 "identity-controller-logout",
+                                 null);
+
+  },
+
+  // once the UI-and-display-logic components have received notifications, they call back with
+  // direct invocation of the following.
+
+  /**
+   * make login happen with an assertion
+   */
+  doLogin: function doLogin(aRpCallerId, aAssertion) {
+    let rp = this._rpFlows[aRpCallerId];
+    if (!rp)
+      return;
+
+    rp.doLogin(aAssertion);
+  },
+
+  doLogout: function doLogout(aRpCallerId) {
+    let rp = this._rpFlows[aRpCallerId];
+    if (!rp)
+      return;
+
+    rp.doLogout();
+  },
+
+  doReady: function doReady(aRpCallerId) {
+    let rp = this._rpFlows[aRpCallerId];
+    if (!rp)
+      return;
+
+    rp.doReady();
+  },
+
+
+  // IdP
+
+  /**
+   * the provisioning iframe sandbox has called navigator.id.beginProvisioning()
+   *
+   * @param aCaller
+   *        (object)  the iframe sandbox caller with all callbacks and
+   *                  other information.  Callbacks include:
+   *                  - doBeginProvisioningCallback(id, duration_s)
+   *                  - doGenKeyPairCallback(pk)
+   */
+  beginProvisioning: function beginProvisioning(aCaller) {
+  },
+
+  /**
+   * the provisioning iframe sandbox has called
+   * navigator.id.raiseProvisioningFailure()
+   *
+   * @param aProvId
+   *        (int)  the identifier of the provisioning flow tied to that sandbox
+   * @param aReason
+   */
+  raiseProvisioningFailure: function raiseProvisioningFailure(aProvId, aReason) {
+    reportError("Provisioning failure", aReason);
+  },
+
+  /**
+   * When navigator.id.genKeyPair is called from provisioning iframe sandbox.
+   * Generates a keypair for the current user being provisioned.
+   *
+   * @param aProvId
+   *        (int)  the identifier of the provisioning caller tied to that sandbox
+   *
+   * It is an error to call genKeypair without receiving the callback for
+   * the beginProvisioning() call first.
+   */
+  genKeyPair: function genKeyPair(aProvId) {
+  },
+
+  /**
+   * When navigator.id.registerCertificate is called from provisioning iframe
+   * sandbox.
+   *
+   * Sets the certificate for the user for which a certificate was requested
+   * via a preceding call to beginProvisioning (and genKeypair).
+   *
+   * @param aProvId
+   *        (integer) the identifier of the provisioning caller tied to that
+   *                  sandbox
+   *
+   * @param aCert
+   *        (String)  A JWT representing the signed certificate for the user
+   *                  being provisioned, provided by the IdP.
+   */
+  registerCertificate: function registerCertificate(aProvId, aCert) {
+  },
+
+  /**
+   * The authentication frame has called navigator.id.beginAuthentication
+   *
+   * IMPORTANT: the aCaller is *always* non-null, even if this is called from
+   * a regular content page. We have to make sure, on every DOM call, that
+   * aCaller is an expected authentication-flow identifier. If not, we throw
+   * an error or something.
+   *
+   * @param aCaller
+   *        (object)  the authentication caller
+   *
+   */
+  beginAuthentication: function beginAuthentication(aCaller) {
+  },
+
+  /**
+   * The auth frame has called navigator.id.completeAuthentication
+   *
+   * @param aAuthId
+   *        (int)  the identifier of the authentication caller tied to that sandbox
+   *
+   */
+  completeAuthentication: function completeAuthentication(aAuthId) {
+  },
+
+  /**
+   * The auth frame has called navigator.id.cancelAuthentication
+   *
+   * @param aAuthId
+   *        (int)  the identifier of the authentication caller
+   *
+   */
+  cancelAuthentication: function cancelAuthentication(aAuthId) {
+  },
+
+  // methods for chrome and add-ons
+
+  /**
+   * Discover the IdP for an identity
+   *
+   * @param aIdentity
+   *        (string) the email we're logging in with
+   *
+   * @param aCallback
+   *        (function) callback to invoke on completion
+   *                   with first-positional parameter the error.
+   */
+  _discoverIdentityProvider: function _discoverIdentityProvider(aIdentity, aCallback) {
+    // XXX bug 767610 - validate email address call
+    // When that is available, we can remove this custom parser
+    var parsedEmail = this.parseEmail(aIdentity);
+    if (parsedEmail === null) {
+      return aCallback("Could not parse email: " + aIdentity);
+    }
+    log("_discoverIdentityProvider: identity:", aIdentity, "domain:", parsedEmail.domain);
+
+    this._fetchWellKnownFile(parsedEmail.domain, function fetchedWellKnown(err, idpParams) {
+      // idpParams includes the pk, authorization url, and
+      // provisioning url.
+
+      // XXX bug 769861 follow any authority delegations
+      // if no well-known at any point in the delegation
+      // fall back to browserid.org as IdP
+      return aCallback(err, idpParams);
+    });
+  },
+
+  /**
+   * Fetch the well-known file from the domain.
+   *
+   * @param aDomain
+   *
+   * @param aScheme
+b   *        (string) (optional) Protocol to use.  Default is https.
+   *                 This is necessary because we are unable to test
+   *                 https.
+   *
+   * @param aCallback
+   *
+   */
+  _fetchWellKnownFile: function _fetchWellKnownFile(aDomain, aCallback, aScheme='https') {
+    // XXX bug 769854 make tests https and remove aScheme option
+    let url = aScheme + '://' + aDomain + "/.well-known/browserid";
+    log("_fetchWellKnownFile:", url);
+
+    // this appears to be a more successful way to get at xmlhttprequest (which supposedly will close with a window
+    let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
+                .createInstance(Ci.nsIXMLHttpRequest);
+
+    // XXX bug 769865 gracefully handle being off-line
+    // XXX bug 769866 decide on how to handle redirects
+    req.open("GET", url, true);
+    req.responseType = "json";
+    req.mozBackgroundRequest = true;
+    req.onload = function _fetchWellKnownFile_onload() {
+      if (req.status < 200 || req.status >= 400) {
+        log("_fetchWellKnownFile", url, ": server returned status:", req.status);
+        return aCallback("Error");
+      }
+      try {
+        let idpParams = req.response;
+
+        // Verify that the IdP returned a valid configuration
+        if (! (idpParams.provisioning &&
+            idpParams.authentication &&
+            idpParams['public-key'])) {
+          let errStr= "Invalid well-known file from: " + aDomain;
+          log("_fetchWellKnownFile:", errStr);
+          return aCallback(errStr);
+        }
+
+        let callbackObj = {
+          domain: aDomain,
+          idpParams: idpParams,
+        };
+        log("_fetchWellKnownFile result: ", callbackObj);
+        // Yay.  Valid IdP configuration for the domain.
+        return aCallback(null, callbackObj);
+
+      } catch (err) {
+        reportError("_fetchWellKnownFile", "Bad configuration from", aDomain, err);
+        return aCallback(err.toString());
+      }
+    };
+    req.onerror = function _fetchWellKnownFile_onerror() {
+      log("_fetchWellKnownFile", "ERROR:", req.status, req.statusText);
+      log("ERROR: _fetchWellKnownFile:", err);
+      return aCallback("Error");
+    };
+    req.send(null);
+  },
+
+};
+
+let IdentityService = new IDService();
--- a/toolkit/identity/tests/Makefile.in
+++ b/toolkit/identity/tests/Makefile.in
@@ -8,11 +8,11 @@ srcdir         = @srcdir@
 VPATH          = @srcdir@
 relativesrcdir = @relativesrcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 MODULE = test_identity
 XPCSHELL_TESTS = unit
 
-DIRS = chrome mochitest
+DIRS = chrome mochitest 
 
 include $(topsrcdir)/config/rules.mk
--- a/toolkit/identity/tests/unit/test_identity.js
+++ b/toolkit/identity/tests/unit/test_identity.js
@@ -41,18 +41,18 @@ function test_select_identity() {
   IDService.reset();
 
   let id = "ishtar@mockmyid.com";
   setup_test_identity(id, TEST_CERT, function() {
     let gotAssertion = false;
     let mockedDoc = mock_doc(null, TEST_URL, call_sequentially(
       function(action, params) {
         // ready emitted from first watch() call
-	do_check_eq(action, 'ready');
-	do_check_null(params);
+        do_check_eq(action, 'ready');
+        do_check_null(params);
       },
       // first the login call
       function(action, params) {
         do_check_eq(action, 'login');
         do_check_neq(params, null);
 
         // XXX - check that the assertion is for the right email
 
new file mode 100644
--- /dev/null
+++ b/toolkit/identity/tests/unit/test_minimalidentity.js
@@ -0,0 +1,95 @@
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "MinimalIDService",
+                                  "resource://gre/modules/identity/MinimalIdentity.jsm",
+                                  "IdentityService");
+
+Cu.import("resource://gre/modules/identity/LogUtils.jsm");
+
+function log(...aMessageArgs) {
+  Logger.log.apply(Logger, ["test_minimalidentity"].concat(aMessageArgs));
+}
+
+function test_overall() {
+  do_check_neq(MinimalIDService, null);
+  run_next_test();
+}
+
+function test_mock_doc() {
+  do_test_pending();
+  let mockedDoc = mock_doc(null, TEST_URL, function(action, params) {
+    do_check_eq(action, 'coffee');
+    do_test_finished();
+    run_next_test();
+  });
+
+  mockedDoc.doCoffee();
+}
+
+/*
+ * Test that the "identity-controller-watch" signal is emitted correctly
+ */
+function test_watch() {
+  do_test_pending();
+
+  let mockedDoc = mock_doc(null, TEST_URL);
+  makeObserver("identity-controller-watch", function (aSubject, aTopic, aData) {
+    do_check_eq(aSubject.wrappedJSObject.rpId, mockedDoc.id);
+    do_check_eq(aSubject.wrappedJSObject.origin, TEST_URL);
+    do_test_finished();
+    run_next_test();
+   });
+
+  MinimalIDService.RP.watch(mockedDoc);
+}
+
+/*
+ * Test that the "identity-controller-request" signal is emitted correctly
+ */
+function test_request() {
+  do_test_pending();
+
+  let mockedDoc = mock_doc(null, TEST_URL);
+  makeObserver("identity-controller-request", function (aSubject, aTopic, aData) {
+    do_check_eq(aSubject.wrappedJSObject.rpId, mockedDoc.id);
+    do_check_eq(aSubject.wrappedJSObject.origin, TEST_URL);
+    do_test_finished();
+    run_next_test();
+  });
+
+  MinimalIDService.RP.watch(mockedDoc);
+  MinimalIDService.RP.request(mockedDoc.id, {});
+}
+
+/*
+ * Test that the "identity-controller-logout" signal is emitted correctly
+ */
+function test_logout() {
+  do_test_pending();
+
+  let mockedDoc = mock_doc(null, TEST_URL);
+  makeObserver("identity-controller-logout", function (aSubject, aTopic, aData) {
+    do_check_eq(aSubject.wrappedJSObject.rpId, mockedDoc.id);
+    do_test_finished();
+    run_next_test();
+  });
+
+  MinimalIDService.RP.watch(mockedDoc);
+  MinimalIDService.RP.logout(mockedDoc.id, {});
+}
+
+
+
+let TESTS = [
+  test_overall,
+  test_mock_doc,
+  test_watch,
+  test_request,
+  test_logout
+];
+
+TESTS.forEach(add_test);
+
+function run_test() {
+  run_next_test();
+}
\ No newline at end of file
--- a/toolkit/identity/tests/unit/xpcshell.ini
+++ b/toolkit/identity/tests/unit/xpcshell.ini
@@ -1,16 +1,17 @@
 [DEFAULT]
 head = head_identity.js
 tail = tail_identity.js
 
 # Test load modules first so syntax failures are caught early.
 [test_load_modules.js]
+[test_minimalidentity.js]
+
 [test_log_utils.js]
-
 [test_authentication.js]
 [test_crypto_service.js]
 [test_identity.js]
 [test_jwcrypto.js]
 [test_observer_topics.js]
 [test_provisioning.js]
 [test_relying_party.js]
 [test_store.js]