mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-12-03 02:25:34 +00:00
Bug 1631830 - Fetch Sync tokens with OAuth behind a pref r=rfkelly
Differential Revision: https://phabricator.services.mozilla.com/D72092
This commit is contained in:
parent
af3bb17589
commit
cf50ebbe18
@ -1422,6 +1422,9 @@ pref("identity.fxaccounts.remote.pairing.uri", "wss://channelserver.services.moz
|
||||
// Token server used by the FxA Sync identity.
|
||||
pref("identity.sync.tokenserver.uri", "https://token.services.mozilla.com/1.0/sync/1.5");
|
||||
|
||||
// Fetch Sync tokens using the OAuth token function
|
||||
pref("identity.sync.useOAuthForSyncToken", false);
|
||||
|
||||
// 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
|
||||
|
@ -157,16 +157,19 @@ TokenServerClient.prototype = {
|
||||
/**
|
||||
* Obtain a token from a BrowserID assertion against a specific URL.
|
||||
*
|
||||
* This asynchronously obtains the token. The callback receives 2 arguments:
|
||||
* This asynchronously obtains the token.
|
||||
* It returns a Promise that resolves or rejects:
|
||||
*
|
||||
* (TokenServerClientError | null) If no token could be obtained, this
|
||||
* Rejects with:
|
||||
* (TokenServerClientError) If no token could be obtained, this
|
||||
* will be a TokenServerClientError instance describing why. The
|
||||
* type seen defines the type of error encountered. If an HTTP response
|
||||
* was seen, a RESTResponse instance will be stored in the `response`
|
||||
* property of this object. If there was no error and a token is
|
||||
* available, this will be null.
|
||||
*
|
||||
* (map | null) On success, this will be a map containing the results from
|
||||
* Resolves with:
|
||||
* (map) On success, this will be a map containing the results from
|
||||
* the server. If there was an error, this will be null. The map has the
|
||||
* following properties:
|
||||
*
|
||||
@ -218,23 +221,72 @@ TokenServerClient.prototype = {
|
||||
* (string) URL to fetch token from.
|
||||
* @param assertion
|
||||
* (string) BrowserID assertion to exchange token for.
|
||||
* @param conditionsAccepted
|
||||
* (bool) Whether to send acceptance to service conditions.
|
||||
* @param addHeaders
|
||||
* (object) Extra headers for the request.
|
||||
*/
|
||||
async getTokenFromBrowserIDAssertion(url, assertion, addHeaders = {}) {
|
||||
if (!url) {
|
||||
throw new TokenServerClientError("url argument is not valid.");
|
||||
}
|
||||
this._log.debug("Beginning BID assertion exchange: " + url);
|
||||
|
||||
if (!assertion) {
|
||||
throw new TokenServerClientError("assertion argument is not valid.");
|
||||
}
|
||||
|
||||
this._log.debug("Beginning BID assertion exchange: " + url);
|
||||
return this._tokenServerExchangeRequest(
|
||||
url,
|
||||
`BrowserID ${assertion}`,
|
||||
addHeaders
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Obtain a token from a provided OAuth token against a specific URL.
|
||||
*
|
||||
* @param url
|
||||
* (string) URL to fetch token from.
|
||||
* @param oauthToken
|
||||
* (string) FxA OAuth Token to exchange token for.
|
||||
* @param addHeaders
|
||||
* (object) Extra headers for the request.
|
||||
*/
|
||||
async getTokenFromOAuthToken(url, oauthToken, addHeaders = {}) {
|
||||
this._log.debug("Beginning OAuth token exchange: " + url);
|
||||
|
||||
if (!oauthToken) {
|
||||
throw new TokenServerClientError("oauthToken argument is not valid.");
|
||||
}
|
||||
|
||||
return this._tokenServerExchangeRequest(
|
||||
url,
|
||||
`Bearer ${oauthToken}`,
|
||||
addHeaders
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Performs the exchange request to the token server to
|
||||
* produce a token based on the authorizationHeader input.
|
||||
*
|
||||
* @param url
|
||||
* (string) URL to fetch token from.
|
||||
* @param authorizationHeader
|
||||
* (string) The auth header string that populates the 'Authorization' header.
|
||||
* @param addHeaders
|
||||
* (object) Extra headers for the request.
|
||||
*/
|
||||
async _tokenServerExchangeRequest(url, authorizationHeader, addHeaders = {}) {
|
||||
if (!url) {
|
||||
throw new TokenServerClientError("url argument is not valid.");
|
||||
}
|
||||
|
||||
if (!authorizationHeader) {
|
||||
throw new TokenServerClientError(
|
||||
"authorizationHeader argument is not valid."
|
||||
);
|
||||
}
|
||||
|
||||
let req = this.newRESTRequest(url);
|
||||
req.setHeader("Accept", "application/json");
|
||||
req.setHeader("Authorization", "BrowserID " + assertion);
|
||||
req.setHeader("Authorization", authorizationHeader);
|
||||
|
||||
for (let header in addHeaders) {
|
||||
req.setHeader(header, addHeaders[header]);
|
||||
|
@ -1525,8 +1525,14 @@ FxAccountsInternal.prototype = {
|
||||
delete currentState.whenVerifiedDeferred;
|
||||
},
|
||||
|
||||
// Does the actual fetch of an oauth token for getOAuthToken()
|
||||
async _doTokenFetch(scopeString) {
|
||||
/**
|
||||
* Does the actual fetch of an oauth token for getOAuthToken()
|
||||
* @param scopeString
|
||||
* @param ttl
|
||||
* @returns {Promise<string>}
|
||||
* @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
|
||||
// data-volume processing issues in the server-side FxA metrics pipeline.
|
||||
@ -1536,7 +1542,8 @@ FxAccountsInternal.prototype = {
|
||||
try {
|
||||
let result = await this.fxAccountsOAuthGrantClient.getTokenFromAssertion(
|
||||
assertion,
|
||||
scopeString
|
||||
scopeString,
|
||||
ttl
|
||||
);
|
||||
token = result.access_token;
|
||||
} catch (err) {
|
||||
@ -1552,7 +1559,8 @@ FxAccountsInternal.prototype = {
|
||||
assertion = await this.getAssertion(oAuthURL);
|
||||
let result = await this.fxAccountsOAuthGrantClient.getTokenFromAssertion(
|
||||
assertion,
|
||||
scopeString
|
||||
scopeString,
|
||||
ttl
|
||||
);
|
||||
token = result.access_token;
|
||||
}
|
||||
@ -1597,7 +1605,7 @@ FxAccountsInternal.prototype = {
|
||||
|
||||
// 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)
|
||||
let promise = this._doTokenFetch(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
|
||||
|
@ -51,6 +51,7 @@ exports.ASSERTION_LIFETIME = 1000 * 3600 * 24 * 365 * 25; // 25 years
|
||||
// period).
|
||||
exports.ASSERTION_USE_PERIOD = 1000 * 60 * 5; // 5 minutes
|
||||
exports.CERT_LIFETIME = 1000 * 3600 * 6; // 6 hours
|
||||
exports.OAUTH_TOKEN_FOR_SYNC_LIFETIME_SECONDS = 3600 * 6; // 6 hours
|
||||
exports.KEY_LIFETIME = 1000 * 3600 * 12; // 12 hours
|
||||
|
||||
// After we start polling for account verification, we stop polling when this
|
||||
|
@ -78,10 +78,11 @@ FxAccountsOAuthGrantClient.prototype = {
|
||||
*
|
||||
* @param {Object} assertion BrowserID assertion
|
||||
* @param {String} scope OAuth scope
|
||||
* @param {Number} ttl token time to live
|
||||
* @return Promise
|
||||
* Resolves: {Object} Object with access_token property
|
||||
*/
|
||||
getTokenFromAssertion(assertion, scope) {
|
||||
getTokenFromAssertion(assertion, scope, ttl) {
|
||||
if (!assertion) {
|
||||
throw new Error("Missing 'assertion' parameter");
|
||||
}
|
||||
@ -93,6 +94,7 @@ FxAccountsOAuthGrantClient.prototype = {
|
||||
client_id: this.parameters.client_id,
|
||||
assertion,
|
||||
response_type: "token",
|
||||
ttl,
|
||||
};
|
||||
|
||||
return this._createRequest(AUTH_ENDPOINT, "POST", params);
|
||||
|
@ -105,12 +105,15 @@ function MockFxAccountsOAuthGrantClient(activeTokens) {
|
||||
|
||||
MockFxAccountsOAuthGrantClient.prototype = {
|
||||
serverURL: { href: "http://localhost" },
|
||||
getTokenFromAssertion(assertion, scope) {
|
||||
let token = "token" + this.numTokenFetches;
|
||||
getTokenFromAssertion(assertion, scope, ttl) {
|
||||
let token = `token${this.numTokenFetches}`;
|
||||
if (ttl) {
|
||||
token += `-ttl-${ttl}`;
|
||||
}
|
||||
this.numTokenFetches += 1;
|
||||
this.activeTokens.add(token);
|
||||
print("getTokenFromAssertion returning token", token);
|
||||
return Promise.resolve({ access_token: token });
|
||||
return Promise.resolve({ access_token: token, ttl });
|
||||
},
|
||||
};
|
||||
|
||||
@ -251,3 +254,10 @@ add_task(async function testTokenRaces() {
|
||||
equal(oauthClient.activeTokens.size, 0);
|
||||
ok(client.oauthDestroy.calledTwice);
|
||||
});
|
||||
|
||||
add_task(async function testTokenTTL() {
|
||||
// This tests the TTL option passed into the method
|
||||
let fxa = await createMockFxA();
|
||||
let token = await fxa.getOAuthToken({ scope: "test-ttl", ttl: 1000 });
|
||||
equal(token, "token0-ttl-1000");
|
||||
});
|
||||
|
@ -244,6 +244,15 @@ var configureFxAccountIdentity = function(
|
||||
config.fxaccount.token.uid = config.username;
|
||||
return config.fxaccount.token;
|
||||
},
|
||||
async getTokenFromOAuthToken(url, oauthToken) {
|
||||
Assert.equal(
|
||||
url,
|
||||
Services.prefs.getStringPref("identity.sync.tokenserver.uri")
|
||||
);
|
||||
Assert.ok(oauthToken, "oauth token present");
|
||||
config.fxaccount.token.uid = config.username;
|
||||
return config.fxaccount.token;
|
||||
},
|
||||
};
|
||||
authService._fxaService = fxa;
|
||||
authService._tokenServerClient = mockTSC;
|
||||
|
@ -62,6 +62,12 @@ XPCOMUtils.defineLazyPreferenceGetter(
|
||||
"services.sync.debug.ignoreCachedAuthCredentials"
|
||||
);
|
||||
|
||||
XPCOMUtils.defineLazyPreferenceGetter(
|
||||
this,
|
||||
"USE_OAUTH_FOR_SYNC_TOKEN",
|
||||
"identity.sync.useOAuthForSyncToken"
|
||||
);
|
||||
|
||||
// FxAccountsCommon.js doesn't use a "namespace", so create one here.
|
||||
var fxAccountsCommon = {};
|
||||
ChromeUtils.import(
|
||||
@ -396,16 +402,22 @@ this.BrowserIDManager.prototype = {
|
||||
// Do the assertion/certificate/token dance, with a retry.
|
||||
let getToken = async keys => {
|
||||
this._log.info("Getting an assertion from", this._tokenServerUrl);
|
||||
const audience = Services.io.newURI(this._tokenServerUrl).prePath;
|
||||
const assertion = await fxa._internal.getAssertion(audience);
|
||||
let token;
|
||||
|
||||
if (USE_OAUTH_FOR_SYNC_TOKEN) {
|
||||
token = await this._fetchTokenUsingOAuth();
|
||||
} else {
|
||||
const audience = Services.io.newURI(this._tokenServerUrl).prePath;
|
||||
const assertion = await fxa._internal.getAssertion(audience);
|
||||
this._log.debug("Getting a token using an Assertion");
|
||||
const headers = { "X-Client-State": keys.kXCS };
|
||||
token = await this._tokenServerClient.getTokenFromBrowserIDAssertion(
|
||||
this._tokenServerUrl,
|
||||
assertion,
|
||||
headers
|
||||
);
|
||||
}
|
||||
|
||||
this._log.debug("Getting a token");
|
||||
const headers = { "X-Client-State": keys.kXCS };
|
||||
const token = await this._tokenServerClient.getTokenFromBrowserIDAssertion(
|
||||
this._tokenServerUrl,
|
||||
assertion,
|
||||
headers
|
||||
);
|
||||
this._log.trace("Successfully got a token");
|
||||
return token;
|
||||
};
|
||||
@ -473,6 +485,32 @@ this.BrowserIDManager.prototype = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetches an OAuth token using the OLD_SYNC scope and later exchanges it
|
||||
* for a TokenServer token.
|
||||
*
|
||||
* @returns {Promise}
|
||||
* @private
|
||||
*/
|
||||
async _fetchTokenUsingOAuth() {
|
||||
this._log.debug("Getting a token using OAuth");
|
||||
const fxa = this._fxaService;
|
||||
const clientId = fxAccountsCommon.FX_OAUTH_CLIENT_ID;
|
||||
const scope = fxAccountsCommon.SCOPE_OLD_SYNC;
|
||||
const ttl = fxAccountsCommon.OAUTH_TOKEN_FOR_SYNC_LIFETIME_SECONDS;
|
||||
const oauthToken = await fxa.getOAuthToken({ scope, ttl });
|
||||
const scopedKeys = await fxa.keys.getScopedKeys(scope, clientId);
|
||||
const key = scopedKeys[scope];
|
||||
const headers = {
|
||||
"X-KeyId": key.kid,
|
||||
};
|
||||
return this._tokenServerClient.getTokenFromOAuthToken(
|
||||
this._tokenServerUrl,
|
||||
oauthToken,
|
||||
headers
|
||||
);
|
||||
},
|
||||
|
||||
// Returns a promise that is resolved with a valid token for the current
|
||||
// user, or rejects if one can't be obtained.
|
||||
// NOTE: This does all the authentication for Sync - it both sets the
|
||||
|
@ -33,6 +33,23 @@ const SECOND_MS = 1000;
|
||||
const MINUTE_MS = SECOND_MS * 60;
|
||||
const HOUR_MS = MINUTE_MS * 60;
|
||||
|
||||
const MOCK_TOKEN_RESPONSE = {
|
||||
access_token:
|
||||
"e3c5caf17f27a0d9e351926a928938b3737df43e91d4992a5a5fca9a7bdef8ba",
|
||||
token_type: "bearer",
|
||||
scope: "https://identity.mozilla.com/apps/oldsync",
|
||||
expires_in: 3600 * 6,
|
||||
auth_at: 1587762898,
|
||||
};
|
||||
const MOCK_SCOPED_KEY_RESPONSE = {
|
||||
"https://identity.mozilla.com/apps/oldsync": {
|
||||
identifier: "https://identity.mozilla.com/apps/oldsync",
|
||||
keyRotationSecret:
|
||||
"0000000000000000000000000000000000000000000000000000000000000000",
|
||||
keyRotationTimestamp: 1510726317123,
|
||||
},
|
||||
};
|
||||
|
||||
var globalIdentityConfig = makeIdentityConfig();
|
||||
var globalBrowseridManager = new BrowserIDManager();
|
||||
configureFxAccountIdentity(globalBrowseridManager, globalIdentityConfig);
|
||||
@ -53,30 +70,6 @@ MockFxAccountsClient.prototype = {
|
||||
},
|
||||
};
|
||||
|
||||
function MockFxAccounts() {
|
||||
let fxa = new FxAccounts({
|
||||
_now_is: Date.now(),
|
||||
|
||||
now() {
|
||||
return this._now_is;
|
||||
},
|
||||
|
||||
fxAccountsClient: new MockFxAccountsClient(),
|
||||
});
|
||||
fxa._internal.currentAccountState.getCertificate = function(
|
||||
data,
|
||||
keyPair,
|
||||
mustBeValidUntil
|
||||
) {
|
||||
this.cert = {
|
||||
validUntil: fxa._internal.now() + CERT_LIFETIME,
|
||||
cert: "certificate",
|
||||
};
|
||||
return Promise.resolve(this.cert.cert);
|
||||
};
|
||||
return fxa;
|
||||
}
|
||||
|
||||
add_test(function test_initial_state() {
|
||||
_("Verify initial state");
|
||||
Assert.ok(!globalBrowseridManager._token);
|
||||
@ -91,6 +84,26 @@ add_task(async function test_initialialize() {
|
||||
Assert.ok(globalBrowseridManager._hasValidToken());
|
||||
});
|
||||
|
||||
add_task(async function test_initialialize_via_oauth_token() {
|
||||
_("Verify start after fetching token using the oauth token flow");
|
||||
Services.prefs.setBoolPref("identity.sync.useOAuthForSyncToken", true);
|
||||
let browseridManager = new BrowserIDManager();
|
||||
|
||||
let identityConfig = makeIdentityConfig();
|
||||
let fxaInternal = makeFxAccountsInternalMock(identityConfig);
|
||||
configureFxAccountIdentity(browseridManager, identityConfig, fxaInternal);
|
||||
browseridManager._fxaService._internal.initialize();
|
||||
browseridManager._fxaService._internal.fxAccountsOAuthGrantClient.getTokenFromAssertion = () =>
|
||||
Promise.resolve(MOCK_TOKEN_RESPONSE);
|
||||
browseridManager._fxaService._internal._fxAccountsClient.getScopedKeyData = () =>
|
||||
Promise.resolve(MOCK_SCOPED_KEY_RESPONSE);
|
||||
|
||||
await browseridManager._ensureValidToken();
|
||||
Assert.ok(!!browseridManager._token);
|
||||
Assert.ok(browseridManager._hasValidToken());
|
||||
Services.prefs.setBoolPref("identity.sync.useOAuthForSyncToken", false);
|
||||
});
|
||||
|
||||
add_task(async function test_initialializeWithAuthErrorAndDeletedAccount() {
|
||||
_("Verify sync state with auth error + account deleted");
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user