Bug 1878172 - Test getting messages with OAuth2 authentication. r=mkmelin
Differential Revision: https://phabricator.services.mozilla.com/D202534 --HG-- extra : rebase_source : f6b01674afd6ffb2054cb924e7c3408acb9c906e extra : amend_source : 9af81d0364e40cf1ec578a08589f117acf4291bc
This commit is contained in:
Родитель
5b220cc144
Коммит
258f4d8fe6
|
@ -2,10 +2,7 @@
|
|||
* 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/. */
|
||||
|
||||
import {
|
||||
Pop3Daemon,
|
||||
POP3_RFC5034_handler,
|
||||
} from "resource://testing-common/mailnews/Pop3d.sys.mjs";
|
||||
import * as Pop3D from "resource://testing-common/mailnews/Pop3d.sys.mjs";
|
||||
import { nsMailServer } from "resource://testing-common/mailnews/Maild.sys.mjs";
|
||||
|
||||
/**
|
||||
|
@ -15,15 +12,18 @@ export class POP3Server {
|
|||
constructor(testScope, options = {}) {
|
||||
this.testScope = testScope;
|
||||
this.options = options;
|
||||
this.open();
|
||||
this.open(options.handler);
|
||||
}
|
||||
|
||||
open() {
|
||||
open(handlerName = "RFC5034") {
|
||||
if (!this.daemon) {
|
||||
this.daemon = new Pop3Daemon();
|
||||
this.daemon = new Pop3D.Pop3Daemon();
|
||||
}
|
||||
this.server = new nsMailServer(daemon => {
|
||||
const handler = new POP3_RFC5034_handler(daemon, this.options);
|
||||
const handler = new Pop3D[`POP3_${handlerName}_handler`](
|
||||
daemon,
|
||||
this.options
|
||||
);
|
||||
if (this.options.offerStartTLS) {
|
||||
// List startTLS as a capability, even though we don't support it.
|
||||
handler.kCapabilities.push("STLS");
|
||||
|
|
|
@ -34,6 +34,8 @@ skip-if = os == 'mac' && debug # Fails almost every time. Unsure why.
|
|||
[browser_getMessages_certError.js]
|
||||
[browser_getMessages_connectionError.js]
|
||||
[browser_getMessages_deferredAccount.js]
|
||||
[browser_getMessages_oAuth2.js]
|
||||
tags = oauth
|
||||
[browser_getMessages_offline.js]
|
||||
[browser_goMenu.js]
|
||||
skip-if = os == 'mac'
|
||||
|
|
|
@ -0,0 +1,268 @@
|
|||
/* 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/. */
|
||||
|
||||
/**
|
||||
* Tests fetching mail with OAuth2 authentication, including the dialog
|
||||
* windows that uses.
|
||||
*/
|
||||
|
||||
const { MessageGenerator } = ChromeUtils.importESModule(
|
||||
"resource://testing-common/mailnews/MessageGenerator.sys.mjs"
|
||||
);
|
||||
const { OAuth2TestUtils } = ChromeUtils.importESModule(
|
||||
"resource://testing-common/mailnews/OAuth2TestUtils.sys.mjs"
|
||||
);
|
||||
const { ServerTestUtils } = ChromeUtils.importESModule(
|
||||
"resource://testing-common/mailnews/ServerTestUtils.sys.mjs"
|
||||
);
|
||||
|
||||
const generator = new MessageGenerator();
|
||||
let localAccount, localRootFolder;
|
||||
let imapServer, imapAccount, imapRootFolder, imapInbox;
|
||||
let pop3Server, pop3Account, pop3RootFolder, pop3Inbox;
|
||||
|
||||
const allInboxes = [];
|
||||
|
||||
const about3Pane = document.getElementById("tabmail").currentAbout3Pane;
|
||||
const getMessagesButton = about3Pane.document.getElementById(
|
||||
"folderPaneGetMessages"
|
||||
);
|
||||
const getMessagesContext = about3Pane.document.getElementById(
|
||||
"folderPaneGetMessagesContext"
|
||||
);
|
||||
|
||||
add_setup(async function () {
|
||||
Services.prefs.setStringPref("mailnews.oauth.loglevel", "Debug");
|
||||
Services.prefs.setBoolPref("signon.rememberSignons", true);
|
||||
|
||||
localAccount = MailServices.accounts.createLocalMailAccount();
|
||||
localRootFolder = localAccount.incomingServer.rootFolder;
|
||||
|
||||
[imapServer, pop3Server] = await ServerTestUtils.createServers(this, [
|
||||
ServerTestUtils.serverDefs.imap.oAuth,
|
||||
ServerTestUtils.serverDefs.pop3.oAuth,
|
||||
]);
|
||||
|
||||
imapAccount = MailServices.accounts.createAccount();
|
||||
imapAccount.addIdentity(MailServices.accounts.createIdentity());
|
||||
imapAccount.incomingServer = MailServices.accounts.createIncomingServer(
|
||||
"user",
|
||||
"test.test",
|
||||
"imap"
|
||||
);
|
||||
imapAccount.incomingServer.prettyName = "IMAP Account";
|
||||
imapAccount.incomingServer.port = 143;
|
||||
imapAccount.incomingServer.authMethod = Ci.nsMsgAuthMethod.OAuth2;
|
||||
Services.prefs.getStringPref(
|
||||
"mail.server." + imapAccount.incomingServer.key + ".oauth2.issuer",
|
||||
"mochi.test"
|
||||
);
|
||||
Services.prefs.getStringPref(
|
||||
"mail.server." + imapAccount.incomingServer.key + ".oauth2.scope",
|
||||
"test_scope"
|
||||
);
|
||||
imapRootFolder = imapAccount.incomingServer.rootFolder;
|
||||
imapInbox = imapRootFolder.getFolderWithFlags(Ci.nsMsgFolderFlags.Inbox);
|
||||
allInboxes.push(imapInbox);
|
||||
|
||||
pop3Account = MailServices.accounts.createAccount();
|
||||
pop3Account.addIdentity(MailServices.accounts.createIdentity());
|
||||
pop3Account.incomingServer = MailServices.accounts.createIncomingServer(
|
||||
"user",
|
||||
"test.test",
|
||||
"pop3"
|
||||
);
|
||||
pop3Account.incomingServer.prettyName = "POP3 Account";
|
||||
pop3Account.incomingServer.port = 110;
|
||||
pop3Account.incomingServer.authMethod = Ci.nsMsgAuthMethod.OAuth2;
|
||||
Services.prefs.getStringPref(
|
||||
"mail.server." + pop3Account.incomingServer.key + ".oauth2.issuer",
|
||||
"mochi.test"
|
||||
);
|
||||
Services.prefs.getStringPref(
|
||||
"mail.server." + pop3Account.incomingServer.key + ".oauth2.scope",
|
||||
"test_scope"
|
||||
);
|
||||
pop3RootFolder = pop3Account.incomingServer.rootFolder;
|
||||
pop3Inbox = pop3RootFolder.getFolderWithFlags(Ci.nsMsgFolderFlags.Inbox);
|
||||
allInboxes.push(pop3Inbox);
|
||||
|
||||
OAuth2TestUtils.startServer(this);
|
||||
|
||||
registerCleanupFunction(async () => {
|
||||
MailServices.accounts.removeAccount(localAccount, false);
|
||||
MailServices.accounts.removeAccount(imapAccount, false);
|
||||
MailServices.accounts.removeAccount(pop3Account, false);
|
||||
|
||||
Services.logins.removeAllLogins();
|
||||
Services.prefs.clearUserPref("mailnews.oauth.loglevel");
|
||||
Services.prefs.clearUserPref("signon.rememberSignons");
|
||||
});
|
||||
});
|
||||
|
||||
async function addMessagesToServer(type) {
|
||||
if (type == "imap") {
|
||||
await imapServer.addMessages(imapInbox, generator.makeMessages({}), false);
|
||||
} else if (type == "pop3") {
|
||||
await pop3Server.addMessages(generator.makeMessages({}));
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchMessages(inbox) {
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
getMessagesButton,
|
||||
{ type: "contextmenu" },
|
||||
about3Pane
|
||||
);
|
||||
await BrowserTestUtils.waitForPopupEvent(getMessagesContext, "shown");
|
||||
getMessagesContext.activateItem(
|
||||
getMessagesContext.querySelector(`[data-server-key="${inbox.server.key}"]`)
|
||||
);
|
||||
await BrowserTestUtils.waitForPopupEvent(getMessagesContext, "hidden");
|
||||
}
|
||||
|
||||
async function waitForMessages(inbox) {
|
||||
await TestUtils.waitForCondition(
|
||||
() => inbox.getNumUnread(false) == 10 && inbox.numPendingUnread == 0,
|
||||
`waiting for new ${inbox.server.type} messages to be received`
|
||||
);
|
||||
await promiseServerIdle(inbox.server);
|
||||
info(`${inbox.server.type} messages received`);
|
||||
|
||||
inbox.markAllMessagesRead(window.msgWindow);
|
||||
await promiseServerIdle(inbox.server);
|
||||
await TestUtils.waitForCondition(
|
||||
() => inbox.getNumUnread(false) == 0 && inbox.numPendingUnread == 0,
|
||||
`waiting for ${inbox.server.type} messages to be marked read`
|
||||
);
|
||||
info(`${inbox.server.type} messages marked as read`);
|
||||
}
|
||||
|
||||
async function handleOAuthDialog() {
|
||||
const oAuthWindow = await OAuth2TestUtils.promiseOAuthWindow();
|
||||
info("oauth2 window shown");
|
||||
await SpecialPowers.spawn(
|
||||
oAuthWindow.getBrowser(),
|
||||
[{ expectedHint: "user", username: "user", password: "password" }],
|
||||
OAuth2TestUtils.submitOAuthLogin
|
||||
);
|
||||
}
|
||||
|
||||
function checkSavedPassword(inbox) {
|
||||
const logins = Services.logins.findLogins("oauth://test.test", "", "");
|
||||
Assert.equal(
|
||||
logins.length,
|
||||
1,
|
||||
"there should be a saved password for this server"
|
||||
);
|
||||
Assert.equal(logins[0].origin, "oauth://test.test", "login origin");
|
||||
Assert.equal(logins[0].formActionOrigin, null, "login formActionOrigin");
|
||||
Assert.equal(logins[0].httpRealm, "test_scope", "login httpRealm");
|
||||
Assert.equal(logins[0].username, "user", "login username");
|
||||
Assert.equal(logins[0].password, "refresh_token", "login password");
|
||||
Assert.equal(logins[0].usernameField, "", "login usernameField");
|
||||
Assert.equal(logins[0].passwordField, "", "login passwordField");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests getting messages when there is no access token and no refresh token.
|
||||
*/
|
||||
add_task(async function testNoTokens() {
|
||||
for (const inbox of allInboxes) {
|
||||
info(`getting messages for ${inbox.server.type} inbox with no tokens`);
|
||||
await addMessagesToServer(inbox.server.type);
|
||||
|
||||
const oAuthPromise = handleOAuthDialog();
|
||||
await fetchMessages(inbox);
|
||||
await oAuthPromise;
|
||||
await waitForMessages(inbox);
|
||||
|
||||
// TODO: check this does NOT hit the oauth server
|
||||
await addMessagesToServer(inbox.server.type);
|
||||
await fetchMessages(inbox);
|
||||
await waitForMessages(inbox);
|
||||
|
||||
checkSavedPassword(inbox);
|
||||
Services.logins.removeAllLogins();
|
||||
|
||||
await promiseServerIdle(inbox.server);
|
||||
inbox.server.closeCachedConnections();
|
||||
OAuth2TestUtils.forgetObjects();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests that with a saved refresh token, but no access token, a new access token is requested.
|
||||
*/
|
||||
add_task(async function testNoAccessToken() {
|
||||
const loginInfo = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
|
||||
Ci.nsILoginInfo
|
||||
);
|
||||
loginInfo.init(
|
||||
"oauth://test.test",
|
||||
null,
|
||||
"test_scope",
|
||||
"user",
|
||||
"refresh_token",
|
||||
"",
|
||||
""
|
||||
);
|
||||
await Services.logins.addLoginAsync(loginInfo);
|
||||
|
||||
for (const inbox of allInboxes) {
|
||||
info(
|
||||
`getting messages for ${inbox.server.type} inbox with a refresh token but no access token`
|
||||
);
|
||||
await addMessagesToServer(inbox.server.type);
|
||||
|
||||
await fetchMessages(inbox);
|
||||
await waitForMessages(inbox);
|
||||
|
||||
checkSavedPassword(inbox);
|
||||
await promiseServerIdle(inbox.server);
|
||||
inbox.server.closeCachedConnections();
|
||||
|
||||
OAuth2TestUtils.forgetObjects();
|
||||
}
|
||||
|
||||
Services.logins.removeAllLogins();
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests that with a bad saved refresh token, new tokens are requested.
|
||||
*/
|
||||
add_task(async function testBadRefreshToken() {
|
||||
for (const inbox of allInboxes) {
|
||||
const loginInfo = Cc[
|
||||
"@mozilla.org/login-manager/loginInfo;1"
|
||||
].createInstance(Ci.nsILoginInfo);
|
||||
loginInfo.init(
|
||||
"oauth://test.test",
|
||||
null,
|
||||
"test_scope",
|
||||
"user",
|
||||
"old_refresh_token",
|
||||
"",
|
||||
""
|
||||
);
|
||||
await Services.logins.addLoginAsync(loginInfo);
|
||||
|
||||
info(
|
||||
`getting messages for ${inbox.server.type} inbox with a bad refresh token`
|
||||
);
|
||||
await addMessagesToServer(inbox.server.type);
|
||||
|
||||
const oAuthPromise = handleOAuthDialog();
|
||||
await fetchMessages(inbox);
|
||||
await oAuthPromise;
|
||||
await waitForMessages(inbox);
|
||||
|
||||
checkSavedPassword(inbox);
|
||||
await promiseServerIdle(inbox.server);
|
||||
inbox.server.closeCachedConnections();
|
||||
|
||||
OAuth2TestUtils.forgetObjects();
|
||||
Services.logins.removeAllLogins();
|
||||
}
|
||||
});
|
|
@ -25,10 +25,9 @@ var alertHook = {
|
|||
},
|
||||
|
||||
get alertService() {
|
||||
delete this.alertService;
|
||||
return (this.alertService = Cc["@mozilla.org/alerts-service;1"].getService(
|
||||
Ci.nsIAlertsService
|
||||
));
|
||||
// Don't store a reference to the alerts service, as it can be swapped out
|
||||
// during tests.
|
||||
return Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);
|
||||
},
|
||||
|
||||
get brandShortName() {
|
||||
|
|
|
@ -78,6 +78,7 @@ var kHostnames = new Map([
|
|||
|
||||
// For testing purposes.
|
||||
["mochi.test", ["mochi.test", "test_scope"]],
|
||||
["test.test", ["test.test", "test_scope"]],
|
||||
]);
|
||||
|
||||
/**
|
||||
|
@ -192,6 +193,16 @@ var kIssuers = new Map([
|
|||
redirectionEndpoint: "https://localhost",
|
||||
},
|
||||
],
|
||||
[
|
||||
"test.test",
|
||||
{
|
||||
clientId: "test_client_id",
|
||||
clientSecret: "test_secret",
|
||||
authorizationEndpoint: "http://oauth.test.test/form",
|
||||
tokenEndpoint: "http://oauth.test.test/token",
|
||||
redirectionEndpoint: "https://localhost",
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
/**
|
||||
|
|
|
@ -66,6 +66,7 @@ TESTING_JS_MODULES.mailnews += [
|
|||
"test/resources/MessageGenerator.sys.mjs",
|
||||
"test/resources/MessageInjection.sys.mjs",
|
||||
"test/resources/NetworkTestUtils.sys.mjs",
|
||||
"test/resources/OAuth2TestUtils.sys.mjs",
|
||||
"test/resources/PromiseTestUtils.sys.mjs",
|
||||
"test/resources/SmimeUtils.sys.mjs",
|
||||
]
|
||||
|
|
|
@ -2475,6 +2475,29 @@ export var IMAP_RFC2195_extension = {
|
|||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements XOAUTH2 authentication.
|
||||
*/
|
||||
export var IMAP_OAUTH2_extension = {
|
||||
kAuthSchemes: ["XOAUTH2"],
|
||||
|
||||
preload(handler) {
|
||||
handler._kAuthSchemeStartFunction.XOAUTH2 = this.authXOAUTH2Start;
|
||||
},
|
||||
|
||||
authXOAUTH2Start(lineRest) {
|
||||
const [user, auth] = atob(lineRest).split("\u0001");
|
||||
if (
|
||||
user == `user=${this.kUsername}` &&
|
||||
auth == `auth=Bearer ${this.kPassword}`
|
||||
) {
|
||||
this._state = IMAP_STATE_AUTHED;
|
||||
return "OK Yeah, that's the right access token.";
|
||||
}
|
||||
return "BAD Yeah, nah, that's the wrong access token.";
|
||||
},
|
||||
};
|
||||
|
||||
// FETCH BODYSTRUCTURE
|
||||
function bodystructure(msg, extension) {
|
||||
if (!msg || msg == "") {
|
||||
|
|
|
@ -460,3 +460,33 @@ export class POP3_RFC5034_handler extends POP3_RFC2449_handler {
|
|||
return "-ERR Wrong username or password, crook!";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements XOAUTH2 authentication.
|
||||
*/
|
||||
export class POP3_OAUTH2_handler extends POP3_RFC5034_handler {
|
||||
kAuthSchemes = ["XOAUTH2"];
|
||||
|
||||
constructor(daemon, options) {
|
||||
super(daemon, options);
|
||||
this._kAuthSchemeStartFunction.XOAUTH2 = this.authXOAUTH2Start;
|
||||
}
|
||||
|
||||
authXOAUTH2Start(lineRest) {
|
||||
this._nextAuthFunction = this.authXOAUTH2Cred;
|
||||
this._multiline = true;
|
||||
|
||||
return "+";
|
||||
}
|
||||
authXOAUTH2Cred(lineRest) {
|
||||
const [user, auth] = atob(lineRest).split("\u0001");
|
||||
if (
|
||||
user == `user=${this.kUsername}` &&
|
||||
auth == `auth=Bearer ${this.kPassword}`
|
||||
) {
|
||||
this._state = kStateTransaction;
|
||||
return "+OK Yeah, that's the right access token.";
|
||||
}
|
||||
return "-BAD Yeah, nah, that's the wrong access token.";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -161,6 +161,12 @@ const serverDefs = {
|
|||
hostname: "expired.test.test",
|
||||
port: 993,
|
||||
},
|
||||
oAuth: {
|
||||
type: "imap",
|
||||
baseOptions: { extensions: ["OAUTH2"], password: "access_token" },
|
||||
hostname: "test.test",
|
||||
port: 143,
|
||||
},
|
||||
},
|
||||
pop3: {
|
||||
plain: {
|
||||
|
@ -199,6 +205,16 @@ const serverDefs = {
|
|||
hostname: "expired.test.test",
|
||||
port: 995,
|
||||
},
|
||||
oAuth: {
|
||||
type: "pop3",
|
||||
baseOptions: {
|
||||
handler: ["OAUTH2"],
|
||||
username: "user",
|
||||
password: "access_token",
|
||||
},
|
||||
hostname: "test.test",
|
||||
port: 110,
|
||||
},
|
||||
},
|
||||
smtp: {
|
||||
plain: {
|
||||
|
|
|
@ -0,0 +1,259 @@
|
|||
/* 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/. */
|
||||
|
||||
/**
|
||||
* Utils for testing interactions with OAuth2 authentication servers.
|
||||
*/
|
||||
|
||||
import { BrowserTestUtils } from "resource://testing-common/BrowserTestUtils.sys.mjs";
|
||||
import { CommonUtils } from "resource://services-common/utils.sys.mjs";
|
||||
import { HttpServer, HTTP_405 } from "resource://testing-common/httpd.sys.mjs";
|
||||
import { NetworkTestUtils } from "resource://testing-common/mailnews/NetworkTestUtils.sys.mjs";
|
||||
|
||||
const { OAuth2Module } = ChromeUtils.import(
|
||||
"resource:///modules/OAuth2Module.jsm"
|
||||
);
|
||||
|
||||
const validCodes = new Set();
|
||||
|
||||
export const OAuth2TestUtils = {
|
||||
/**
|
||||
* Start an OAuth2 server and add it to the proxy at oauth.test.test:80.
|
||||
*
|
||||
* @param {object} testScope - The JS scope for the current test, so
|
||||
* `registerCleanupFunction` can be used.
|
||||
*/
|
||||
startServer(testScope) {
|
||||
const oAuth2Server = new OAuth2Server(testScope);
|
||||
oAuth2Server.httpServer.identity.add("http", "oauth.test.test", 80);
|
||||
NetworkTestUtils.configureProxy(
|
||||
"oauth.test.test",
|
||||
80,
|
||||
oAuth2Server.httpServer.identity.primaryPort
|
||||
);
|
||||
testScope.registerCleanupFunction(() => {
|
||||
NetworkTestUtils.clearProxy();
|
||||
});
|
||||
return oAuth2Server;
|
||||
},
|
||||
|
||||
/**
|
||||
* Forget any `OAuth2` objects remembered by OAuth2Module.jsm
|
||||
*/
|
||||
forgetObjects() {
|
||||
OAuth2Module._forgetObjects();
|
||||
},
|
||||
|
||||
/**
|
||||
* Waits for a login prompt window to appear and load.
|
||||
*
|
||||
* @returns {Window}
|
||||
*/
|
||||
async promiseOAuthWindow() {
|
||||
const oAuthWindow = await BrowserTestUtils.domWindowOpenedAndLoaded(
|
||||
undefined,
|
||||
win =>
|
||||
win.document.documentURI ==
|
||||
"chrome://messenger/content/browserRequest.xhtml"
|
||||
);
|
||||
const oAuthBrowser = oAuthWindow.getBrowser();
|
||||
if (
|
||||
oAuthBrowser.webProgress?.isLoadingDocument ||
|
||||
oAuthBrowser.currentURI.spec == "about:blank"
|
||||
) {
|
||||
await BrowserTestUtils.browserLoaded(oAuthBrowser);
|
||||
}
|
||||
return oAuthWindow;
|
||||
},
|
||||
|
||||
/**
|
||||
* Callback function to run in a login prompt window. Note: This function is
|
||||
* serialized by SpecialPowers, so it can't use function shorthand.
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {string} [options.expectedHint] - If given, the login_hint URL parameter
|
||||
* will be checked.
|
||||
* @param {string} options.username - The username to use to log in.
|
||||
* @param {string} options.password - The password to use to log in.
|
||||
*/
|
||||
submitOAuthLogin: ({ expectedHint, username, password }) => {
|
||||
/* globals content, EventUtils */
|
||||
const searchParams = new URL(content.location).searchParams;
|
||||
Assert.equal(
|
||||
searchParams.get("response_type"),
|
||||
"code",
|
||||
"request response_type"
|
||||
);
|
||||
Assert.equal(
|
||||
searchParams.get("client_id"),
|
||||
"test_client_id",
|
||||
"request client_id"
|
||||
);
|
||||
Assert.equal(
|
||||
searchParams.get("redirect_uri"),
|
||||
"https://localhost",
|
||||
"request redirect_uri"
|
||||
);
|
||||
Assert.equal(searchParams.get("scope"), "test_scope", "request scope");
|
||||
Assert.equal(
|
||||
searchParams.get("login_hint"),
|
||||
expectedHint,
|
||||
"request login_hint"
|
||||
);
|
||||
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
content.document.querySelector(`input[name="username"]`),
|
||||
{},
|
||||
content
|
||||
);
|
||||
EventUtils.sendString(username, content);
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
content.document.querySelector(`input[name="password"]`),
|
||||
{},
|
||||
content
|
||||
);
|
||||
EventUtils.sendString(password, content);
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
content.document.querySelector(`input[type="submit"]`),
|
||||
{},
|
||||
content
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
class OAuth2Server {
|
||||
username = "user";
|
||||
password = "password";
|
||||
accessToken = "access_token";
|
||||
refreshToken = "refresh_token";
|
||||
expiry = null;
|
||||
|
||||
constructor(testScope) {
|
||||
this.httpServer = new HttpServer();
|
||||
this.httpServer.registerPathHandler("/form", this.formHandler.bind(this));
|
||||
this.httpServer.registerPathHandler(
|
||||
"/authorize",
|
||||
this.authorizeHandler.bind(this)
|
||||
);
|
||||
this.httpServer.registerPathHandler("/token", this.tokenHandler.bind(this));
|
||||
this.httpServer.start(-1);
|
||||
|
||||
const port = this.httpServer.identity.primaryPort;
|
||||
dump(`OAuth2 server at localhost:${port} opened\n`);
|
||||
|
||||
testScope.registerCleanupFunction(() => {
|
||||
this.httpServer.stop();
|
||||
dump(`OAuth2 server at localhost:${port} closed\n`);
|
||||
});
|
||||
}
|
||||
|
||||
formHandler(request, response) {
|
||||
if (request.method != "GET") {
|
||||
throw HTTP_405;
|
||||
}
|
||||
const params = new URLSearchParams(request.queryString);
|
||||
this._formHandler(response, params.get("redirect_uri"));
|
||||
}
|
||||
|
||||
_formHandler(response, redirectUri) {
|
||||
response.setHeader("Content-Type", "text/html", false);
|
||||
response.write(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Log in to test.test</title>
|
||||
</head>
|
||||
<body>
|
||||
<form action="/authorize" method="post">
|
||||
<input type="text" name="redirect_uri" readonly="readonly" value="${redirectUri}" />
|
||||
<input type="text" name="username" />
|
||||
<input type="password" name="password" />
|
||||
<input type="submit" />
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
}
|
||||
|
||||
authorizeHandler(request, response) {
|
||||
if (request.method != "POST") {
|
||||
throw HTTP_405;
|
||||
}
|
||||
|
||||
const input = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
|
||||
const params = new URLSearchParams(input);
|
||||
|
||||
if (
|
||||
params.get("username") != this.username ||
|
||||
params.get("password") != this.password
|
||||
) {
|
||||
this._formHandler(response, params.get("redirect_uri"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a unique code. It will become invalid after the first use.
|
||||
const bytes = new Uint8Array(12);
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
bytes[i] = Math.floor(Math.random() * 255);
|
||||
}
|
||||
const code = ChromeUtils.base64URLEncode(bytes, { pad: false });
|
||||
validCodes.add(code);
|
||||
|
||||
const url = new URL(params.get("redirect_uri"));
|
||||
url.searchParams.set("code", code);
|
||||
|
||||
response.setStatusLine(request.httpVersion, 303, "Redirected");
|
||||
response.setHeader("Location", url.href);
|
||||
}
|
||||
|
||||
tokenHandler(request, response) {
|
||||
if (request.method != "POST") {
|
||||
throw HTTP_405;
|
||||
}
|
||||
|
||||
const stream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
|
||||
Ci.nsIBinaryInputStream
|
||||
);
|
||||
stream.setInputStream(request.bodyInputStream);
|
||||
const input = stream.readBytes(request.bodyInputStream.available());
|
||||
const params = new URLSearchParams(input);
|
||||
|
||||
const goodRequest =
|
||||
params.get("client_id") == "test_client_id" &&
|
||||
params.get("client_secret") == "test_secret";
|
||||
const grantType = params.get("grant_type");
|
||||
const code = params.get("code");
|
||||
const data = {};
|
||||
|
||||
if (
|
||||
goodRequest &&
|
||||
grantType == "authorization_code" &&
|
||||
code &&
|
||||
validCodes.has(code)
|
||||
) {
|
||||
// Authorisation just happened.
|
||||
validCodes.delete(code);
|
||||
data.access_token = this.accessToken;
|
||||
data.refresh_token = this.refreshToken;
|
||||
} else if (
|
||||
goodRequest &&
|
||||
grantType == "refresh_token" &&
|
||||
params.get("refresh_token") == this.refreshToken
|
||||
) {
|
||||
// Client provided a valid refresh token.
|
||||
data.access_token = this.accessToken;
|
||||
} else {
|
||||
response.setStatusLine("1.1", 400, "Bad Request");
|
||||
data.error = "invalid_grant";
|
||||
}
|
||||
|
||||
if (data.accessToken && this.expiry !== null) {
|
||||
data.expires_in = this.expiry;
|
||||
}
|
||||
|
||||
response.setHeader("Content-Type", "application/json", false);
|
||||
response.write(JSON.stringify(data));
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче