Bug 1130634 - More consistent error handing in FxAccounts.getOauthToken r=markh

--HG--
extra : rebase_source : 24006d87d3255fac6f0c7dd85f48d95dba42c703
This commit is contained in:
Zachary Carter 2015-02-13 13:54:15 -08:00
parent b0b2e34da7
commit 9c7eebabd4
5 changed files with 350 additions and 15 deletions

View File

@ -910,26 +910,96 @@ FxAccountsInternal.prototype = {
}).then(result => currentState.resolve(result));
},
/*
/**
* Get an OAuth token for the user
*
* @param options
* {
* scope: (string) the oauth scope being requested
* }
*
* @return Promise.<string | Error>
* The promise resolves the oauth token as a string or rejects with
* an error object ({error: ERROR, details: {}}) of the following:
* INVALID_PARAMETER
* NO_ACCOUNT
* UNVERIFIED_ACCOUNT
* NETWORK_ERROR
* AUTH_ERROR
* UNKNOWN_ERROR
*/
getOAuthToken: function (options = {}) {
log.debug("getOAuthToken enter");
if (!options.scope) {
throw new Error("Missing 'scope' option");
return this._error(ERROR_INVALID_PARAMETER, "Missing 'scope' option");
}
let oAuthURL = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.oauth.uri");
let client = options.client;
let client = options.client || new FxAccountsOAuthGrantClient({
serverURL: oAuthURL,
client_id: FX_OAUTH_CLIENT_ID
});
if (!client) {
try {
client = new FxAccountsOAuthGrantClient({
serverURL: oAuthURL,
client_id: FX_OAUTH_CLIENT_ID
});
} catch (e) {
return this._error(ERROR_INVALID_PARAMETER, e);
}
}
return this.getAssertion(oAuthURL)
return this._getVerifiedAccountOrReject()
.then(() => this.getAssertion(oAuthURL))
.then(assertion => client.getTokenFromAssertion(assertion, options.scope))
.then(result => result.access_token);
.then(result => result.access_token)
.then(null, err => this._errorToErrorClass(err));
},
_getVerifiedAccountOrReject: function () {
return this.currentAccountState.getUserAccountData().then(data => {
if (!data) {
// No signed-in user
return this._error(ERROR_NO_ACCOUNT);
}
if (!this.isUserEmailVerified(data)) {
// Signed-in user has not verified email
return this._error(ERROR_UNVERIFIED_ACCOUNT);
}
});
},
/*
* Coerce an error into one of the general error cases:
* NETWORK_ERROR
* AUTH_ERROR
* UNKNOWN_ERROR
*
* These errors will pass through:
* INVALID_PARAMETER
* NO_ACCOUNT
* UNVERIFIED_ACCOUNT
*/
_errorToErrorClass: function (aError) {
if (aError.errno) {
let error = SERVER_ERRNO_TO_ERROR[aError.errno];
return this._error(ERROR_TO_GENERAL_ERROR_CLASS[error] || ERROR_UNKNOWN, aError);
} else if (aError.message &&
aError.message === "INVALID_PARAMETER" ||
aError.message === "NO_ACCOUNT" ||
aError.message === "UNVERIFIED_ACCOUNT") {
return Promise.reject(aError);
}
return this._error(ERROR_UNKNOWN, aError);
},
_error: function(aError, aDetails) {
log.error("FxA rejecting with error ${aError}, details: ${aDetails}", {aError, aDetails});
let reason = new Error(aError);
if (aDetails) {
reason.details = aDetails;
}
return Promise.reject(reason);
}
};

View File

