Revision control

Copy as Markdown

Other Tools

/* 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/. */
// Creates calendars in various configurations (current and legacy) and performs
// requests in each of them to prove that OAuth2 authentication is working as expected.
//
// Previous versions used a separate client ID and/or stored credentials differently.
// All of those have been migrated to use the Thunderbird client ID and store credentials
// in the same format (origin as `oauth://{hostname}`, username as the username) except
// where no username was stored, in which case the calendar ID is used as the username.
var { CalDavCalendar } = ChromeUtils.importESModule("resource:///modules/CalDavCalendar.sys.mjs");
var { CalDavGenericRequest } = ChromeUtils.importESModule(
);
var { OAuth2TestUtils } = ChromeUtils.importESModule(
);
var LoginInfo = Components.Constructor(
"@mozilla.org/login-manager/loginInfo;1",
Ci.nsILoginInfo,
"init"
);
// This test is best left in one file, as it relies on the state of the server and the
// logins service to be consistent between tests, so setting a longer timeout to prevent
// the server from timing out and causing the tests to fail.
requestLongerTimeout(2);
// Ideal login info. This is what would be saved if you created a new calendar.
const ORIGIN = "oauth://test.test";
const SCOPE = "test_scope";
const USERNAME = "user";
const PASSWORD = "password";
const VALID_TOKEN = "refresh_token";
const defaultLogin = {
origin: ORIGIN,
scope: SCOPE,
username: USERNAME,
password: VALID_TOKEN,
};
const GOOGLE_SCOPE = "Google CalDAV v2";
const googleLogin = { ...defaultLogin, scope: GOOGLE_SCOPE };
let oAuth2Server;
add_setup(async function () {
Services.logins.removeAllLogins();
oAuth2Server = await OAuth2TestUtils.startServer();
});
/**
* Set a string pref for the given calendar.
*
* @param {string} calendarId
* @param {string} key
* @param {string} value
*/
function setPref(calendarId, key, value) {
Services.prefs.setStringPref(`calendar.registry.${calendarId}.${key}`, value);
}
/**
* @typedef {LoginData}
* @property {string} origin
* @property {string} scope
* @property {string} username
* @property {string} password
*/
/**
* Clear any existing saved logins and add the given ones.
*
* @param {LoginData[]} logins - Zero or more login data objects.
*/
async function setLogins(logins) {
Services.logins.removeAllLogins();
for (const { origin, scope, username, password } of logins) {
await Services.logins.addLoginAsync(
new LoginInfo(origin, null, scope, username, password, "", "")
);
}
}
/**
* Wait for a login prompt window to appear, and submit it.
*
* @param {string} expectedHint - Expected value of the login_hint URL param.
*/
async function handleOAuthDialog(expectedHint) {
const oAuthWindow = await OAuth2TestUtils.promiseOAuthWindow();
info("oauth2 window shown");
await SpecialPowers.spawn(
oAuthWindow.getBrowser(),
[{ expectedHint, expectedScope: SCOPE, username: USERNAME, password: PASSWORD }],
OAuth2TestUtils.submitOAuthLogin
);
}
/**
* Create a calendar with the given id, perform a request, and check that the correct
* authorisation header was used. If the user is required to re-authenticate with the provider,
* check that the new token is stored in the right place.
*
* @param {string} calendarId - ID of the new calendar
* @param {object} [newTokenDetails] - If given, re-authentication must happen.
* @param {string} [newTokenDetails.username] - The new token must be stored with this user name.
* @param {string} [newTokenDetails.reason] - The telemetry reason for the new token request.
*/
async function subtest(calendarId, newTokenDetails) {
Services.fog.testResetFOG();
const calendar = new CalDavCalendar();
calendar.id = calendarId;
const request = new CalDavGenericRequest(
calendar.wrappedJSObject.session,
calendar,
"GET",
Services.io.newURI(
)
);
const dialogPromise = newTokenDetails
? handleOAuthDialog(newTokenDetails.username)
: Promise.resolve();
const response = await request.commit();
await dialogPromise;
const headers = JSON.parse(response.text);
Assert.equal(headers.authorization, "Bearer access_token");
if (newTokenDetails) {
OAuth2TestUtils.checkTelemetry([
{
issuer: "test.test",
reason: newTokenDetails.reason,
result: "succeeded",
},
]);
} else {
OAuth2TestUtils.checkTelemetry([]);
}
}
/**
* Checks that the saved logins are as expected, and then clears all saved logins.
*
* @param {LoginData[]} expectedLogins - Zero or more login data objects.
*/
function checkAndClearLogins(expectedLogins) {
const logins = Services.logins.findLogins("", null, "");
Assert.equal(logins.length, expectedLogins.length);
for (let i = 0; i < logins.length; i++) {
Assert.equal(logins[i].origin, expectedLogins[i].origin);
Assert.equal(logins[i].httpRealm, expectedLogins[i].scope);
Assert.equal(logins[i].username, expectedLogins[i].username);
Assert.equal(logins[i].password, expectedLogins[i].password);
}
Services.logins.removeAllLogins();
OAuth2TestUtils.forgetObjects();
oAuth2Server.grantedScope = null;
}
// Test making a request when there is no matching token stored.
/** No token stored, no username or session ID set. */
add_task(async function testCalendarOAuth_id_none() {
const calendarId = "testCalendarOAuth_id_none";
await subtest(calendarId, { username: calendarId, reason: "no refresh token" });
checkAndClearLogins([{ ...defaultLogin, username: calendarId }]);
});
/** No token stored, session ID set. */
add_task(async function testCalendarOAuth_sessionId_none() {
const calendarId = "testCalendarOAuth_sessionId_none";
setPref(calendarId, "sessionId", "test_session");
await subtest(calendarId, { username: calendarId, reason: "no refresh token" });
checkAndClearLogins([{ ...defaultLogin, username: calendarId }]);
});
/** No token stored, username set. */
add_task(async function testCalendarOAuth_username_none() {
const calendarId = "testCalendarOAuth_username_none";
setPref(calendarId, "username", USERNAME);
await subtest(calendarId, { username: USERNAME, reason: "no refresh token" });
checkAndClearLogins([defaultLogin]);
});
// Test making a request when there IS a matching token, but the server rejects it.
// A new token is requested on failure.
/** Expired token stored with calendar ID. */
add_task(async function testCalendarOAuth_id_expired() {
const calendarId = "testCalendarOAuth_id_expired";
const logins = [
{
...defaultLogin,
username: calendarId,
password: "expired_token",
},
];
await setLogins(logins);
await subtest(calendarId, { username: calendarId, reason: "invalid grant" });
logins[0].password = VALID_TOKEN;
checkAndClearLogins(logins);
});
/** Expired token stored with calendar ID, username set. The new token is stored with the username. */
add_task(async function testCalendarOAuth_id_and_username_expired() {
const calendarId = "testCalendarOAuth_id_and_username_expired";
const logins = [
{
...defaultLogin,
username: calendarId,
password: "expired_token",
},
];
setPref(calendarId, "username", USERNAME);
await setLogins(logins);
await subtest(calendarId, { username: USERNAME, reason: "no refresh token" });
checkAndClearLogins([logins[0], defaultLogin]);
});
/** Expired token stored with username. */
add_task(async function testCalendarOAuth_username_expired() {
const calendarId = "testCalendarOAuth_username_expired";
const logins = [
{
...defaultLogin,
password: "expired_token",
},
];
setPref(calendarId, "username", USERNAME);
await setLogins(logins);
await subtest(calendarId, { username: USERNAME, reason: "invalid grant" });
logins[0].password = VALID_TOKEN;
checkAndClearLogins(logins);
});
// Test making a request with a valid token, using Lightning's client ID and secret.
/** Valid token stored with calendar ID. */
add_task(async function testCalendarOAuth_id_valid() {
const calendarId = "testCalendarOAuth_id_valid";
const logins = [{ ...googleLogin, origin: `oauth:${calendarId}`, username: calendarId }];
await setLogins(logins);
await subtest(calendarId, { username: calendarId, reason: "no refresh token" });
checkAndClearLogins([logins[0], { ...defaultLogin, username: calendarId }]);
});
/** Valid token stored with session ID. */
add_task(async function testCalendarOAuth_sessionId_valid() {
const calendarId = "testCalendarOAuth_sessionId_valid";
const logins = [{ ...googleLogin, origin: "oauth:test_session", username: "test_session" }];
setPref(calendarId, "sessionId", "test_session");
await setLogins(logins);
await subtest(calendarId, { username: calendarId, reason: "no refresh token" });
checkAndClearLogins([logins[0], { ...defaultLogin, username: calendarId }]);
});
/** Valid token stored with calendar ID, username set. */
add_task(async function testCalendarOAuth_username_valid() {
const calendarId = "testCalendarOAuth_username_valid";
const logins = [{ ...googleLogin, origin: `oauth:${calendarId}`, username: calendarId }];
setPref(calendarId, "username", USERNAME);
await setLogins(logins);
await subtest(calendarId, { username: USERNAME, reason: "no refresh token" });
checkAndClearLogins([logins[0], defaultLogin]);
});
// Test making a request with a valid token, using Thunderbird's client ID and secret.
/** Valid token stored with calendar ID. */
add_task(async function testCalendarOAuthTB_id_valid() {
const calendarId = "testCalendarOAuthTB_id_valid";
const logins = [{ ...defaultLogin, username: calendarId }];
await setLogins(logins);
await subtest(calendarId);
checkAndClearLogins(logins);
});
/** Valid token stored with session ID. */
add_task(async function testCalendarOAuthTB_sessionId_valid() {
const calendarId = "testCalendarOAuthTB_sessionId_valid";
const logins = [{ ...defaultLogin, username: "test_session" }];
setPref(calendarId, "sessionId", "test_session");
await setLogins(logins);
await subtest(calendarId, { username: calendarId, reason: "no refresh token" });
checkAndClearLogins([logins[0], { ...defaultLogin, username: calendarId }]);
});
/** Valid token stored with calendar ID, username set. */
add_task(async function testCalendarOAuthTB_username_valid() {
const calendarId = "testCalendarOAuthTB_username_valid";
const logins = [{ ...defaultLogin, username: calendarId }];
setPref(calendarId, "username", USERNAME);
await setLogins(logins);
await subtest(calendarId, { username: USERNAME, reason: "no refresh token" });
checkAndClearLogins([logins[0], defaultLogin]);
});
/** Valid token stored with username, exact scope. */
add_task(async function testCalendarOAuthTB_username_validSingle() {
const calendarId = "testCalendarOAuthTB_username_validSingle";
const logins = [
{ ...defaultLogin },
{ ...defaultLogin, scope: "other_scope", password: "other_refresh_token" },
];
setPref(calendarId, "username", USERNAME);
await setLogins(logins);
await subtest(calendarId);
checkAndClearLogins(logins);
});
/** Valid token stored with username, many scopes. */
add_task(async function testCalendarOAuthTB_username_validMultiple() {
const calendarId = "testCalendarOAuthTB_username_validMultiple";
const logins = [{ ...defaultLogin, scope: "scope test_scope other_scope" }];
setPref(calendarId, "username", USERNAME);
await setLogins(logins);
await subtest(calendarId);
checkAndClearLogins(logins);
});