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"

This commit is contained in:
Fernando Jiménez 2014-07-11 16:13:32 +02:00
parent 4c2613d1f3
commit 4643d65bf1
4 changed files with 130 additions and 41 deletions

View File

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

View File

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

View File

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

View File

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