mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 911378 - A BrowserID/Hawk based IdentityManager for Sync. r=rnewman
This commit is contained in:
parent
aad1463007
commit
2a656e7a26
@ -18,6 +18,7 @@ PP_TARGETS += SYNC_PP
|
||||
sync_modules := \
|
||||
addonsreconciler.js \
|
||||
addonutils.js \
|
||||
browserid_identity.js \
|
||||
engines.js \
|
||||
identity.js \
|
||||
jpakeclient.js \
|
||||
|
170
services/sync/modules/browserid_identity.js
Normal file
170
services/sync/modules/browserid_identity.js
Normal file
@ -0,0 +1,170 @@
|
||||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["BrowserIDManager"];
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
||||
|
||||
Cu.import("resource://services-common/async.js");
|
||||
Cu.import("resource://services-common/log4moz.js");
|
||||
Cu.import("resource://services-common/tokenserverclient.js");
|
||||
Cu.import("resource://services-crypto/utils.js");
|
||||
Cu.import("resource://services-sync/identity.js");
|
||||
Cu.import("resource://services-sync/util.js");
|
||||
|
||||
/**
|
||||
* Fetch a token for the sync storage server by passing a BrowserID assertion
|
||||
* from FxAccounts() to TokenServerClient, then wrap the token in in a Hawk
|
||||
* header so that SyncStorageRequest can connect.
|
||||
*/
|
||||
|
||||
this.BrowserIDManager = function BrowserIDManager(fxaService, tokenServerClient) {
|
||||
this._fxaService = fxaService;
|
||||
this._tokenServerClient = tokenServerClient;
|
||||
this._log = Log4Moz.repository.getLogger("Sync.Identity");
|
||||
this._log.Level = Log4Moz.Level[Svc.Prefs.get("log.logger.identity")];
|
||||
|
||||
};
|
||||
|
||||
this.BrowserIDManager.prototype = {
|
||||
__proto__: IdentityManager.prototype,
|
||||
|
||||
_fxaService: null,
|
||||
_tokenServerClient: null,
|
||||
// https://docs.services.mozilla.com/token/apis.html
|
||||
_token: null,
|
||||
|
||||
_clearUserState: function() {
|
||||
this.account = null;
|
||||
this._token = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Unify string munging in account setter and testers (e.g. hasValidToken).
|
||||
*/
|
||||
_normalizeAccountValue: function(value) {
|
||||
return value.toLowerCase();
|
||||
},
|
||||
|
||||
/**
|
||||
* Provide override point for testing token expiration.
|
||||
*/
|
||||
_now: function() {
|
||||
return Date.now();
|
||||
},
|
||||
|
||||
/**
|
||||
* Do we have a non-null, not yet expired token whose email field
|
||||
* matches (when normalized) our account field?
|
||||
*
|
||||
* If the calling function receives false from hasValidToken, it is
|
||||
* responsible for calling _clearUserData().
|
||||
*/
|
||||
hasValidToken: function() {
|
||||
if (!this._token) {
|
||||
return false;
|
||||
}
|
||||
if (this._token.expiration < this._now()) {
|
||||
return false;
|
||||
}
|
||||
let signedInUser = this._getSignedInUser();
|
||||
if (!signedInUser) {
|
||||
return false;
|
||||
}
|
||||
// Does the signed in user match the user we retrieved the token for?
|
||||
if (this._normalizeAccountValue(signedInUser.email) !== this.account) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Wrap and synchronize FxAccounts.getSignedInUser().
|
||||
*
|
||||
* @return credentials per wrapped.
|
||||
*/
|
||||
_getSignedInUser: function() {
|
||||
let userBlob;
|
||||
let cb = Async.makeSpinningCallback();
|
||||
|
||||
this._fxaService.getSignedInUser().then(function (result) {
|
||||
cb(null, result);
|
||||
},
|
||||
function (err) {
|
||||
cb(err);
|
||||
});
|
||||
|
||||
try {
|
||||
userBlob = cb.wait();
|
||||
} catch (err) {
|
||||
this._log.info("FxAccounts.getSignedInUser() failed with: " + err);
|
||||
return null;
|
||||
}
|
||||
return userBlob;
|
||||
},
|
||||
|
||||
_fetchTokenForUser: function(user) {
|
||||
let token;
|
||||
let cb = Async.makeSpinningCallback();
|
||||
let tokenServerURI = Svc.Prefs.get("services.sync.tokenServerURI");
|
||||
|
||||
try {
|
||||
this._tokenServerClient.getTokenFromBrowserIDAssertion(
|
||||
tokenServerURI, user.assertion, cb);
|
||||
token = cb.wait();
|
||||
} catch (err) {
|
||||
this._log.info("TokenServerClient.getTokenFromBrowserIDAssertion() failed with: " + err.api_endpoint);
|
||||
return null;
|
||||
}
|
||||
|
||||
token.expiration = this._now() + (token.duration * 1000);
|
||||
return token;
|
||||
},
|
||||
|
||||
getResourceAuthenticator: function() {
|
||||
return this._getAuthenticationHeader.bind(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* @return a Hawk HTTP Authorization Header, lightly wrapped, for the .uri
|
||||
* of a RESTRequest or AsyncResponse object.
|
||||
*/
|
||||
_getAuthenticationHeader: function(httpObject, method) {
|
||||
if (!this.hasValidToken()) {
|
||||
this._clearUserState();
|
||||
let user = this._getSignedInUser();
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
this._token = this._fetchTokenForUser(user);
|
||||
if (!this._token) {
|
||||
return null;
|
||||
}
|
||||
this.account = this._normalizeAccountValue(user.email);
|
||||
}
|
||||
let credentials = {algorithm: "sha256",
|
||||
id: this.username,
|
||||
key: this._token,
|
||||
};
|
||||
method = method || httpObject.method;
|
||||
let headerValue = CryptoUtils.computeHAWK(httpObject.uri, method,
|
||||
{credentials: credentials});
|
||||
return {headers: {authorization: headerValue.field}};
|
||||
},
|
||||
|
||||
getRequestAuthenticator: function() {
|
||||
return this._addAuthenticationHeader.bind(this);
|
||||
},
|
||||
|
||||
_addAuthenticationHeader: function(request, method) {
|
||||
let header = this._getAuthenticationHeader(request, method);
|
||||
if (!header) {
|
||||
return null;
|
||||
}
|
||||
request.setHeader("authorization", header.headers.authorization);
|
||||
return request;
|
||||
}
|
||||
};
|
@ -71,3 +71,5 @@ pref("services.sync.log.logger.engine.addons", "Debug");
|
||||
pref("services.sync.log.logger.engine.apps", "Debug");
|
||||
pref("services.sync.log.logger.userapi", "Debug");
|
||||
pref("services.sync.log.cryptoDebug", false);
|
||||
|
||||
pref("services.sync.tokenServerURI", "http://auth.oldsync.dev.lcip.org/1.0/sync/1.1");
|
||||
|
136
services/sync/tests/unit/test_browserid_identity.js
Normal file
136
services/sync/tests/unit/test_browserid_identity.js
Normal file
@ -0,0 +1,136 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
Cu.import("resource://gre/modules/FxAccounts.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://services-sync/browserid_identity.js");
|
||||
Cu.import("resource://services-sync/rest.js");
|
||||
Cu.import("resource://services-sync/util.js");
|
||||
|
||||
let mockUser = {assertion: 'assertion',
|
||||
email: 'email',
|
||||
kA: 'kA',
|
||||
kB: 'kB',
|
||||
sessionToken: 'sessionToken',
|
||||
uid: 'user_uid',
|
||||
};
|
||||
|
||||
let _MockFXA = function(blob) {
|
||||
this.user = blob;
|
||||
};
|
||||
_MockFXA.prototype = {
|
||||
__proto__: FxAccounts.prototype,
|
||||
getSignedInUser: function getSignedInUser() {
|
||||
let deferred = Promise.defer();
|
||||
deferred.resolve(this.user);
|
||||
return deferred.promise;
|
||||
},
|
||||
};
|
||||
let mockFXA = new _MockFXA(mockUser);
|
||||
|
||||
let mockToken = {
|
||||
api_endpoint: Svc.Prefs.get("services.sync.tokenServerURI"),
|
||||
duration: 300,
|
||||
id: "id",
|
||||
key: "key",
|
||||
uid: "token_uid",
|
||||
};
|
||||
let mockTSC = { // TokenServerClient
|
||||
getTokenFromBrowserIDAssertion: function(uri, assertion, cb) {
|
||||
cb(null, mockToken);
|
||||
},
|
||||
};
|
||||
|
||||
let browseridManager = new BrowserIDManager(mockFXA, mockTSC);
|
||||
|
||||
function run_test() {
|
||||
initTestLogging("Trace");
|
||||
Log4Moz.repository.getLogger("Sync.Identity").level = Log4Moz.Level.Trace;
|
||||
run_next_test();
|
||||
};
|
||||
|
||||
add_test(function test_initial_state() {
|
||||
_("Verify initial state");
|
||||
do_check_false(!!browseridManager._token);
|
||||
do_check_false(browseridManager.hasValidToken());
|
||||
do_check_false(!!browseridManager.account);
|
||||
run_next_test();
|
||||
}
|
||||
);
|
||||
|
||||
add_test(function test_getResourceAuthenticator() {
|
||||
_("BrowserIDManager supplies a Resource Authenticator callback which returns a Hawk header.");
|
||||
let authenticator = browseridManager.getResourceAuthenticator();
|
||||
do_check_true(!!authenticator);
|
||||
let req = {uri: CommonUtils.makeURI(
|
||||
"https://example.net/somewhere/over/the/rainbow"),
|
||||
method: 'GET'};
|
||||
let output = authenticator(req, 'GET');
|
||||
do_check_true('headers' in output);
|
||||
do_check_true('authorization' in output.headers);
|
||||
do_check_true(output.headers.authorization.startsWith('Hawk'));
|
||||
_("Expected internal state after successful call.");
|
||||
do_check_eq(browseridManager._token.uid, mockToken.uid);
|
||||
do_check_eq(browseridManager.account, browseridManager._normalizeAccountValue(mockUser.email));
|
||||
run_next_test();
|
||||
}
|
||||
);
|
||||
|
||||
add_test(function test_getRequestAuthenticator() {
|
||||
_("BrowserIDManager supplies a Request Authenticator callback which sets a Hawk header on a request object.");
|
||||
let request = new SyncStorageRequest(
|
||||
"https://example.net/somewhere/over/the/rainbow");
|
||||
let authenticator = browseridManager.getRequestAuthenticator();
|
||||
do_check_true(!!authenticator);
|
||||
let output = authenticator(request, 'GET');
|
||||
do_check_eq(request.uri, output.uri);
|
||||
do_check_true(output._headers.authorization.startsWith('Hawk'));
|
||||
do_check_true(output._headers.authorization.contains('nonce'));
|
||||
do_check_true(browseridManager.hasValidToken());
|
||||
run_next_test();
|
||||
}
|
||||
);
|
||||
|
||||
add_test(function test_tokenExpiration() {
|
||||
_("BrowserIDManager notices token expiration:");
|
||||
let bimExp = new BrowserIDManager(mockFXA, mockTSC);
|
||||
|
||||
let authenticator = bimExp.getResourceAuthenticator();
|
||||
do_check_true(!!authenticator);
|
||||
let req = {uri: CommonUtils.makeURI(
|
||||
"https://example.net/somewhere/over/the/rainbow"),
|
||||
method: 'GET'};
|
||||
authenticator(req, 'GET');
|
||||
|
||||
// Mock the clock.
|
||||
_("Forcing the token to expire ...");
|
||||
Object.defineProperty(bimExp, "_now", {
|
||||
value: function customNow() {
|
||||
return (Date.now() + 3000001);
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
do_check_true(bimExp._token.expiration < bimExp._now());
|
||||
_("... means BrowserIDManager knows to re-fetch it on the next call.");
|
||||
do_check_false(bimExp.hasValidToken());
|
||||
run_next_test();
|
||||
}
|
||||
);
|
||||
|
||||
add_test(function test_userChangeAndLogOut() {
|
||||
_("BrowserIDManager notices when the FxAccounts.getSignedInUser().email changes.");
|
||||
let mockFXA2 = new _MockFXA(mockUser);
|
||||
let bidUser = new BrowserIDManager(mockFXA2, mockTSC);
|
||||
let request = new SyncStorageRequest(
|
||||
"https://example.net/somewhere/over/the/rainbow");
|
||||
let authenticator = bidUser.getRequestAuthenticator();
|
||||
do_check_true(!!authenticator);
|
||||
let output = authenticator(request, 'GET');
|
||||
do_check_true(!!output);
|
||||
do_check_eq(bidUser.account, mockUser.email);
|
||||
do_check_true(bidUser.hasValidToken());
|
||||
mockUser.email = "something@new";
|
||||
do_check_false(bidUser.hasValidToken());
|
||||
run_next_test();
|
||||
}
|
||||
);
|
@ -4,6 +4,7 @@
|
||||
const modules = [
|
||||
"addonutils.js",
|
||||
"addonsreconciler.js",
|
||||
"browserid_identity.js",
|
||||
"constants.js",
|
||||
"engines/addons.js",
|
||||
"engines/bookmarks.js",
|
||||
@ -50,4 +51,3 @@ function run_test() {
|
||||
Cu.import(res, {});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,6 +50,7 @@ skip-if = os == "win" || os == "android"
|
||||
[test_syncstoragerequest.js]
|
||||
|
||||
# Generic Sync types.
|
||||
[test_browserid_identity.js]
|
||||
[test_collection_inc_get.js]
|
||||
[test_collections_recovery.js]
|
||||
[test_identity_manager.js]
|
||||
|
Loading…
Reference in New Issue
Block a user