Bug 1132293 - Let reliers access derived encryption keys through FxAccountsOAuthClient. r=mhammond

This commit is contained in:
Ryan Kelly 2015-03-09 21:46:00 -04:00
parent 8bb3af49c6
commit 99a7a88365
5 changed files with 248 additions and 17 deletions

View File

@ -12,6 +12,7 @@ support-files =
browser_bug678392-2.html
browser_bug970746.xhtml
browser_fxa_oauth.html
browser_fxa_oauth_with_keys.html
browser_fxa_profile_channel.html
browser_registerProtocolHandler_notification.html
browser_ssl_error_reports_content.js

View File

@ -17,11 +17,12 @@ XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsOAuthClient",
const HTTP_PATH = "http://example.com";
const HTTP_ENDPOINT = "/browser/browser/base/content/test/general/browser_fxa_oauth.html";
const HTTP_ENDPOINT_WITH_KEYS = "/browser/browser/base/content/test/general/browser_fxa_oauth_with_keys.html";
let gTests = [
{
desc: "FxA OAuth - should open a new tab, complete OAuth flow",
run: function* () {
run: function () {
return new Promise(function(resolve, reject) {
let tabOpened = false;
let properUrl = "http://example.com/browser/browser/base/content/test/general/browser_fxa_oauth.html";
@ -71,6 +72,169 @@ let gTests = [
resolve();
};
client.onError = reject;
client.launchWebFlow();
});
}
},
{
desc: "FxA OAuth - should receive an error when there's a state mismatch",
run: function () {
return new Promise(function(resolve, reject) {
let tabOpened = false;
waitForTab(function (tab) {
Assert.ok("Tab successfully opened");
// It should have passed in the expected non-matching state value.
let queryString = gBrowser.currentURI.spec.split('?')[1];
Assert.ok(queryString.indexOf('state=different-state') >= 0);
tabOpened = true;
});
let client = new FxAccountsOAuthClient({
parameters: {
state: "different-state",
client_id: "client_id",
oauth_uri: HTTP_PATH,
content_uri: HTTP_PATH,
},
authorizationEndpoint: HTTP_ENDPOINT
});
client.onComplete = reject;
client.onError = function(err) {
Assert.ok(tabOpened);
Assert.equal(err.message, "OAuth flow failed. State doesn't match");
resolve();
};
client.launchWebFlow();
});
}
},
{
desc: "FxA OAuth - should be able to request keys during OAuth flow",
run: function () {
return new Promise(function(resolve, reject) {
let tabOpened = false;
waitForTab(function (tab) {
Assert.ok("Tab successfully opened");
// It should have asked for keys.
let queryString = gBrowser.currentURI.spec.split('?')[1];
Assert.ok(queryString.indexOf('keys=true') >= 0);
tabOpened = true;
});
let client = new FxAccountsOAuthClient({
parameters: {
state: "state",
client_id: "client_id",
oauth_uri: HTTP_PATH,
content_uri: HTTP_PATH,
keys: true,
},
authorizationEndpoint: HTTP_ENDPOINT_WITH_KEYS
});
client.onComplete = function(tokenData, keys) {
Assert.ok(tabOpened);
Assert.equal(tokenData.code, "code1");
Assert.equal(tokenData.state, "state");
Assert.equal(keys.kAr, "kAr");
Assert.equal(keys.kBr, "kBr");
resolve();
};
client.onError = reject;
client.launchWebFlow();
});
}
},
{
desc: "FxA OAuth - should not receive keys if not explicitly requested",
run: function () {
return new Promise(function(resolve, reject) {
let tabOpened = false;
waitForTab(function (tab) {
Assert.ok("Tab successfully opened");
// It should not have asked for keys.
let queryString = gBrowser.currentURI.spec.split('?')[1];
Assert.ok(queryString.indexOf('keys=true') == -1);
tabOpened = true;
});
let client = new FxAccountsOAuthClient({
parameters: {
state: "state",
client_id: "client_id",
oauth_uri: HTTP_PATH,
content_uri: HTTP_PATH
},
// This endpoint will cause the completion message to contain keys.
authorizationEndpoint: HTTP_ENDPOINT_WITH_KEYS
});
client.onComplete = function(tokenData, keys) {
Assert.ok(tabOpened);
Assert.equal(tokenData.code, "code1");
Assert.equal(tokenData.state, "state");
Assert.strictEqual(keys, undefined);
resolve();
};
client.onError = reject;
client.launchWebFlow();
});
}
},
{
desc: "FxA OAuth - should receive an error if keys could not be obtained",
run: function () {
return new Promise(function(resolve, reject) {
let tabOpened = false;
waitForTab(function (tab) {
Assert.ok("Tab successfully opened");
// It should have asked for keys.
let queryString = gBrowser.currentURI.spec.split('?')[1];
Assert.ok(queryString.indexOf('keys=true') >= 0);
tabOpened = true;
});
let client = new FxAccountsOAuthClient({
parameters: {
state: "state",
client_id: "client_id",
oauth_uri: HTTP_PATH,
content_uri: HTTP_PATH,
keys: true,
},
// This endpoint will cause the completion message not to contain keys.
authorizationEndpoint: HTTP_ENDPOINT
});
client.onComplete = reject;
client.onError = function(err) {
Assert.ok(tabOpened);
Assert.equal(err.message, "OAuth flow failed. Keys were not returned");
resolve();
};
client.launchWebFlow();
});
}

View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>fxa_oauth_test</title>
</head>
<body>
<script>
window.onload = function(){
var event = new window.CustomEvent("WebChannelMessageToChrome", {
detail: {
id: "oauth_client_id",
message: {
command: "oauth_complete",
data: {
state: "state",
code: "code1",
closeWindow: "signin",
keys: { kAr: 'kAr', kBr: 'kBr' },
},
},
},
});
window.dispatchEvent(event);
};
</script>
</body>
</html>

View File

@ -964,6 +964,7 @@ let MozLoopServiceInternal = {
this.promiseFxAOAuthClient().then(
client => {
client.onComplete = this._fxAOAuthComplete.bind(this, deferred);
client.onError = this._fxAOAuthError.bind(this, deferred);
client.launchWebFlow();
},
error => {
@ -1003,18 +1004,24 @@ let MozLoopServiceInternal = {
/**
* Called once gFxAOAuthClient fires onComplete.
*
* @param {Deferred} deferred used to resolve or reject the gFxAOAuthClientPromise
* @param {Deferred} deferred used to resolve the gFxAOAuthClientPromise
* @param {Object} result (with code and state)
*/
_fxAOAuthComplete: function(deferred, result) {
gFxAOAuthClientPromise = null;
// Note: The state was already verified in FxAccountsOAuthClient.
if (result) {
deferred.resolve(result);
} else {
deferred.reject("Invalid token data");
}
deferred.resolve(result);
},
/**
* Called if gFxAOAuthClient fires onError.
*
* @param {Deferred} deferred used to reject the gFxAOAuthClientPromise
* @param {Object} error object returned by FxAOAuthClient
*/
_fxAOAuthError: function(deferred, err) {
gFxAOAuthClientPromise = null;
deferred.reject(err);
},
};
Object.freeze(MozLoopServiceInternal);

View File

@ -37,6 +37,9 @@ Cu.importGlobalProperties(["URL"]);
* Optional. A colon-separated list of scopes that the user has authorized
* @param {String} [options.parameters.action]
* Optional. If provided, should be either signup or signin.
* @param {Boolean} [options.parameters.keys]
* Optional. If true then relier-specific encryption keys will be
* available in the second argument to onComplete.
* @param [authorizationEndpoint] {String}
* Optional authorization endpoint for the OAuth server
* @constructor
@ -60,16 +63,26 @@ this.FxAccountsOAuthClient = function(options) {
params.append("scope", this.parameters.scope || "");
params.append("action", this.parameters.action || "signin");
params.append("webChannelId", this._webChannelId);
if (this.parameters.keys) {
params.append("keys", "true");
}
};
this.FxAccountsOAuthClient.prototype = {
/**
* Function that gets called once the OAuth flow is complete.
* The callback will receive null as it's argument if there is a state mismatch or an object with
* code and state properties otherwise.
* The callback will receive an object with code and state properties.
* If the keys parameter was specified and true, the callback will receive
* a second argument with kAr and kBr properties.
*/
onComplete: null,
/**
* Function that gets called if there is an error during the OAuth flow,
* for example due to a state mismatch.
* The callback will receive an Error object as its argument.
*/
onError: null,
/**
* Configuration object that stores all OAuth parameters.
*/
@ -116,6 +129,7 @@ this.FxAccountsOAuthClient.prototype = {
*/
tearDown: function() {
this.onComplete = null;
this.onError = null;
this._complete = true;
this._channel.stopListening();
this._channel = null;
@ -160,21 +174,37 @@ this.FxAccountsOAuthClient.prototype = {
switch (command) {
case "oauth_complete":
// validate the state parameter and call onComplete
// validate the returned state and call onComplete or onError
let result = null;
if (this.parameters.state === data.state) {
let err = null;
if (this.parameters.state !== data.state) {
err = new Error("OAuth flow failed. State doesn't match");
} else if (this.parameters.keys && !data.keys) {
err = new Error("OAuth flow failed. Keys were not returned");
} else {
result = {
code: data.code,
state: data.state
};
log.debug("OAuth flow completed.");
} else {
log.debug("OAuth flow failed. State doesn't match");
}
if (this.onComplete) {
this.onComplete(result);
if (err) {
log.debug(err.message);
if (this.onError) {
this.onError(err);
}
} else {
log.debug("OAuth flow completed.");
if (this.onComplete) {
if (this.parameters.keys) {
this.onComplete(result, data.keys);
} else {
this.onComplete(result);
}
}
}
// onComplete will be called for this client only once
// calling onComplete again will result in a failure of the OAuth flow
this.tearDown();