--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -4711,16 +4711,18 @@ pref("xpinstall.whitelist.required", tru
pref("xpinstall.signatures.required", false);
pref("extensions.alwaysUnpack", false);
pref("extensions.minCompatiblePlatformVersion", "2.0");
pref("extensions.webExtensionsMinPlatformVersion", "42.0a1");
// Other webextensions prefs
pref("extensions.webextensions.keepStorageOnUninstall", false);
pref("extensions.webextensions.keepUuidOnUninstall", false);
+// Redirect basedomain used by identity api
+pref("extensions.webextensions.identity.redirectDomain", "extensions.allizom.org");
pref("network.buffer.cache.count", 24);
pref("network.buffer.cache.size", 32768);
// Desktop Notification
pref("notification.feature.enabled", false);
// Web Notification
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ext-c-identity.js
@@ -0,0 +1,156 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* global redirectDomain */
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr, Constructor: CC} = Components;
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
+ "resource://services-common/utils.js");
+XPCOMUtils.defineLazyPreferenceGetter(this, "redirectDomain",
+ "extensions.webextensions.identity.redirectDomain");
+
+let CryptoHash = CC("@mozilla.org/security/hash;1", "nsICryptoHash", "initWithString");
+Cu.importGlobalProperties(["URL", "XMLHttpRequest", "TextEncoder"]);
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+const {
+ promiseDocumentLoaded,
+} = ExtensionUtils;
+
+function computeHash(str) {
+ let byteArr = new TextEncoder().encode(str);
+ let hash = new CryptoHash("sha1");
+ hash.update(byteArr, byteArr.length);
+ return CommonUtils.bytesAsHex(hash.finish(false));
+}
+
+function checkRedirected(url, redirectURI) {
+ return new Promise((resolve, reject) => {
+ let xhr = new XMLHttpRequest();
+ xhr.open("HEAD", 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: XPCOMUtils.generateQI([Ci.nsIInterfaceRequestor, Ci.nsIChannelEventSync]),
+
+ getInterface: XPCOMUtils.generateQI([Ci.nsIChannelEventSink]),
+
+ asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
+ let responseURL = newChannel.URI.spec;
+ if (responseURL.startsWith(redirectURI)) {
+ resolve(responseURL);
+ // Cancel the redirect.
+ callback.onRedirectVerifyCallback(Components.results.NS_BINDING_ABORTED);
+ return;
+ }
+ callback.onRedirectVerifyCallback(Components.results.NS_OK);
+ },
+ };
+ xhr.send();
+ });
+}
+
+function 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, /* weak =*/ false);
+
+ let window = Services.ww.openWindow(null,
+ Services.prefs.getCharPref("browser.chromeURL"),
+ "launchWebAuthFlow_dialog",
+ "chrome,location=yes,centerscreen,dialog=no,resizable=yes",
+ args);
+
+ return new Promise((resolve, reject) => {
+ let wpl;
+
+ // If the user just closes the window we need to reject
+ function unloadlistener() {
+ window.removeEventListener("unload", unloadlistener);
+ window.gBrowser.removeTabsProgressListener(wpl);
+ reject({message: "User cancelled or denied access."});
+ }
+
+ wpl = {
+ onLocationChange(browser, webProgress, request, locationURI) {
+ if (locationURI.spec.startsWith(redirectURI)) {
+ resolve(locationURI.spec);
+ window.removeEventListener("unload", unloadlistener);
+ window.gBrowser.removeTabsProgressListener(wpl);
+ window.close();
+ }
+ },
+ onProgressChange() {},
+ onStatusChange() {},
+ onSecurityChange() {},
+ };
+
+ promiseDocumentLoaded(window.document).then(() => {
+ window.gBrowser.addTabsProgressListener(wpl);
+ window.addEventListener("unload", unloadlistener);
+ });
+ });
+}
+
+extensions.registerSchemaAPI("identity", "addon_child", context => {
+ let {extension} = context;
+ return {
+ identity: {
+ launchWebAuthFlow: function(details) {
+ // In OAuth2 the url should have a redirect_uri param, parse the url and grab it
+ let url, redirectURI;
+ try {
+ url = new URL(details.url);
+ } catch (e) {
+ return Promise.reject({message: "details.url is invalid"});
+ }
+ try {
+ redirectURI = new URL(url.searchParams.get("redirect_uri"));
+ if (!redirectURI) {
+ return Promise.reject({message: "redirect_uri is missing"});
+ }
+ } catch (e) {
+ return Promise.reject({message: "redirect_uri is invalid"});
+ }
+ if (!redirectURI.href.startsWith(this.getRedirectURL())) {
+ // Any url will work, but we suggest addons use getRedirectURL.
+ Services.console.logStringMessage("WebExtensions: redirect_uri should use browser.identity.getRedirectURL");
+ }
+
+ // If the request is automatically redirected the user has already
+ // authorized and we do not want to show the window.
+ return 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);
+ });
+ },
+
+ getRedirectURL: function(path = "") {
+ let hash = computeHash(extension.id);
+ let url = new URL(`https://${hash}.${redirectDomain}/`);
+ url.pathname = path;
+ return url.href;
+ },
+ },
+ };
+});
--- a/toolkit/components/extensions/extensions-toolkit.manifest
+++ b/toolkit/components/extensions/extensions-toolkit.manifest
@@ -20,28 +20,34 @@ category webextension-scripts-content i1
category webextension-scripts-content runtime chrome://extensions/content/ext-c-runtime.js
category webextension-scripts-content test chrome://extensions/content/ext-c-test.js
category webextension-scripts-content storage chrome://extensions/content/ext-c-storage.js
# scripts that must run in the same process as addon code.
category webextension-scripts-addon backgroundPage chrome://extensions/content/ext-c-backgroundPage.js
category webextension-scripts-addon extension chrome://extensions/content/ext-c-extension.js
category webextension-scripts-addon i18n chrome://extensions/content/ext-i18n.js
+#ifndef ANDROID
+category webextension-scripts-addon identity chrome://extensions/content/ext-c-identity.js
+#endif
category webextension-scripts-addon runtime chrome://extensions/content/ext-c-runtime.js
category webextension-scripts-addon test chrome://extensions/content/ext-c-test.js
category webextension-scripts-addon storage chrome://extensions/content/ext-c-storage.js
# schemas
category webextension-schemas alarms chrome://extensions/content/schemas/alarms.json
category webextension-schemas cookies chrome://extensions/content/schemas/cookies.json
category webextension-schemas downloads chrome://extensions/content/schemas/downloads.json
category webextension-schemas events chrome://extensions/content/schemas/events.json
category webextension-schemas extension chrome://extensions/content/schemas/extension.json
category webextension-schemas extension_types chrome://extensions/content/schemas/extension_types.json
category webextension-schemas i18n chrome://extensions/content/schemas/i18n.json
+#ifndef ANDROID
+category webextension-schemas identity chrome://extensions/content/schemas/identity.json
+#endif
category webextension-schemas idle chrome://extensions/content/schemas/idle.json
category webextension-schemas management chrome://extensions/content/schemas/management.json
category webextension-schemas native_host_manifest chrome://extensions/content/schemas/native_host_manifest.json
category webextension-schemas notifications chrome://extensions/content/schemas/notifications.json
category webextension-schemas runtime chrome://extensions/content/schemas/runtime.json
category webextension-schemas storage chrome://extensions/content/schemas/storage.json
category webextension-schemas test chrome://extensions/content/schemas/test.json
category webextension-schemas top_sites chrome://extensions/content/schemas/top_sites.json
--- a/toolkit/components/extensions/jar.mn
+++ b/toolkit/components/extensions/jar.mn
@@ -16,11 +16,14 @@ toolkit.jar:
content/extensions/ext-webRequest.js
content/extensions/ext-webNavigation.js
content/extensions/ext-runtime.js
content/extensions/ext-extension.js
content/extensions/ext-storage.js
content/extensions/ext-topSites.js
content/extensions/ext-c-backgroundPage.js
content/extensions/ext-c-extension.js
+#ifndef ANDROID
+ content/extensions/ext-c-identity.js
+#endif
content/extensions/ext-c-runtime.js
content/extensions/ext-c-storage.js
content/extensions/ext-c-test.js
--- a/toolkit/components/extensions/moz.build
+++ b/toolkit/components/extensions/moz.build
@@ -16,17 +16,17 @@ EXTRA_JS_MODULES += [
'ExtensionStorageSync.jsm',
'ExtensionUtils.jsm',
'LegacyExtensionsUtils.jsm',
'MessageChannel.jsm',
'NativeMessaging.jsm',
'Schemas.jsm',
]
-EXTRA_COMPONENTS += [
+EXTRA_PP_COMPONENTS += [
'extensions-toolkit.manifest',
]
TESTING_JS_MODULES += [
'ExtensionTestCommon.jsm',
'ExtensionXPCShellUtils.jsm',
]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/schemas/identity.json
@@ -0,0 +1,218 @@
+[
+ {
+ "namespace": "manifest",
+ "types": [
+ {
+ "$extend": "Permission",
+ "choices": [{
+ "type": "string",
+ "enum": [
+ "identity"
+ ]
+ }]
+ }
+ ]
+ },
+ {
+ "namespace": "identity",
+ "description": "Use the chrome.identity API to get OAuth2 access tokens. ",
+ "permissions": ["identity"],
+ "types": [
+ {
+ "id": "AccountInfo",
+ "type": "object",
+ "description": "An object encapsulating an OAuth account id.",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "A unique identifier for the account. This ID will not change for the lifetime of the account. "
+ }
+ }
+ }
+ ],
+ "functions": [
+ {
+ "name": "getAccounts",
+ "type": "function",
+ "unsupported": true,
+ "description": "Retrieves a list of AccountInfo objects describing the accounts present on the profile.",
+ "async": "callback",
+ "parameters": [
+ {
+ "name": "callback",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "results",
+ "type": "array",
+ "items": {
+ "$ref": "AccountInfo"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "getAuthToken",
+ "type": "function",
+ "unsupported": true,
+ "description": "Gets an OAuth2 access token using the client ID and scopes specified in the oauth2 section of manifest.json.",
+ "async": "callback",
+ "parameters": [
+ {
+ "name": "details",
+ "optional": true,
+ "type": "object",
+ "properties": {
+ "interactive": {
+ "optional": true,
+ "type": "boolean"
+ },
+ "account": {
+ "optional": true,
+ "$ref": "AccountInfo"
+ },
+ "scopes": {
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ {
+ "name": "callback",
+ "optional": true,
+ "type": "function",
+ "parameters": [
+ {
+ "name": "results",
+ "type": "array",
+ "items": {
+ "$ref": "AccountInfo"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "getProfileUserInfo",
+ "type": "function",
+ "unsupported": true,
+ "description": "Retrieves email address and obfuscated gaia id of the user signed into a profile.",
+ "async": "callback",
+ "parameters": [
+ {
+ "name": "callback",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "userinfo",
+ "type": "object",
+ "properties": {
+ "email": {"type": "string"},
+ "id": { "type": "string" }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "removeCachedAuthToken",
+ "type": "function",
+ "unsupported": true,
+ "description": "Removes an OAuth2 access token from the Identity API's token cache.",
+ "async": "callback",
+ "parameters": [
+ {
+ "name": "details",
+ "type": "object",
+ "properties": {
+ "token": {"type": "string"}
+ }
+ },
+ {
+ "name": "callback",
+ "optional": true,
+ "type": "function",
+ "parameters": [
+ {
+ "name": "userinfo",
+ "type": "object",
+ "properties": {
+ "email": {"type": "string"},
+ "id": { "type": "string" }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "launchWebAuthFlow",
+ "type": "function",
+ "description": "Starts an auth flow at the specified URL.",
+ "async": "callback",
+ "parameters": [
+ {
+ "name": "details",
+ "type": "object",
+ "properties": {
+ "url": {"type": "string"},
+ "interactive": {"type": "boolean", "optional": true}
+ }
+ },
+ {
+ "name": "callback",
+ "type": "function",
+ "parameters": [
+ {
+ "name": " responseUrl",
+ "type": "string",
+ "optional": true
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "getRedirectURL",
+ "type": "function",
+ "description": "Generates a redirect URL to be used in |launchWebAuthFlow|.",
+ "parameters": [
+ {
+ "name": " path",
+ "type": "string",
+ "optional": true,
+ "description": "The path appended to the end of the generated URL. "
+ }
+ ],
+ "returns": {
+ "string": "path"
+ }
+ }
+ ],
+ "events": [
+ {
+ "name": "onSignInChanged",
+ "unsupported": true,
+ "type": "function",
+ "description": "Fired when signin state changes for an account on the user's profile.",
+ "parameters": [
+ {
+ "name": "account",
+ "$ref": "AccountInfo"
+ },
+ {
+ "name": "signedIn",
+ "type": "boolean"
+ }
+ ]
+ }
+ ]
+ }
+]
--- a/toolkit/components/extensions/schemas/jar.mn
+++ b/toolkit/components/extensions/schemas/jar.mn
@@ -7,16 +7,19 @@ toolkit.jar:
content/extensions/schemas/alarms.json
content/extensions/schemas/cookies.json
content/extensions/schemas/downloads.json
content/extensions/schemas/events.json
content/extensions/schemas/experiments.json
content/extensions/schemas/extension.json
content/extensions/schemas/extension_types.json
content/extensions/schemas/i18n.json
+#ifndef ANDROID
+ content/extensions/schemas/identity.json
+#endif
content/extensions/schemas/idle.json
content/extensions/schemas/management.json
content/extensions/schemas/manifest.json
content/extensions/schemas/native_host_manifest.json
content/extensions/schemas/notifications.json
content/extensions/schemas/runtime.json
content/extensions/schemas/storage.json
content/extensions/schemas/test.json
--- a/toolkit/components/extensions/test/mochitest/chrome.ini
+++ b/toolkit/components/extensions/test/mochitest/chrome.ini
@@ -1,15 +1,17 @@
[DEFAULT]
support-files =
chrome_head.js
head.js
file_sample.html
webrequest_chromeworker.js
webrequest_test.jsm
+ oauth.html
+ redirect_auto.sjs
tags = webextensions
[test_chrome_ext_background_debug_global.html]
skip-if = (os == 'android') # android doesn't have devtools
[test_chrome_ext_background_page.html]
skip-if = (toolkit == 'android') # android doesn't have devtools
[test_chrome_ext_eventpage_warning.html]
[test_chrome_ext_contentscript_unrecognizedprop_warning.html]
@@ -23,11 +25,13 @@ skip-if = (os == 'android') # browser.ta
skip-if = os != "mac" && os != "linux"
[test_ext_cookies_expiry.html]
[test_ext_cookies_permissions.html]
[test_ext_cookies_containers.html]
[test_ext_jsversion.html]
[test_ext_schema.html]
[test_chrome_ext_storage_cleanup.html]
[test_chrome_ext_idle.html]
+[test_chrome_ext_identity.html]
+skip-if = os == 'android' # unsupported.
[test_chrome_ext_downloads_saveAs.html]
[test_chrome_ext_webrequest_background_events.html]
skip-if = os == 'android' # webrequest api unsupported (bug 1258975).
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/oauth.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script>
+ "use strict";
+
+ var url = new URL(location);
+ var end = new URL(url.searchParams.get("redirect_uri"));
+ end.searchParams.set("access_token", "here ya go");
+ location.href = end.href;
+ </script>
+</head>
+<body>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/redirect_auto.sjs
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+Components.utils.importGlobalProperties(["URLSearchParams", "URL"]);
+
+function handleRequest(request, response) {
+ let params = new URLSearchParams(request.queryString);
+ if (params.has("no_redirect")) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write("ok");
+ } else {
+ response.setStatusLine(request.httpVersion, 302, "Moved Temporarily");
+ let url = new URL(params.get("redirect_uri"));
+ url.searchParams.set("access_token", "here ya go");
+ response.setHeader("Location", url.href);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_identity.html
@@ -0,0 +1,200 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for WebExtension Identity</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="chrome_head.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({
+ set: [["extensions.webextensions.identity.redirectDomain", "example.com"]],
+ });
+});
+
+add_task(function* test_noPermission() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.test.assertEq(undefined, browser.identity, "No identity api without permission");
+ browser.test.sendMessage("done");
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("done");
+ yield extension.unload();
+});
+
+add_task(function* test_badAuthURI() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "permissions": [
+ "identity",
+ "https://example.com/",
+ ],
+ },
+ async background() {
+ await browser.test.assertRejects(browser.identity.launchWebAuthFlow({interactive: true, url: "foobar"}),
+ "details.url is invalid", "invalid param url");
+ browser.test.sendMessage("done");
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("done");
+ yield extension.unload();
+});
+
+
+add_task(function* test_badRequestURI() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "permissions": [
+ "identity",
+ "https://example.com/",
+ ],
+ },
+ async background() {
+ let base_uri = "https://example.com/chrome/toolkit/components/extensions/test/mochitest/";
+ let url = `${base_uri}?redirect_uri=badrobot}`;
+ await browser.test.assertRejects(browser.identity.launchWebAuthFlow({interactive: true, url}),
+ "redirect_uri is invalid", "invalid redirect url");
+ browser.test.sendMessage("done");
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("done");
+ yield extension.unload();
+});
+
+add_task(function* test_otherRedirectURL() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "permissions": [
+ "identity",
+ "https://example.com/",
+ ],
+ },
+ async background() {
+ let base_uri = "https://example.com/chrome/toolkit/components/extensions/test/mochitest/";
+ let url = `${base_uri}?redirect_uri=https://somesite.com/redirect`;
+ await browser.test.assertRejects(browser.identity.launchWebAuthFlow({interactive: false, url}),
+ "Requires user interaction", "alternate redirect_uri ok");
+ browser.test.sendMessage("done");
+ },
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("done");
+ yield extension.unload();
+});
+
+function background_launchWebAuthFlow(interactive, path, redirect = true) {
+ let expected_redirect = "https://35b64b676900f491c00e7f618d43f7040e88422e.example.com/identity_cb";
+ let base_uri = "https://example.com/chrome/toolkit/components/extensions/test/mochitest/";
+ let redirect_uri = browser.identity.getRedirectURL("/identity_cb");
+ browser.test.assertEq(expected_redirect, redirect_uri, "expected redirect uri matches hash");
+ let url = `${base_uri}${path}?redirect_uri=${encodeURIComponent(redirect_uri)}`;
+ if (!redirect) {
+ url = `${url}&no_redirect=1`;
+ }
+
+ browser.identity.launchWebAuthFlow({interactive, url}).then((redirectURL) => {
+ browser.test.assertTrue(redirectURL.startsWith(redirect_uri), `correct redirect url ${redirectURL}`);
+ if (redirect) {
+ let url = new URL(redirectURL);
+ browser.test.assertEq("here ya go", url.searchParams.get("access_token"), "Handled auto redirection");
+ }
+ browser.test.sendMessage("done");
+ }).catch((error) => {
+ if (redirect) {
+ browser.test.fail(error.message);
+ } else {
+ browser.test.assertEq("Requires user interaction", error.message, "Auth page loaded, interaction required.");
+ }
+ browser.test.sendMessage("done");
+ });
+}
+
+// Tests the situation where the oauth provider has already granted access and
+// simply redirects the oauth client to provide the access key or code.
+add_task(function* test_autoRedirect() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "applications": {
+ "gecko": {
+ "id": "identity@mozilla.org",
+ },
+ },
+ "permissions": [
+ "identity",
+ "https://example.com/",
+ ],
+ },
+ background: `(${background_launchWebAuthFlow})(false, "redirect_auto.sjs")`,
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("done");
+ yield extension.unload();
+});
+
+// Tests the situation where the oauth provider has not granted access and interactive=false
+add_task(function* test_noRedirect() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "applications": {
+ "gecko": {
+ "id": "identity@mozilla.org",
+ },
+ },
+ "permissions": [
+ "identity",
+ "https://example.com/",
+ ],
+ },
+ background: `(${background_launchWebAuthFlow})(false, "redirect_auto.sjs", false)`,
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("done");
+ yield extension.unload();
+});
+
+// Tests the situation where the oauth provider must show a window where
+// presumably the user interacts, then the redirect occurs and access key or
+// code is provided. We bypass any real interaction, but want the window to
+// open and result in a redirect.
+add_task(function* test_interaction() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "applications": {
+ "gecko": {
+ "id": "identity@mozilla.org",
+ },
+ },
+ "permissions": [
+ "identity",
+ "https://example.com/",
+ ],
+ },
+ background: `(${background_launchWebAuthFlow})(true, "oauth.html")`,
+ });
+
+ yield extension.startup();
+ yield extension.awaitMessage("done");
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>