Bug 1632557 - Add pref and logic for direct use of session tokens to provision OAuth tokens r=rfkelly

Differential Revision: https://phabricator.services.mozilla.com/D75204
This commit is contained in:
Vlad Filippov 2020-05-20 22:06:35 +00:00
Родитель a6395f4f1f
Коммит de6e0f725b
5 изменённых файлов: 194 добавлений и 8 удалений

Просмотреть файл

@ -1447,6 +1447,9 @@ pref("identity.sync.tokenserver.uri", "https://token.services.mozilla.com/1.0/sy
pref("identity.sync.useOAuthForSyncToken", false);
#endif
// Using session tokens to fetch OAuth tokens
pref("identity.fxaccounts.useSessionTokensForOAuth", true);
// Auto-config URL for FxA self-hosters, makes an HTTP request to
// [identity.fxaccounts.autoconfig.uri]/.well-known/fxa-client-configuration
// This is now the prefered way of pointing to a custom FxA server, instead

Просмотреть файл

@ -116,6 +116,12 @@ XPCOMUtils.defineLazyPreferenceGetter(
true
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"USE_SESSION_TOKENS_FOR_OAUTH",
"identity.fxaccounts.useSessionTokensForOAuth"
);
// An AccountState object holds all state related to one specific account.
// It is considered "private" to the FxAccounts modules.
// Only one AccountState is ever "current" in the FxAccountsInternal object -
@ -1577,8 +1583,9 @@ FxAccountsInternal.prototype = {
* @private
*/
async _doTokenFetch(scopeString, ttl) {
// Ideally, we would auth this call directly with our `sessionToken` rather than
// going via a BrowserID assertion. Before we can do so we need to resolve some
// Ideally, we would auth this call directly with our `sessionToken`
// using the `_doTokenFetchWithSessionToken` method rather than going
// via a BrowserID assertion. Before we can do so we need to resolve some
// data-volume processing issues in the server-side FxA metrics pipeline.
let token;
let oAuthURL = this.fxAccountsOAuthGrantClient.serverURL.href;
@ -1611,6 +1618,26 @@ FxAccountsInternal.prototype = {
return token;
},
/**
* Does the actual fetch of an oauth token for getOAuthToken()
* using the account session token.
* @param {String} scopeString
* @param {Number} ttl
* @returns {Promise<string>}
* @private
*/
async _doTokenFetchWithSessionToken(scopeString, ttl) {
return this.withSessionToken(async sessionToken => {
const result = await this.fxAccountsClient.accessTokenWithSessionToken(
sessionToken,
FX_OAUTH_CLIENT_ID,
scopeString,
ttl
);
return result.access_token;
});
},
getOAuthToken(options = {}) {
log.debug("getOAuthToken enter");
let scope = options.scope;
@ -1646,10 +1673,13 @@ FxAccountsInternal.prototype = {
log.debug("getOAuthToken has an in-flight request for this scope");
return maybeInFlight;
}
let fetchFunction = this._doTokenFetch.bind(this);
if (USE_SESSION_TOKENS_FOR_OAUTH) {
fetchFunction = this._doTokenFetchWithSessionToken.bind(this);
}
// We need to start a new fetch and stick the promise in our in-flight map
// and remove it when it resolves.
let promise = this._doTokenFetch(scopeString, options.ttl)
let promise = fetchFunction(scopeString, options.ttl)
.then(token => {
// As a sanity check, ensure something else hasn't raced getting a token
// of the same scope. If something has we just make noise rather than

Просмотреть файл

@ -488,6 +488,32 @@ FxAccountsClient.prototype = {
);
},
/**
* Obtain an OAuth access token by authenticating using a session token.
*
* @param {String} sessionTokenHex
* The session token encoded in hex
* @param {String} clientId
* @param {String} scope
* List of space-separated scopes.
* @param {Number} ttl
* Token time to live.
* @return {Promise<Object>} Object containing an `access_token`.
*/
async accessTokenWithSessionToken(sessionTokenHex, clientId, scope, ttl) {
const credentials = await deriveHawkCredentials(
sessionTokenHex,
"sessionToken"
);
const body = {
client_id: clientId,
grant_type: "fxa-credentials",
scope,
ttl,
};
return this._request("/oauth/token", "POST", credentials, body);
},
/**
* Determine if an account exists
*

Просмотреть файл

@ -13,9 +13,6 @@ const {
ASSERTION_LIFETIME,
CERT_LIFETIME,
ERRNO_INVALID_AUTH_TOKEN,
ERRNO_INVALID_FXA_ASSERTION,
ERRNO_NETWORK,
ERROR_INVALID_FXA_ASSERTION,
ERROR_NETWORK,
ERROR_NO_ACCOUNT,
KEY_LIFETIME,
@ -36,7 +33,14 @@ var { AccountState } = ChromeUtils.import(
const ONE_HOUR_MS = 1000 * 60 * 60;
const ONE_DAY_MS = ONE_HOUR_MS * 24;
const TWO_MINUTES_MS = 1000 * 60 * 2;
const MOCK_TOKEN_RESPONSE = {
access_token:
"43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69",
token_type: "bearer",
scope: "https://identity.mozilla.com/apps/oldsync",
expires_in: 21600,
auth_at: 1589579900,
};
initTestLogging("Trace");
@ -1380,6 +1384,95 @@ add_test(function test_getOAuthToken() {
});
});
add_test(async function test_getOAuthTokenWithSessionToken() {
Services.prefs.setBoolPref(
"identity.fxaccounts.useSessionTokensForOAuth",
true
);
let fxa = new MockFxAccounts();
let alice = getTestUser("alice");
alice.verified = true;
let oauthTokenCalled = false;
let client = fxa._internal.fxAccountsClient;
client.accessTokenWithSessionToken = async (
sessionTokenHex,
clientId,
scope,
ttl
) => {
oauthTokenCalled = true;
Assert.equal(sessionTokenHex, "alice's session token");
Assert.equal(clientId, "5882386c6d801776");
Assert.equal(scope, "profile");
Assert.equal(ttl, undefined);
return MOCK_TOKEN_RESPONSE;
};
await fxa.setSignedInUser(alice);
const result = await fxa.getOAuthToken({ scope: "profile" });
Assert.ok(oauthTokenCalled);
Assert.equal(result, MOCK_TOKEN_RESPONSE.access_token);
Services.prefs.setBoolPref(
"identity.fxaccounts.useSessionTokensForOAuth",
false
);
run_next_test();
});
add_task(async function test_getOAuthTokenCachedWithSessionToken() {
Services.prefs.setBoolPref(
"identity.fxaccounts.useSessionTokensForOAuth",
true
);
let fxa = new MockFxAccounts();
let alice = getTestUser("alice");
alice.verified = true;
let numOauthTokenCalls = 0;
let client = fxa._internal.fxAccountsClient;
client.accessTokenWithSessionToken = async () => {
numOauthTokenCalls++;
return MOCK_TOKEN_RESPONSE;
};
await fxa.setSignedInUser(alice);
let result = await fxa.getOAuthToken({
scope: "profile",
service: "test-service",
});
Assert.equal(numOauthTokenCalls, 1);
Assert.equal(
result,
"43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69"
);
// requesting it again should not re-fetch the token.
result = await fxa.getOAuthToken({
scope: "profile",
service: "test-service",
});
Assert.equal(numOauthTokenCalls, 1);
Assert.equal(
result,
"43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69"
);
// But requesting the same service and a different scope *will* get a new one.
result = await fxa.getOAuthToken({
scope: "something-else",
service: "test-service",
});
Assert.equal(numOauthTokenCalls, 2);
Assert.equal(
result,
"43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69"
);
Services.prefs.setBoolPref(
"identity.fxaccounts.useSessionTokensForOAuth",
false
);
});
add_test(function test_getOAuthTokenScoped() {
let fxa = new MockFxAccounts();
let alice = getTestUser("alice");

Просмотреть файл

@ -672,6 +672,40 @@ add_task(async function test_signCertificate() {
await promiseStopServer(server);
});
add_task(async function test_accessTokenWithSessionToken() {
let server = httpd_setup({
"/oauth/token": function(request, response) {
const responseMessage = JSON.stringify({
access_token:
"43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69",
token_type: "bearer",
scope: "https://identity.mozilla.com/apps/oldsync",
expires_in: 21600,
auth_at: 1589579900,
});
response.setStatusLine(request.httpVersion, 200, "OK");
response.bodyOutputStream.write(responseMessage, responseMessage.length);
},
});
let client = new FxAccountsClient(server.baseURI);
let sessionTokenHex =
"0599c36ebb5cad6feb9285b9547b65342b5434d55c07b33bffd4307ab8f82dc4";
let clientId = "5882386c6d801776";
let scope = "https://identity.mozilla.com/apps/oldsync";
let ttl = 100;
let result = await client.accessTokenWithSessionToken(
sessionTokenHex,
clientId,
scope,
ttl
);
Assert.ok(result);
await promiseStopServer(server);
});
add_task(async function test_accountExists() {
let existsMessage = JSON.stringify({
error: "wrong password",