mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-05 16:46:26 +00:00
445 lines
16 KiB
JavaScript
445 lines
16 KiB
JavaScript
/* 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/. */
|
|
|
|
/*
|
|
* SignInToWebsite.jsm - UX Controller and means for accessing identity
|
|
* cookies on behalf of relying parties.
|
|
*
|
|
* Currently, the b2g security architecture isolates web applications
|
|
* so that each window has access only to a local cookie jar:
|
|
*
|
|
* To prevent Web apps from interfering with one another, each one is
|
|
* hosted on a separate domain, and therefore may only access the
|
|
* resources associated with its domain. These resources include
|
|
* things such as IndexedDB databases, cookies, offline storage,
|
|
* and so forth.
|
|
*
|
|
* -- https://developer.mozilla.org/en-US/docs/Mozilla/Firefox_OS/Security/Security_model
|
|
*
|
|
* As a result, an authentication system like Persona cannot share its
|
|
* cookie jar with multiple relying parties, and so would require a
|
|
* fresh login request in every window. This would not be a good
|
|
* experience.
|
|
*
|
|
*
|
|
* In order for navigator.id.request() to maintain state in a single
|
|
* cookie jar, we cause all Persona interactions to take place in a
|
|
* content context that is launched by the system application, with the
|
|
* result that Persona has a single cookie jar that all Relying
|
|
* Parties can use. Since of course those Relying Parties cannot
|
|
* reach into the system cookie jar, the Controller in this module
|
|
* provides a way to get messages and data to and fro between the
|
|
* Relying Party in its window context, and the Persona internal api
|
|
* in its context.
|
|
*
|
|
* On the Relying Party's side, say a web page invokes
|
|
* navigator.id.watch(), to register callbacks, and then
|
|
* navigator.id.request() to request an assertion. The navigator.id
|
|
* calls are provided by nsDOMIdentity. nsDOMIdentity messages down
|
|
* to the privileged DOMIdentity code (using cpmm and ppmm message
|
|
* managers). DOMIdentity stores the state of Relying Party flows
|
|
* using an Identity service (MinimalIdentity.jsm), and emits messages
|
|
* requesting Persona functions (doWatch, doReady, doLogout).
|
|
*
|
|
* The Identity service sends these observer messages to the
|
|
* Controller in this module, which in turn triggers content to open a
|
|
* window to host the Persona js. If user interaction is required,
|
|
* content will open the trusty UI. If user interaction is not required,
|
|
* and we only need to get to Persona functions, content will open a
|
|
* hidden iframe. In either case, a window is opened into which the
|
|
* controller causes the script identity.js to be injected. This
|
|
* script provides the glue between the in-page javascript and the
|
|
* pipe back down to the Controller, translating navigator.internal
|
|
* function callbacks into messages sent back to the Controller.
|
|
*
|
|
* As a result, a navigator.internal function in the hosted popup or
|
|
* iframe can call back to the injected identity.js (doReady, doLogin,
|
|
* or doLogout). identity.js callbacks send messages back through the
|
|
* pipe to the Controller. The controller invokes the corresponding
|
|
* function on the Identity Service (doReady, doLogin, or doLogout).
|
|
* The IdentityService calls the corresponding callback for the
|
|
* correct Relying Party, which causes DOMIdentity to send a message
|
|
* up to the Relying Party through nsDOMIdentity
|
|
* (Identity:RP:Watch:OnLogin etc.), and finally, nsDOMIdentity
|
|
* receives these messages and calls the original callback that the
|
|
* Relying Party registered (navigator.id.watch(),
|
|
* navigator.id.request(), or navigator.id.logout()).
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
this.EXPORTED_SYMBOLS = ["SignInToWebsiteController"];
|
|
|
|
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, "getRandomId",
|
|
"resource://gre/modules/identity/IdentityUtils.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "IdentityService",
|
|
"resource://gre/modules/identity/MinimalIdentity.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "Logger",
|
|
"resource://gre/modules/identity/LogUtils.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
|
|
"resource://gre/modules/SystemAppProxy.jsm");
|
|
|
|
// The default persona uri; can be overwritten with toolkit.identity.uri pref.
|
|
// Do this if you want to repoint to a different service for testing.
|
|
// There's no point in setting up an observer to monitor the pref, as b2g prefs
|
|
// can only be overwritten when the profie is recreated. So just get the value
|
|
// on start-up.
|
|
let kPersonaUri = "https://firefoxos.persona.org";
|
|
try {
|
|
kPersonaUri = Services.prefs.getCharPref("toolkit.identity.uri");
|
|
} catch(noSuchPref) {
|
|
// stick with the default value
|
|
}
|
|
|
|
// JS shim that contains the callback functions that
|
|
// live within the identity UI provisioning frame.
|
|
const kIdentityShimFile = "chrome://b2g/content/identity.js";
|
|
|
|
// Type of MozChromeEvents to handle id dialogs.
|
|
const kOpenIdentityDialog = "id-dialog-open";
|
|
const kDoneIdentityDialog = "id-dialog-done";
|
|
const kCloseIdentityDialog = "id-dialog-close-iframe";
|
|
|
|
// Observer messages to communicate to shim
|
|
const kIdentityDelegateWatch = "identity-delegate-watch";
|
|
const kIdentityDelegateRequest = "identity-delegate-request";
|
|
const kIdentityDelegateLogout = "identity-delegate-logout";
|
|
const kIdentityDelegateFinished = "identity-delegate-finished";
|
|
const kIdentityDelegateReady = "identity-delegate-ready";
|
|
|
|
const kIdentityControllerDoMethod = "identity-controller-doMethod";
|
|
|
|
function log(...aMessageArgs) {
|
|
Logger.log.apply(Logger, ["SignInToWebsiteController"].concat(aMessageArgs));
|
|
}
|
|
|
|
log("persona uri =", kPersonaUri);
|
|
|
|
function sendChromeEvent(details) {
|
|
details.uri = kPersonaUri;
|
|
SystemAppProxy.dispatchEvent(details);
|
|
}
|
|
|
|
function Pipe() {
|
|
this._watchers = [];
|
|
}
|
|
|
|
Pipe.prototype = {
|
|
init: function pipe_init() {
|
|
Services.obs.addObserver(this, "identity-child-process-shutdown", false);
|
|
Services.obs.addObserver(this, "identity-controller-unwatch", false);
|
|
},
|
|
|
|
uninit: function pipe_uninit() {
|
|
Services.obs.removeObserver(this, "identity-child-process-shutdown");
|
|
Services.obs.removeObserver(this, "identity-controller-unwatch");
|
|
},
|
|
|
|
observe: function Pipe_observe(aSubject, aTopic, aData) {
|
|
let options = {};
|
|
if (aSubject) {
|
|
options = aSubject.wrappedJSObject;
|
|
}
|
|
switch (aTopic) {
|
|
case "identity-child-process-shutdown":
|
|
log("pipe removing watchers by message manager");
|
|
this._removeWatchers(null, options.messageManager);
|
|
break;
|
|
|
|
case "identity-controller-unwatch":
|
|
log("unwatching", options.id);
|
|
this._removeWatchers(options.id, options.messageManager);
|
|
break;
|
|
}
|
|
},
|
|
|
|
_addWatcher: function Pipe__addWatcher(aId, aMm) {
|
|
log("Adding watcher with id", aId);
|
|
for (let i = 0; i < this._watchers.length; ++i) {
|
|
let watcher = this._watchers[i];
|
|
if (this._watcher.id === aId) {
|
|
watcher.count++;
|
|
return;
|
|
}
|
|
}
|
|
this._watchers.push({id: aId, count: 1, mm: aMm});
|
|
},
|
|
|
|
_removeWatchers: function Pipe__removeWatcher(aId, aMm) {
|
|
let checkId = aId !== null;
|
|
let index = -1;
|
|
for (let i = 0; i < this._watchers.length; ++i) {
|
|
let watcher = this._watchers[i];
|
|
if (watcher.mm === aMm &&
|
|
(!checkId || (checkId && watcher.id === aId))) {
|
|
index = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (index !== -1) {
|
|
if (checkId) {
|
|
if (--(this._watchers[index].count) === 0) {
|
|
this._watchers.splice(index, 1);
|
|
}
|
|
} else {
|
|
this._watchers.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
if (this._watchers.length === 0) {
|
|
log("No more watchers; clean up persona host iframe");
|
|
let detail = {
|
|
type: kCloseIdentityDialog
|
|
};
|
|
log('telling content to close the dialog');
|
|
// tell content to close the dialog
|
|
sendChromeEvent(detail);
|
|
}
|
|
},
|
|
|
|
communicate: function(aRpOptions, aContentOptions, aMessageCallback) {
|
|
let rpID = aRpOptions.id;
|
|
let rpMM = aRpOptions.mm;
|
|
if (rpMM) {
|
|
this._addWatcher(rpID, rpMM);
|
|
}
|
|
|
|
log("RP options:", aRpOptions, "\n content options:", aContentOptions);
|
|
|
|
// This content variable is injected into the scope of
|
|
// kIdentityShimFile, where it is used to access the BrowserID object
|
|
// and its internal API.
|
|
let mm = null;
|
|
let uuid = getRandomId();
|
|
let self = this;
|
|
|
|
function removeMessageListeners() {
|
|
if (mm) {
|
|
mm.removeMessageListener(kIdentityDelegateFinished, identityDelegateFinished);
|
|
mm.removeMessageListener(kIdentityControllerDoMethod, aMessageCallback);
|
|
}
|
|
}
|
|
|
|
function identityDelegateFinished() {
|
|
removeMessageListeners();
|
|
|
|
let detail = {
|
|
type: kDoneIdentityDialog,
|
|
showUI: aContentOptions.showUI || false,
|
|
id: kDoneIdentityDialog + "-" + uuid,
|
|
requestId: aRpOptions.id
|
|
};
|
|
log('received delegate finished; telling content to close the dialog');
|
|
sendChromeEvent(detail);
|
|
self._removeWatchers(rpID, rpMM);
|
|
}
|
|
|
|
SystemAppProxy.addEventListener("mozContentEvent", function getAssertion(evt) {
|
|
let msg = evt.detail;
|
|
if (!msg.id.match(uuid)) {
|
|
return;
|
|
}
|
|
|
|
switch (msg.id) {
|
|
case kOpenIdentityDialog + '-' + uuid:
|
|
if (msg.type === 'cancel') {
|
|
// The user closed the dialog. Clean up and call cancel.
|
|
SystemAppProxy.removeEventListener("mozContentEvent", getAssertion);
|
|
removeMessageListeners();
|
|
aMessageCallback({json: {method: "cancel"}});
|
|
} else {
|
|
// The window has opened. Inject the identity shim file containing
|
|
// the callbacks in the content script. This could be either the
|
|
// visible popup that the user interacts with, or it could be an
|
|
// invisible frame.
|
|
let frame = evt.detail.frame;
|
|
let frameLoader = frame.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
|
|
mm = frameLoader.messageManager;
|
|
try {
|
|
mm.loadFrameScript(kIdentityShimFile, true, true);
|
|
log("Loaded shim", kIdentityShimFile);
|
|
} catch (e) {
|
|
log("Error loading", kIdentityShimFile, "as a frame script:", e);
|
|
}
|
|
|
|
// There are two messages that the delegate can send back: a "do
|
|
// method" event, and a "finished" event. We pass the do-method
|
|
// events straight to the caller for interpretation and handling.
|
|
// If we receive a "finished" event, then the delegate is done, so
|
|
// we shut down the pipe and clean up.
|
|
mm.addMessageListener(kIdentityControllerDoMethod, aMessageCallback);
|
|
mm.addMessageListener(kIdentityDelegateFinished, identityDelegateFinished);
|
|
|
|
mm.sendAsyncMessage(aContentOptions.message, aRpOptions);
|
|
}
|
|
break;
|
|
|
|
case kDoneIdentityDialog + '-' + uuid:
|
|
// Received our assertion. The message manager callbacks will handle
|
|
// communicating back to the IDService. All we have to do is remove
|
|
// this listener.
|
|
SystemAppProxy.removeEventListener("mozContentEvent", getAssertion);
|
|
break;
|
|
|
|
default:
|
|
log("ERROR - Unexpected message: id=" + msg.id + ", type=" + msg.type + ", errorMsg=" + msg.errorMsg);
|
|
break;
|
|
}
|
|
|
|
});
|
|
|
|
// Tell content to open the identity iframe or trusty popup. The parameter
|
|
// showUI signals whether user interaction is needed. If it is, content will
|
|
// open a dialog; if not, a hidden iframe. In each case, BrowserID is
|
|
// available in the context.
|
|
let detail = {
|
|
type: kOpenIdentityDialog,
|
|
showUI: aContentOptions.showUI || false,
|
|
id: kOpenIdentityDialog + "-" + uuid,
|
|
requestId: aRpOptions.id
|
|
};
|
|
|
|
sendChromeEvent(detail);
|
|
}
|
|
|
|
};
|
|
|
|
/*
|
|
* The controller sits between the IdentityService used by DOMIdentity
|
|
* and a content process launches an (invisible) iframe or (visible)
|
|
* trusty UI. Using an injected js script (identity.js), the
|
|
* controller enables the content window to access the persona identity
|
|
* storage in the system cookie jar and send events back via the
|
|
* controller into IdentityService and DOM, and ultimately up to the
|
|
* Relying Party, which is open in a different window context.
|
|
*/
|
|
this.SignInToWebsiteController = {
|
|
|
|
/*
|
|
* Initialize the controller. To use a different content communication pipe,
|
|
* such as when mocking it in tests, pass aOptions.pipe.
|
|
*/
|
|
init: function SignInToWebsiteController_init(aOptions) {
|
|
aOptions = aOptions || {};
|
|
this.pipe = aOptions.pipe || new Pipe();
|
|
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;
|
|
}
|
|
},
|
|
|
|
/*
|
|
* options: method required - name of method to invoke
|
|
* assertion optional
|
|
*/
|
|
_makeDoMethodCallback: function SignInToWebsiteController__makeDoMethodCallback(aRpId) {
|
|
return function SignInToWebsiteController_methodCallback(aOptions) {
|
|
let message = aOptions.json;
|
|
if (typeof message === 'string') {
|
|
message = JSON.parse(message);
|
|
}
|
|
|
|
switch (message.method) {
|
|
case "ready":
|
|
IdentityService.doReady(aRpId);
|
|
break;
|
|
|
|
case "login":
|
|
if (message._internalParams) {
|
|
IdentityService.doLogin(aRpId, message.assertion, message._internalParams);
|
|
} else {
|
|
IdentityService.doLogin(aRpId, message.assertion);
|
|
}
|
|
break;
|
|
|
|
case "logout":
|
|
IdentityService.doLogout(aRpId);
|
|
break;
|
|
|
|
case "cancel":
|
|
IdentityService.doCancel(aRpId);
|
|
break;
|
|
|
|
default:
|
|
log("WARNING: wonky method call:", message.method);
|
|
break;
|
|
}
|
|
};
|
|
},
|
|
|
|
doWatch: function SignInToWebsiteController_doWatch(aRpOptions) {
|
|
// dom prevents watch from being called twice
|
|
let contentOptions = {
|
|
message: kIdentityDelegateWatch,
|
|
showUI: false
|
|
};
|
|
this.pipe.communicate(aRpOptions, contentOptions,
|
|
this._makeDoMethodCallback(aRpOptions.id));
|
|
},
|
|
|
|
/**
|
|
* The website is requesting login so the user must choose an identity to use.
|
|
*/
|
|
doRequest: function SignInToWebsiteController_doRequest(aRpOptions) {
|
|
log("doRequest", aRpOptions);
|
|
let contentOptions = {
|
|
message: kIdentityDelegateRequest,
|
|
showUI: true
|
|
};
|
|
this.pipe.communicate(aRpOptions, contentOptions,
|
|
this._makeDoMethodCallback(aRpOptions.id));
|
|
},
|
|
|
|
/*
|
|
*
|
|
*/
|
|
doLogout: function SignInToWebsiteController_doLogout(aRpOptions) {
|
|
log("doLogout", aRpOptions);
|
|
let contentOptions = {
|
|
message: kIdentityDelegateLogout,
|
|
showUI: false
|
|
};
|
|
this.pipe.communicate(aRpOptions, contentOptions,
|
|
this._makeDoMethodCallback(aRpOptions.id));
|
|
}
|
|
|
|
};
|