From 2423e4fc3aabc348e7ead780745ddc68f7f28e5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Jim=C3=A9nez?= Date: Fri, 11 Jul 2014 16:13:32 +0200 Subject: [PATCH] Bug 1028398 - FxA will silently provide user's email to privileged apps in 2.0. Part 2: Trigger forceAuth when new privileged app tries to get a FxA assertion. r=jedp" --- dom/identity/DOMIdentity.jsm | 20 ++-- services/fxaccounts/FxAccountsCommon.js | 3 + services/fxaccounts/FxAccountsManager.jsm | 108 ++++++++++++++++------ toolkit/identity/FirefoxAccounts.jsm | 40 +++++++- 4 files changed, 130 insertions(+), 41 deletions(-) diff --git a/dom/identity/DOMIdentity.jsm b/dom/identity/DOMIdentity.jsm index 54a8b9def11..e6c6e4a2b02 100644 --- a/dom/identity/DOMIdentity.jsm +++ b/dom/identity/DOMIdentity.jsm @@ -103,7 +103,7 @@ IDPAuthenticationContext.prototype = { } }; -function RPWatchContext(aOptions, aTargetMM) { +function RPWatchContext(aOptions, aTargetMM, aPrincipal) { objectCopy(aOptions, this); // id and origin are required @@ -111,6 +111,8 @@ function RPWatchContext(aOptions, aTargetMM) { throw new Error("id and origin are required for RP watch context"); } + this.principal = aPrincipal; + // default for no loggedInUser is undefined, not null this.loggedInUser = aOptions.loggedInUser; @@ -187,8 +189,8 @@ this.DOMIdentity = { /* * Create a new RPWatchContext, and update the context maps. */ - newContext: function(message, targetMM) { - let context = new RPWatchContext(message, targetMM); + newContext: function(message, targetMM, principal) { + let context = new RPWatchContext(message, targetMM, principal); this._serviceContexts.set(message.id, context); this._mmContexts.set(targetMM, message.id); return context; @@ -276,16 +278,16 @@ this.DOMIdentity = { switch (aMessage.name) { // RP case "Identity:RP:Watch": - this._watch(msg, targetMM); + this._watch(msg, targetMM, aMessage.principal); break; case "Identity:RP:Unwatch": this._unwatch(msg, targetMM); break; case "Identity:RP:Request": - this._request(msg, targetMM); + this._request(msg); break; case "Identity:RP:Logout": - this._logout(msg, targetMM); + this._logout(msg); break; // IDP case "Identity:IDP:BeginProvisioning": @@ -359,9 +361,9 @@ this.DOMIdentity = { ppmm = null; }, - _watch: function DOMIdentity__watch(message, targetMM) { - log("DOMIdentity__watch: " + message.id); - let context = this.newContext(message, targetMM); + _watch: function DOMIdentity__watch(message, targetMM, principal) { + log("DOMIdentity__watch: " + message.id + " - " + principal); + let context = this.newContext(message, targetMM, principal); this.getService(message).RP.watch(context); }, diff --git a/services/fxaccounts/FxAccountsCommon.js b/services/fxaccounts/FxAccountsCommon.js index 2bef093f361..d3a2756f2f9 100644 --- a/services/fxaccounts/FxAccountsCommon.js +++ b/services/fxaccounts/FxAccountsCommon.js @@ -60,6 +60,8 @@ XPCOMUtils.defineLazyGetter(this, 'logPII', function() { } }); +this.FXACCOUNTS_PERMISSION = "firefox-accounts"; + this.DATA_FORMAT_VERSION = 1; this.DEFAULT_STORAGE_FILENAME = "signedInUser.json"; @@ -141,6 +143,7 @@ this.ERROR_NO_TOKEN_SESSION = "NO_TOKEN_SESSION"; this.ERROR_NO_SILENT_REFRESH_AUTH = "NO_SILENT_REFRESH_AUTH"; this.ERROR_NOT_VALID_JSON_BODY = "NOT_VALID_JSON_BODY"; this.ERROR_OFFLINE = "OFFLINE"; +this.ERROR_PERMISSION_DENIED = "PERMISSION_DENIED"; this.ERROR_REQUEST_BODY_TOO_LARGE = "REQUEST_BODY_TOO_LARGE"; this.ERROR_SERVER_ERROR = "SERVER_ERROR"; this.ERROR_TOO_MANY_CLIENT_REQUESTS = "TOO_MANY_CLIENT_REQUESTS"; diff --git a/services/fxaccounts/FxAccountsManager.jsm b/services/fxaccounts/FxAccountsManager.jsm index c3609ee91c8..44e5ea9b1f8 100644 --- a/services/fxaccounts/FxAccountsManager.jsm +++ b/services/fxaccounts/FxAccountsManager.jsm @@ -21,6 +21,10 @@ Cu.import("resource://gre/modules/FxAccounts.jsm"); Cu.import("resource://gre/modules/Promise.jsm"); Cu.import("resource://gre/modules/FxAccountsCommon.js"); +XPCOMUtils.defineLazyServiceGetter(this, "permissionManager", + "@mozilla.org/permissionmanager;1", + "nsIPermissionManager"); + this.FxAccountsManager = { init: function() { @@ -175,7 +179,7 @@ this.FxAccountsManager = { * FxAccountsClient.signCertificate() * See the latter method for possible (error code, errno) pairs. */ - _handleGetAssertionError: function(reason, aAudience) { + _handleGetAssertionError: function(reason, aAudience, aPrincipal) { let errno = (reason ? reason.errno : NaN) || NaN; // If the previously valid email/password pair is no longer valid ... if (errno == ERRNO_INVALID_AUTH_TOKEN) { @@ -186,34 +190,42 @@ this.FxAccountsManager = { if (exists) { return this.getAccount().then( (user) => { - return this._refreshAuthentication(aAudience, user.email, true); - } - ); - // ... otherwise, the account was deleted, so ask for Sign In/Up - } else { - return this._localSignOut().then( - () => { - return this._uiRequest(UI_REQUEST_SIGN_IN_FLOW, aAudience); - }, - (reason) => { // reject primary problem, not signout failure - log.error("Signing out in response to server error threw: " + reason); - return this._error(reason); + return this._refreshAuthentication(aAudience, user.email, + aPrincipal, + true /* logoutOnFailure */); } ); } } ); + + // Otherwise, the account was deleted, so ask for Sign In/Up + return this._localSignOut().then( + () => { + return this._uiRequest(UI_REQUEST_SIGN_IN_FLOW, aAudience, + aPrincipal); + }, + (reason) => { + // reject primary problem, not signout failure + log.error("Signing out in response to server error threw: " + + reason); + return this._error(reason); + } + ); } return Promise.reject(reason); }, - _getAssertion: function(aAudience) { + _getAssertion: function(aAudience, aPrincipal) { return this._fxAccounts.getAssertion(aAudience).then( (result) => { + if (aPrincipal) { + this._addPermission(aPrincipal); + } return result; }, (reason) => { - return this._handleGetAssertionError(reason, aAudience); + return this._handleGetAssertionError(reason, aAudience, aPrincipal); } ); }, @@ -228,10 +240,11 @@ this.FxAccountsManager = { * 2) The person typing can't prove knowledge of the password used * to log in. Failure should do nothing. */ - _refreshAuthentication: function(aAudience, aEmail, logoutOnFailure=false) { + _refreshAuthentication: function(aAudience, aEmail, aPrincipal, + logoutOnFailure=false) { this._refreshing = true; return this._uiRequest(UI_REQUEST_REFRESH_AUTH, - aAudience, aEmail).then( + aAudience, aPrincipal, aEmail).then( (assertion) => { this._refreshing = false; return assertion; @@ -293,7 +306,7 @@ this.FxAccountsManager = { ); }, - _uiRequest: function(aRequest, aAudience, aParams) { + _uiRequest: function(aRequest, aAudience, aPrincipal, aParams) { let ui = Cc["@mozilla.org/fxaccounts/fxaccounts-ui-glue;1"] .createInstance(Ci.nsIFxAccountsUIGlue); if (!ui[aRequest]) { @@ -309,7 +322,7 @@ this.FxAccountsManager = { // Even if we get a successful result from the UI, the account will // most likely be unverified, so we cannot get an assertion. if (result && result.verified) { - return this._getAssertion(aAudience); + return this._getAssertion(aAudience, aPrincipal); } return this._error(ERROR_UNVERIFIED_ACCOUNT, { @@ -322,6 +335,17 @@ this.FxAccountsManager = { ); }, + _addPermission: function(aPrincipal) { + // This will fail from tests cause we are running them in the child + // process until we have chrome tests in b2g. Bug 797164. + try { + permissionManager.addFromPrincipal(aPrincipal, FXACCOUNTS_PERMISSION, + Ci.nsIPermissionManager.ALLOW_ACTION); + } catch (e) { + log.warn("Could not add permission " + e); + } + }, + // -- API -- signIn: function(aEmail, aPassword) { @@ -469,22 +493,31 @@ this.FxAccountsManager = { * trigger the sign in flow. * else if we were asked to refresh and the grace period is up: * trigger the refresh flow. - * else ask the core code for an assertion, which might itself - * trigger either the sign in or refresh flows (if our account - * changed on the server). + * else: + * request user permission to share an assertion if we don't have it + * already and ask the core code for an assertion, which might itself + * trigger either the sign in or refresh flows (if our account + * changed on the server). * * aOptions can include: * refreshAuthentication - (bool) Force re-auth. * silent - (bool) Prevent any UI interaction. * I.e., try to get an automatic assertion. */ - getAssertion: function(aAudience, aOptions) { + getAssertion: function(aAudience, aPrincipal, aOptions) { if (!aAudience) { return this._error(ERROR_INVALID_AUDIENCE); } if (Services.io.offline) { return this._error(ERROR_OFFLINE); } + + let secMan = Cc["@mozilla.org/scriptsecuritymanager;1"] + .getService(Ci.nsIScriptSecurityManager); + let uri = Services.io.newURI(aPrincipal.origin, null, null); + let principal = secMan.getAppCodebasePrincipal(uri, + aPrincipal.appId, aPrincipal.isInBrowserElement); + return this.getAccount().then( user => { if (user) { @@ -506,21 +539,42 @@ this.FxAccountsManager = { if (aOptions.silent) { return this._error(ERROR_NO_SILENT_REFRESH_AUTH); } - let secondsSinceAuth = (Date.now() / 1000) - this._activeSession.authAt; + let secondsSinceAuth = (Date.now() / 1000) - + this._activeSession.authAt; if (secondsSinceAuth > gracePeriod) { - return this._refreshAuthentication(aAudience, user.email); + return this._refreshAuthentication(aAudience, user.email, + principal, + false /* logoutOnFailure */); } } // Third case: we are all set *locally*. Probably we just return // the assertion, but the attempt might lead to the server saying // we are deleted or have a new password, which will trigger a flow. - return this._getAssertion(aAudience); + // Also we need to check if we have permission to get the assertion, + // otherwise we need to show the forceAuth UI to let the user know + // that the RP with no fxa permissions is trying to obtain an + // assertion. Once the user authenticates herself in the forceAuth UI + // the permission will be remembered by default. + let permission = permissionManager.testPermissionFromPrincipal( + principal, + FXACCOUNTS_PERMISSION + ); + if (permission == Ci.nsIPermissionManager.PROMPT_ACTION && + !this._refreshing) { + return this._refreshAuthentication(aAudience, user.email, + principal, + false /* logoutOnFailure */); + } else if (permission == Ci.nsIPermissionManager.DENY_ACTION && + !this._refreshing) { + return this._error(ERROR_PERMISSION_DENIED); + } + return this._getAssertion(aAudience, principal); } log.debug("No signed in user"); if (aOptions && aOptions.silent) { return Promise.resolve(null); } - return this._uiRequest(UI_REQUEST_SIGN_IN_FLOW, aAudience); + return this._uiRequest(UI_REQUEST_SIGN_IN_FLOW, aAudience, principal); } ); } diff --git a/toolkit/identity/FirefoxAccounts.jsm b/toolkit/identity/FirefoxAccounts.jsm index aca52bf77c7..33f872fc6c5 100644 --- a/toolkit/identity/FirefoxAccounts.jsm +++ b/toolkit/identity/FirefoxAccounts.jsm @@ -102,11 +102,16 @@ FxAccountsService.prototype = { } }, + cleanupRPRequest: function(aRp) { + aRp.pendingRequest = false; + this._rpFlows.set(aRp.id, aRp); + }, + /** * Register a listener for a given windowID as a result of a call to * navigator.id.watch(). * - * @param aCaller + * @param aRPCaller * (Object) an object that represents the caller document, and * is expected to have properties: * - id (unique, e.g. uuid) @@ -128,7 +133,9 @@ FxAccountsService.prototype = { // Log the user in, if possible, and then call ready(). let runnable = { run: () => { - this.fxAccountsManager.getAssertion(aRpCaller.audience, {silent:true}).then( + this.fxAccountsManager.getAssertion(aRpCaller.audience, + aRpCaller.principal, + { silent:true }).then( data => { if (data) { this.doLogin(aRpCaller.id, data); @@ -161,10 +168,10 @@ FxAccountsService.prototype = { * navigator.id.request(). * * @param aRPId - * (integer) the id of the doc object obtained in .watch() + * (integer) the id of the doc object obtained in .watch() * * @param aOptions - * (Object) options including privacyPolicy, termsOfService + * (Object) options including privacyPolicy, termsOfService */ request: function request(aRPId, aOptions) { aOptions = aOptions || {}; @@ -174,12 +181,25 @@ FxAccountsService.prototype = { return; } + // We check if we already have a pending request for this RP and in that + // case we just bail out. We don't want duplicated onlogin or oncancel + // events. + if (rp.pendingRequest) { + log.debug("request() already called"); + return; + } + + // Otherwise, we set the RP flow with the pending request flag. + rp.pendingRequest = true; + this._rpFlows.set(rp.id, rp); + let options = makeMessageObject(rp); objectCopy(aOptions, options); log.debug("get assertion for " + rp.audience); - this.fxAccountsManager.getAssertion(rp.audience, options).then( + this.fxAccountsManager.getAssertion(rp.audience, rp.principal, options) + .then( data => { log.debug("got assertion for " + rp.audience + ": " + data); this.doLogin(aRPId, data); @@ -193,6 +213,16 @@ FxAccountsService.prototype = { } this.doError(aRPId, error); } + ) + .then( + () => { + this.cleanupRPRequest(rp); + } + ) + .catch( + () => { + this.cleanupRPRequest(rp); + } ); },