@ -124,6 +124,24 @@ exports.ERRNO_PARSE = 997;
exports.ERRNO_NETWORK = 998;
exports.ERRNO_UNKNOWN_ERROR = 999;
// Offset oauth server errnos so they don't conflict with auth server errnos
const OAUTH_SERVER_ERRNO_OFFSET = exports.OAUTH_SERVER_ERRNO_OFFSET = 1000;
// OAuth Server errno.
exports.ERRNO_UNKNOWN_CLIENT_ID = 101 + OAUTH_SERVER_ERRNO_OFFSET;
exports.ERRNO_INCORRECT_CLIENT_SECRET = 102 + OAUTH_SERVER_ERRNO_OFFSET;
exports.ERRNO_INCORRECT_REDIRECT_URI = 103 + OAUTH_SERVER_ERRNO_OFFSET;
exports.ERRNO_INVALID_FXA_ASSERTION = 104 + OAUTH_SERVER_ERRNO_OFFSET;
exports.ERRNO_UNKNOWN_CODE = 105 + OAUTH_SERVER_ERRNO_OFFSET;
exports.ERRNO_INCORRECT_CODE = 106 + OAUTH_SERVER_ERRNO_OFFSET;
exports.ERRNO_EXPIRED_CODE = 107 + OAUTH_SERVER_ERRNO_OFFSET;
exports.ERRNO_OAUTH_INVALID_TOKEN = 108 + OAUTH_SERVER_ERRNO_OFFSET;
exports.ERRNO_INVALID_REQUEST_PARAM = 109 + OAUTH_SERVER_ERRNO_OFFSET;
exports.ERRNO_INVALID_RESPONSE_TYPE = 110 + OAUTH_SERVER_ERRNO_OFFSET;
exports.ERRNO_UNAUTHORIZED = 111 + OAUTH_SERVER_ERRNO_OFFSET;
exports.ERRNO_FORBIDDEN = 112 + OAUTH_SERVER_ERRNO_OFFSET;
exports.ERRNO_INVALID_CONTENT_TYPE = 113 + OAUTH_SERVER_ERRNO_OFFSET;
// Errors.
exports.ERROR_ACCOUNT_ALREADY_EXISTS = "ACCOUNT_ALREADY_EXISTS";
exports.ERROR_ACCOUNT_DOES_NOT_EXIST = "ACCOUNT_DOES_NOT_EXIST ";
@ -162,6 +180,26 @@ exports.ERROR_NETWORK = "NETWORK_ERROR";
exports.ERROR_UNKNOWN = "UNKNOWN_ERROR";
exports.ERROR_UNVERIFIED_ACCOUNT = "UNVERIFIED_ACCOUNT";
// OAuth errors.
exports.ERROR_UNKNOWN_CLIENT_ID = "UNKNOWN_CLIENT_ID";
exports.ERROR_INCORRECT_CLIENT_SECRET = "INCORRECT_CLIENT_SECRET";
exports.ERROR_INCORRECT_REDIRECT_URI = "INCORRECT_REDIRECT_URI";
exports.ERROR_INVALID_FXA_ASSERTION = "INVALID_FXA_ASSERTION";
exports.ERROR_UNKNOWN_CODE = "UNKNOWN_CODE";
exports.ERROR_INCORRECT_CODE = "INCORRECT_CODE";
exports.ERROR_EXPIRED_CODE = "EXPIRED_CODE";
exports.ERROR_OAUTH_INVALID_TOKEN = "OAUTH_INVALID_TOKEN";
exports.ERROR_INVALID_REQUEST_PARAM = "INVALID_REQUEST_PARAM";
exports.ERROR_INVALID_RESPONSE_TYPE = "INVALID_RESPONSE_TYPE";
exports.ERROR_UNAUTHORIZED = "UNAUTHORIZED";
exports.ERROR_FORBIDDEN = "FORBIDDEN";
exports.ERROR_INVALID_CONTENT_TYPE = "INVALID_CONTENT_TYPE";
// Additional generic error classes for external consumers
exports.ERROR_NO_ACCOUNT = "NO_ACCOUNT";
exports.ERROR_AUTH_ERROR = "AUTH_ERROR";
exports.ERROR_INVALID_PARAMETER = "INVALID_PARAMETER";
// Status code errors
exports.ERROR_CODE_METHOD_NOT_ALLOWED = 405;
exports.ERROR_MSG_METHOD_NOT_ALLOWED = "METHOD_NOT_ALLOWED";
@ -182,6 +220,9 @@ exports.FXA_PWDMGR_REALM = "Firefox Accounts credentials";
// Error matching.
exports.SERVER_ERRNO_TO_ERROR = {};
// Error mapping
exports.ERROR_TO_GENERAL_ERROR_CLASS = {};
for (let id in exports) {
this[id] = exports[id];
}
@ -212,3 +253,71 @@ SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_API_VERSION] = ERROR_INCORRECT_AP
SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_EMAIL_CASE] = ERROR_INCORRECT_EMAIL_CASE;
SERVER_ERRNO_TO_ERROR[ERRNO_SERVICE_TEMP_UNAVAILABLE] = ERROR_SERVICE_TEMP_UNAVAILABLE;
SERVER_ERRNO_TO_ERROR[ERRNO_UNKNOWN_ERROR] = ERROR_UNKNOWN;
SERVER_ERRNO_TO_ERROR[ERRNO_NETWORK] = ERROR_NETWORK;
// oauth
SERVER_ERRNO_TO_ERROR[ERRNO_UNKNOWN_CLIENT_ID] = ERROR_UNKNOWN_CLIENT_ID;
SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_CLIENT_SECRET] = ERROR_INCORRECT_CLIENT_SECRET;
SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_REDIRECT_URI] = ERROR_INCORRECT_REDIRECT_URI;
SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_FXA_ASSERTION] = ERROR_INVALID_FXA_ASSERTION;
SERVER_ERRNO_TO_ERROR[ERRNO_UNKNOWN_CODE] = ERROR_UNKNOWN_CODE;
SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_CODE] = ERROR_INCORRECT_CODE;
SERVER_ERRNO_TO_ERROR[ERRNO_EXPIRED_CODE] = ERROR_EXPIRED_CODE;
SERVER_ERRNO_TO_ERROR[ERRNO_OAUTH_INVALID_TOKEN] = ERROR_OAUTH_INVALID_TOKEN;
SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_REQUEST_PARAM] = ERROR_INVALID_REQUEST_PARAM;
SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_RESPONSE_TYPE] = ERROR_INVALID_RESPONSE_TYPE;
SERVER_ERRNO_TO_ERROR[ERRNO_UNAUTHORIZED] = ERROR_UNAUTHORIZED;
SERVER_ERRNO_TO_ERROR[ERRNO_FORBIDDEN] = ERROR_FORBIDDEN;
SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_CONTENT_TYPE] = ERROR_INVALID_CONTENT_TYPE;
// Map internal errors to more generic error classes for consumers
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_ACCOUNT_ALREADY_EXISTS] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_ACCOUNT_DOES_NOT_EXIST] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_ALREADY_SIGNED_IN_USER] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_ENDPOINT_NO_LONGER_SUPPORTED] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INCORRECT_API_VERSION] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INCORRECT_EMAIL_CASE] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INCORRECT_KEY_RETRIEVAL_METHOD] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INCORRECT_LOGIN_METHOD] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_EMAIL] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_AUDIENCE] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_AUTH_TOKEN] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_AUTH_TIMESTAMP] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_AUTH_NONCE] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_BODY_PARAMETERS] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_PASSWORD] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_VERIFICATION_CODE] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_REFRESH_AUTH_VALUE] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_REQUEST_SIGNATURE] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INTERNAL_INVALID_USER] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_MISSING_BODY_PARAMETERS] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_MISSING_CONTENT_LENGTH] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_NO_TOKEN_SESSION] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_NO_SILENT_REFRESH_AUTH] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_NOT_VALID_JSON_BODY] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_PERMISSION_DENIED] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_REQUEST_BODY_TOO_LARGE] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_UNVERIFIED_ACCOUNT] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_UI_ERROR] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_UI_REQUEST] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_OFFLINE] = ERROR_NETWORK;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_SERVER_ERROR] = ERROR_NETWORK;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_TOO_MANY_CLIENT_REQUESTS] = ERROR_NETWORK;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_SERVICE_TEMP_UNAVAILABLE] = ERROR_NETWORK;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_PARSE] = ERROR_NETWORK;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_NETWORK] = ERROR_NETWORK;
// oauth
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INCORRECT_CLIENT_SECRET] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INCORRECT_REDIRECT_URI] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_FXA_ASSERTION] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_UNKNOWN_CODE] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INCORRECT_CODE] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_EXPIRED_CODE] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_OAUTH_INVALID_TOKEN] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_REQUEST_PARAM] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_RESPONSE_TYPE] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_UNAUTHORIZED] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_FORBIDDEN] = ERROR_AUTH_ERROR;
ERROR_TO_GENERAL_ERROR_CLASS[ERROR_INVALID_CONTENT_TYPE] = ERROR_AUTH_ERROR;

