diff --git a/browser/components/loop/MozLoopService.jsm b/browser/components/loop/MozLoopService.jsm index 4c2325dd76eb..4526fdd89182 100644 --- a/browser/components/loop/MozLoopService.jsm +++ b/browser/components/loop/MozLoopService.jsm @@ -60,6 +60,7 @@ let gLocalizedStrings = null; let gInitializeTimer = null; let gFxAOAuthClientPromise = null; let gFxAOAuthClient = null; +let gFxAOAuthTokenData = null; /** * Internal helper methods and state @@ -195,7 +196,10 @@ let MozLoopServiceInternal = { 2 * 32, true); } - return gHawkClient.request(path, method, credentials, payloadObj); + return gHawkClient.request(path, method, credentials, payloadObj).catch(error => { + console.error("Loop hawkRequest error:", error); + throw error; + }); }, /** @@ -492,7 +496,7 @@ let MozLoopServiceInternal = { }, error => { gFxAOAuthClientPromise = null; - return error; + throw error; } ); @@ -500,7 +504,9 @@ let MozLoopServiceInternal = { }), /** - * Params => web flow => code + * Get the OAuth client and do the authorization web flow to get an OAuth code. + * + * @return {Promise} */ promiseFxAOAuthAuthorization: function() { let deferred = Promise.defer(); @@ -510,13 +516,38 @@ let MozLoopServiceInternal = { client.launchWebFlow(); }, error => { - Cu.reportError(error); + console.error(error); deferred.reject(error); } ); return deferred.promise; }, + /** + * Get the OAuth token using the OAuth code and state. + * + * The caller should approperiately handle 4xx errors (which should lead to a logout) + * and 5xx or connectivity issues with messaging to try again later. + * + * @param {String} code + * @param {String} state + * + * @return {Promise} resolving with OAuth token data. + */ + promiseFxAOAuthToken: function(code, state) { + if (!code || !state) { + throw new Error("promiseFxAOAuthToken: code and state are required."); + } + + let payload = { + code: code, + state: state, + }; + return this.hawkRequest("/fxa-oauth/token", "POST", payload).then(response => { + return JSON.parse(response.body); + }); + }, + /** * Called once gFxAOAuthClient fires onComplete. * @@ -558,8 +589,14 @@ this.MozLoopService = { return MozLoopServiceInternal; }, + get gFxAOAuthTokenData() { + return gFxAOAuthTokenData; + }, + resetFxA: function() { gFxAOAuthClientPromise = null; + gFxAOAuthClient = null; + gFxAOAuthTokenData = null; }, #endif @@ -740,6 +777,13 @@ this.MozLoopService = { logInToFxA: function() { return MozLoopServiceInternal.promiseFxAOAuthAuthorization().then(response => { return MozLoopServiceInternal.promiseFxAOAuthToken(response.code, response.state); + }).then(tokenData => { + gFxAOAuthTokenData = tokenData; + return tokenData; + }, + error => { + gFxAOAuthTokenData = null; + throw error; }); }, diff --git a/browser/components/loop/test/mochitest/browser_fxa_login.js b/browser/components/loop/test/mochitest/browser_fxa_login.js index 35640976ea28..7c7b4d00ec84 100644 --- a/browser/components/loop/test/mochitest/browser_fxa_login.js +++ b/browser/components/loop/test/mochitest/browser_fxa_login.js @@ -90,3 +90,122 @@ add_task(function* invalidState() { ok(error, "The login promise should be rejected due to invalid state"); }); }); + +add_task(function* basicRegistration() { + MozLoopService.resetFxA(); + let params = { + client_id: "client_id", + content_uri: BASE_URL + "/content", + oauth_uri: BASE_URL + "/oauth", + profile_uri: BASE_URL + "/profile", + state: "state", + }; + yield promiseOAuthParamsSetup(BASE_URL, params); + + let tokenData = yield MozLoopService.internal.promiseFxAOAuthToken("code1", "state"); + is(tokenData.access_token, "code1_access_token", "Check access_token"); + is(tokenData.scope, "profile", "Check scope"); + is(tokenData.token_type, "bearer", "Check token_type"); +}); + +add_task(function* registrationWithInvalidState() { + MozLoopService.resetFxA(); + let params = { + client_id: "client_id", + content_uri: BASE_URL + "/content", + oauth_uri: BASE_URL + "/oauth", + profile_uri: BASE_URL + "/profile", + state: "invalid_state", + }; + yield promiseOAuthParamsSetup(BASE_URL, params); + + let tokenPromise = MozLoopService.internal.promiseFxAOAuthToken("code1", "state"); + yield tokenPromise.then(body => { + ok(false, "Promise should have rejected"); + }, + error => { + is(error.code, 400, "Check error code"); + }); +}); + +add_task(function* registrationWith401() { + MozLoopService.resetFxA(); + let params = { + client_id: "client_id", + content_uri: BASE_URL + "/content", + oauth_uri: BASE_URL + "/oauth", + profile_uri: BASE_URL + "/profile", + state: "state", + test_error: "token_401", + }; + yield promiseOAuthParamsSetup(BASE_URL, params); + + let tokenPromise = MozLoopService.internal.promiseFxAOAuthToken("code1", "state"); + yield tokenPromise.then(body => { + ok(false, "Promise should have rejected"); + }, + error => { + is(error.code, 401, "Check error code"); + }); +}); + +add_task(function* basicAuthorizationAndRegistration() { + MozLoopService.resetFxA(); + let params = { + client_id: "client_id", + content_uri: BASE_URL + "/content", + oauth_uri: BASE_URL + "/oauth", + profile_uri: BASE_URL + "/profile", + state: "state", + }; + yield promiseOAuthParamsSetup(BASE_URL, params); + + let tokenData = yield MozLoopService.logInToFxA(); + ise(tokenData.access_token, "code1_access_token", "Check access_token"); + ise(tokenData.scope, "profile", "Check scope"); + ise(tokenData.token_type, "bearer", "Check token_type"); +}); + +add_task(function* loginWithParams401() { + MozLoopService.resetFxA(); + let params = { + client_id: "client_id", + content_uri: BASE_URL + "/content", + oauth_uri: BASE_URL + "/oauth", + profile_uri: BASE_URL + "/profile", + state: "state", + test_error: "params_401", + }; + yield promiseOAuthParamsSetup(BASE_URL, params); + + let loginPromise = MozLoopService.logInToFxA(); + yield loginPromise.then(tokenData => { + ok(false, "Promise should have rejected"); + }, + error => { + ise(error.code, 401, "Check error code"); + ise(MozLoopService.gFxAOAuthTokenData, null, "Check there is no saved token data"); + }); +}); + +add_task(function* loginWithRegistration401() { + MozLoopService.resetFxA(); + let params = { + client_id: "client_id", + content_uri: BASE_URL + "/content", + oauth_uri: BASE_URL + "/oauth", + profile_uri: BASE_URL + "/profile", + state: "state", + test_error: "token_401", + }; + yield promiseOAuthParamsSetup(BASE_URL, params); + + let loginPromise = MozLoopService.logInToFxA(); + yield loginPromise.then(tokenData => { + ok(false, "Promise should have rejected"); + }, + error => { + ise(error.code, 401, "Check error code"); + ise(MozLoopService.gFxAOAuthTokenData, null, "Check there is no saved token data"); + }); +}); diff --git a/browser/components/loop/test/mochitest/browser_loop_fxa_server.js b/browser/components/loop/test/mochitest/browser_loop_fxa_server.js index 305ebbfc933e..397fa0a7c616 100644 --- a/browser/components/loop/test/mochitest/browser_loop_fxa_server.js +++ b/browser/components/loop/test/mochitest/browser_loop_fxa_server.js @@ -69,7 +69,7 @@ add_task(function* token_request() { let request = yield promiseToken("my_code", params.state); ise(request.status, 200, "Check token response status"); ise(request.response.access_token, "my_code_access_token", "Check access_token"); - ise(request.response.scopes, "", "Check scopes"); + ise(request.response.scope, "profile", "Check scope"); ise(request.response.token_type, "bearer", "Check token_type"); }); diff --git a/browser/components/loop/test/mochitest/loop_fxa.sjs b/browser/components/loop/test/mochitest/loop_fxa.sjs index 72a239d8d1cd..5293f36dceb6 100644 --- a/browser/components/loop/test/mochitest/loop_fxa.sjs +++ b/browser/components/loop/test/mochitest/loop_fxa.sjs @@ -76,10 +76,14 @@ function params(request, response) { return; } - let origin = request.scheme + "://" + request.host + ":" + request.port; - let params = JSON.parse(getSharedState("/fxa-oauth/params") || "{}"); + if (params.test_error && params.test_error == "params_401") { + response.setStatusLine(request.httpVersion, 401, "Unauthorized"); + response.write("401 Unauthorized"); + return; + } + // Warn if required parameters are missing. for (let paramName of REQUIRED_PARAMS) { if (!(paramName in params)) { @@ -113,6 +117,13 @@ function oauth_authorization(request, response) { */ function token(request, response) { let params = JSON.parse(getSharedState("/fxa-oauth/params") || "{}"); + + if (params.test_error && params.test_error == "token_401") { + response.setStatusLine(request.httpVersion, 401, "Unauthorized"); + response.write("401 Unauthorized"); + return; + } + let body = NetUtil.readInputStreamToString(request.bodyInputStream, request.bodyInputStream.available()); let payload = JSON.parse(body); @@ -124,7 +135,7 @@ function token(request, response) { let tokenData = { access_token: payload.code + "_access_token", - scopes: "", + scope: "profile", token_type: "bearer", }; response.setHeader("Content-Type", "application/json; charset=utf-8", false);