Source code
Revision control
Copy as Markdown
Other Tools
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et 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,
"use strict";
XPCOMUtils.defineLazyGlobalGetters(this, ["XMLHttpRequest", "ChannelWrapper"]);
var { promiseDocumentLoaded } = ExtensionUtils;
const checkRedirected = (url, redirectURI) => {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest({ mozAnon: false });
xhr.open("GET", url);
// We expect this if the user has not authenticated.
xhr.onload = () => {
reject(0);
};
// An unexpected error happened, log for extension authors.
xhr.onerror = () => {
reject(xhr.status);
};
// Catch redirect to our redirect_uri before a new request is made.
xhr.channel.notificationCallbacks = {
QueryInterface: ChromeUtils.generateQI([
"nsIInterfaceRequestor",
"nsIChannelEventSync",
]),
getInterface: ChromeUtils.generateQI(["nsIChannelEventSink"]),
asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
let responseURL = newChannel.URI.spec;
if (responseURL.startsWith(redirectURI)) {
resolve(responseURL);
// Cancel the redirect.
callback.onRedirectVerifyCallback(Cr.NS_BINDING_ABORTED);
return;
}
callback.onRedirectVerifyCallback(Cr.NS_OK);
},
};
xhr.send();
});
};
const openOAuthWindow = (details, redirectURI) => {
let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
let supportsStringPrefURL = Cc[
"@mozilla.org/supports-string;1"
].createInstance(Ci.nsISupportsString);
supportsStringPrefURL.data = details.url;
args.appendElement(supportsStringPrefURL);
let window = Services.ww.openWindow(
null,
AppConstants.BROWSER_CHROME_URL,
"launchWebAuthFlow_dialog",
"chrome,location=yes,centerscreen,dialog=no,resizable=yes,scrollbars=yes",
args
);
return new Promise((resolve, reject) => {
let httpActivityDistributor = Cc[
"@mozilla.org/network/http-activity-distributor;1"
].getService(Ci.nsIHttpActivityDistributor);
let unloadListener;
let httpObserver;
const resolveIfRedirectURI = channel => {
const url = channel.URI && channel.URI.spec;
if (!url || !url.startsWith(redirectURI)) {
return;
}
// Early exit if channel isn't related to the oauth dialog.
let wrapper = ChannelWrapper.get(channel);
if (
!wrapper.browserElement &&
wrapper.browserElement !== window.gBrowser.selectedBrowser
) {
return;
}
wrapper.cancel(Cr.NS_ERROR_ABORT, Ci.nsILoadInfo.BLOCKING_REASON_NONE);
window.gBrowser.webNavigation.stop(Ci.nsIWebNavigation.STOP_ALL);
window.removeEventListener("unload", unloadListener);
httpActivityDistributor.removeObserver(httpObserver);
window.close();
resolve(url);
};
httpObserver = {
observeActivity(channel) {
try {
channel.QueryInterface(Ci.nsIChannel);
} catch {
// Ignore activities for channels that doesn't implement nsIChannel
// (e.g. a NullHttpChannel).
return;
}
resolveIfRedirectURI(channel);
},
};
httpActivityDistributor.addObserver(httpObserver);
// If the user just closes the window we need to reject
unloadListener = () => {
window.removeEventListener("unload", unloadListener);
httpActivityDistributor.removeObserver(httpObserver);
reject({ message: "User cancelled or denied access." });
};
promiseDocumentLoaded(window.document).then(() => {
window.addEventListener("unload", unloadListener);
});
});
};
this.identity = class extends ExtensionAPI {
getAPI(context) {
return {
identity: {
launchWebAuthFlowInParent: function (details, redirectURI) {
// If the request is automatically redirected the user has already
// authorized and we do not want to show the window.
let promise = checkRedirected(details.url, redirectURI).catch(
requestError => {
// requestError is zero or xhr.status
if (requestError !== 0) {
Cu.reportError(
`browser.identity auth check failed with ${requestError}`
);
return Promise.reject({ message: "Invalid request" });
}
if (!details.interactive) {
return Promise.reject({ message: `Requires user interaction` });
}
return openOAuthWindow(details, redirectURI);
}
);
if (context.isBackgroundContext) {
context.extension.emit("background-script-idle-waituntil", {
promise,
reason: "launchWebAuthFlow",
});
}
return promise;
},
},
};
}
};