View File

@ -146,6 +146,12 @@ this.FxAccountsOAuthGrantClient.prototype = {
return resolve(body);
}
if (typeof body.errno === 'number') {
// Offset oauth server errnos to avoid conflict with other FxA server errnos
body.errno += OAUTH_SERVER_ERRNO_OFFSET;
} else if (body.errno) {
body.errno = ERRNO_UNKNOWN_ERROR;
}
return reject(new FxAccountsOAuthGrantClientError(body));
};

View File

@ -210,7 +210,7 @@ add_task(function test_getCertificate() {
// This test, unlike the rest, uses an un-mocked FxAccounts instance.
// However, we still need to pass an object to the constructor to
// force it to expose "internal".
let fxa = new FxAccounts({onlySetInternal: true})
let fxa = new FxAccounts({onlySetInternal: true});
let credentials = {
email: "foo@example.com",
uid: "1234@lcip.org",
@ -690,6 +690,7 @@ add_test(function test_sign_out_with_remote_error() {
add_test(function test_getOAuthToken() {
let fxa = new MockFxAccounts();
let alice = getTestUser("alice");
alice.verified = true;
let getTokenFromAssertionCalled = false;
fxa.internal._d_signCertificate.resolve("cert1");
@ -718,13 +719,140 @@ add_test(function test_getOAuthToken() {
});
add_test(function test_getOAuthToken_missing_scope() {
Services.prefs.setCharPref("identity.fxaccounts.remote.oauth.uri", "https://example.com/v1");
add_test(function test_getOAuthToken_invalid_param() {
let fxa = new MockFxAccounts();
do_check_throws_message(() => {
fxa.getOAuthToken();
}, "Missing 'scope' option");
run_next_test();
fxa.getOAuthToken()
.then(null, err => {
do_check_eq(err.message, "INVALID_PARAMETER");
run_next_test();
});
});
add_test(function test_getOAuthToken_misconfigure_oauth_uri() {
let fxa = new MockFxAccounts();
Services.prefs.deleteBranch("identity.fxaccounts.remote.oauth.uri");
fxa.getOAuthToken()
.then(null, err => {
do_check_eq(err.message, "INVALID_PARAMETER");
// revert the pref
Services.prefs.setCharPref("identity.fxaccounts.remote.oauth.uri", "https://example.com/v1");
run_next_test();
});
});
add_test(function test_getOAuthToken_no_account() {
let fxa = new MockFxAccounts();
fxa.internal.currentAccountState.getUserAccountData = function () {
return Promise.resolve(null);
};
fxa.getOAuthToken({ scope: "profile" })
.then(null, err => {
do_check_eq(err.message, "NO_ACCOUNT");
run_next_test();
});
});
add_test(function test_getOAuthToken_unverified() {
let fxa = new MockFxAccounts();
let alice = getTestUser("alice");
fxa.setSignedInUser(alice).then(() => {
fxa.getOAuthToken({ scope: "profile" })
.then(null, err => {
do_check_eq(err.message, "UNVERIFIED_ACCOUNT");
run_next_test();
});
});
});
add_test(function test_getOAuthToken_network_error() {
let fxa = new MockFxAccounts();
let alice = getTestUser("alice");
alice.verified = true;
fxa.internal._d_signCertificate.resolve("cert1");
// create a mock oauth client
let client = new FxAccountsOAuthGrantClient({
serverURL: "http://example.com/v1",
client_id: "abc123"
});
client.getTokenFromAssertion = function () {
return Promise.reject(new FxAccountsOAuthGrantClientError({
error: ERROR_NETWORK,
errno: ERRNO_NETWORK
}));
};
fxa.setSignedInUser(alice).then(() => {
fxa.getOAuthToken({ scope: "profile", client: client })
.then(null, err => {
do_check_eq(err.message, "NETWORK_ERROR");
do_check_eq(err.details.errno, ERRNO_NETWORK);
run_next_test();
});
});
});
add_test(function test_getOAuthToken_auth_error() {
let fxa = new MockFxAccounts();
let alice = getTestUser("alice");
alice.verified = true;
fxa.internal._d_signCertificate.resolve("cert1");
// create a mock oauth client
let client = new FxAccountsOAuthGrantClient({
serverURL: "http://example.com/v1",
client_id: "abc123"
});
client.getTokenFromAssertion = function () {
return Promise.reject(new FxAccountsOAuthGrantClientError({
error: ERROR_INVALID_FXA_ASSERTION,
errno: ERRNO_INVALID_FXA_ASSERTION
}));
};
fxa.setSignedInUser(alice).then(() => {
fxa.getOAuthToken({ scope: "profile", client: client })
.then(null, err => {
do_check_eq(err.message, "AUTH_ERROR");
do_check_eq(err.details.errno, ERRNO_INVALID_FXA_ASSERTION);
run_next_test();
});
});
});
add_test(function test_getOAuthToken_unknown_error() {
let fxa = new MockFxAccounts();
let alice = getTestUser("alice");
alice.verified = true;
fxa.internal._d_signCertificate.resolve("cert1");
// create a mock oauth client
let client = new FxAccountsOAuthGrantClient({
serverURL: "http://example.com/v1",
client_id: "abc123"
});
client.getTokenFromAssertion = function () {
return Promise.reject("boom");
};
fxa.setSignedInUser(alice).then(() => {
fxa.getOAuthToken({ scope: "profile", client: client })
.then(null, err => {
do_check_eq(err.message, "UNKNOWN_ERROR");
run_next_test();
});
});
});
/*

View File

@ -121,7 +121,7 @@ add_test(function serverErrorResponse () {
function (e) {
do_check_eq(e.name, "FxAccountsOAuthGrantClientError");
do_check_eq(e.code, 400);
do_check_eq(e.errno, 104);
do_check_eq(e.errno, ERRNO_INVALID_FXA_ASSERTION);
do_check_eq(e.error, "Bad Request");
do_check_eq(e.message, "Unauthorized");
run_next_test();
@ -181,6 +181,28 @@ add_test(function onCompleteRequestError () {
);
});
add_test(function incorrectErrno() {
let client = new FxAccountsOAuthGrantClient(CLIENT_OPTIONS);
let response = {
status: 400,
body: "{ \"code\": 400, \"errno\": \"bad errno\", \"error\": \"Bad Request\", \"message\": \"Unauthorized\", \"reason\": \"Invalid fxa assertion\" }",
};
client._Request = new mockResponse(response);
client.getTokenFromAssertion("blah", "scope")
.then(
null,
function (e) {
do_check_eq(e.name, "FxAccountsOAuthGrantClientError");
do_check_eq(e.code, 400);
do_check_eq(e.errno, ERRNO_UNKNOWN_ERROR);
do_check_eq(e.error, "Bad Request");
do_check_eq(e.message, "Unauthorized");
run_next_test();
}
);
});
add_test(function constructorTests() {
validationHelper(undefined,
"Error: Missing configuration options");