Bug 911378 - A BrowserID/Hawk based IdentityManager for Sync. r=rnewman

This commit is contained in:
Sam Penrose 2013-10-02 23:48:08 +02:00
parent aad1463007
commit 2a656e7a26
6 changed files with 311 additions and 1 deletions

View File

@ -18,6 +18,7 @@ PP_TARGETS += SYNC_PP
sync_modules := \
addonsreconciler.js \
addonutils.js \
browserid_identity.js \
engines.js \
identity.js \
jpakeclient.js \

View 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;
}
};

View File

@ -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");

View 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();
}
);

View File

@ -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, {});
}
}

View File

@ -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]