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 1ee91d533a
commit 2423e4fc3a
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); objectCopy(aOptions, this);
// id and origin are required // 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"); throw new Error("id and origin are required for RP watch context");
} }
this.principal = aPrincipal;
// default for no loggedInUser is undefined, not null // default for no loggedInUser is undefined, not null
this.loggedInUser = aOptions.loggedInUser; this.loggedInUser = aOptions.loggedInUser;
@ -187,8 +189,8 @@ this.DOMIdentity = {
/* /*
* Create a new RPWatchContext, and update the context maps. * Create a new RPWatchContext, and update the context maps.
*/ */
newContext: function(message, targetMM) { newContext: function(message, targetMM, principal) {
let context = new RPWatchContext(message, targetMM); let context = new RPWatchContext(message, targetMM, principal);
this._serviceContexts.set(message.id, context); this._serviceContexts.set(message.id, context);
this._mmContexts.set(targetMM, message.id); this._mmContexts.set(targetMM, message.id);
return context; return context;
@ -276,16 +278,16 @@ this.DOMIdentity = {
switch (aMessage.name) { switch (aMessage.name) {
// RP // RP
case "Identity:RP:Watch": case "Identity:RP:Watch":
this._watch(msg, targetMM); this._watch(msg, targetMM, aMessage.principal);
break; break;
case "Identity:RP:Unwatch": case "Identity:RP:Unwatch":
this._unwatch(msg, targetMM); this._unwatch(msg, targetMM);
break; break;
case "Identity:RP:Request": case "Identity:RP:Request":
this._request(msg, targetMM); this._request(msg);
break; break;
case "Identity:RP:Logout": case "Identity:RP:Logout":
this._logout(msg, targetMM); this._logout(msg);
break; break;
// IDP // IDP
case "Identity:IDP:BeginProvisioning": case "Identity:IDP:BeginProvisioning":
@ -359,9 +361,9 @@ this.DOMIdentity = {
ppmm = null; ppmm = null;
}, },
_watch: function DOMIdentity__watch(message, targetMM) { _watch: function DOMIdentity__watch(message, targetMM, principal) {
log("DOMIdentity__watch: " + message.id); log("DOMIdentity__watch: " + message.id + " - " + principal);
let context = this.newContext(message, targetMM); let context = this.newContext(message, targetMM, principal);
this.getService(message).RP.watch(context); 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.DATA_FORMAT_VERSION = 1;
this.DEFAULT_STORAGE_FILENAME = "signedInUser.json"; 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_NO_SILENT_REFRESH_AUTH = "NO_SILENT_REFRESH_AUTH";
this.ERROR_NOT_VALID_JSON_BODY = "NOT_VALID_JSON_BODY"; this.ERROR_NOT_VALID_JSON_BODY = "NOT_VALID_JSON_BODY";
this.ERROR_OFFLINE = "OFFLINE"; this.ERROR_OFFLINE = "OFFLINE";
this.ERROR_PERMISSION_DENIED = "PERMISSION_DENIED";
this.ERROR_REQUEST_BODY_TOO_LARGE = "REQUEST_BODY_TOO_LARGE"; this.ERROR_REQUEST_BODY_TOO_LARGE = "REQUEST_BODY_TOO_LARGE";
this.ERROR_SERVER_ERROR = "SERVER_ERROR"; this.ERROR_SERVER_ERROR = "SERVER_ERROR";
this.ERROR_TOO_MANY_CLIENT_REQUESTS = "TOO_MANY_CLIENT_REQUESTS"; 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/Promise.jsm");
Cu.import("resource://gre/modules/FxAccountsCommon.js"); Cu.import("resource://gre/modules/FxAccountsCommon.js");
XPCOMUtils.defineLazyServiceGetter(this, "permissionManager",
"@mozilla.org/permissionmanager;1",
"nsIPermissionManager");
this.FxAccountsManager = { this.FxAccountsManager = {
init: function() { init: function() {
@ -175,7 +179,7 @@ this.FxAccountsManager = {
* FxAccountsClient.signCertificate() * FxAccountsClient.signCertificate()
* See the latter method for possible (error code, errno) pairs. * 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; let errno = (reason ? reason.errno : NaN) || NaN;
// If the previously valid email/password pair is no longer valid ... // If the previously valid email/password pair is no longer valid ...
if (errno == ERRNO_INVALID_AUTH_TOKEN) { if (errno == ERRNO_INVALID_AUTH_TOKEN) {
@ -186,34 +190,42 @@ this.FxAccountsManager = {
if (exists) { if (exists) {
return this.getAccount().then( return this.getAccount().then(
(user) => { (user) => {
return this._refreshAuthentication(aAudience, user.email, true); return this._refreshAuthentication(aAudience, user.email,
} aPrincipal,
); true /* logoutOnFailure */);
// ... 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);
} }
); );
} }
} }
); );
// 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); return Promise.reject(reason);
}, },
_getAssertion: function(aAudience) { _getAssertion: function(aAudience, aPrincipal) {
return this._fxAccounts.getAssertion(aAudience).then( return this._fxAccounts.getAssertion(aAudience).then(
(result) => { (result) => {
if (aPrincipal) {
this._addPermission(aPrincipal);
}
return result; return result;
}, },
(reason) => { (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 * 2) The person typing can't prove knowledge of the password used
* to log in. Failure should do nothing. * to log in. Failure should do nothing.
*/ */
_refreshAuthentication: function(aAudience, aEmail, logoutOnFailure=false) { _refreshAuthentication: function(aAudience, aEmail, aPrincipal,
logoutOnFailure=false) {
this._refreshing = true; this._refreshing = true;
return this._uiRequest(UI_REQUEST_REFRESH_AUTH, return this._uiRequest(UI_REQUEST_REFRESH_AUTH,
aAudience, aEmail).then( aAudience, aPrincipal, aEmail).then(
(assertion) => { (assertion) => {
this._refreshing = false; this._refreshing = false;
return assertion; 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"] let ui = Cc["@mozilla.org/fxaccounts/fxaccounts-ui-glue;1"]
.createInstance(Ci.nsIFxAccountsUIGlue); .createInstance(Ci.nsIFxAccountsUIGlue);
if (!ui[aRequest]) { if (!ui[aRequest]) {
@ -309,7 +322,7 @@ this.FxAccountsManager = {
// Even if we get a successful result from the UI, the account will // Even if we get a successful result from the UI, the account will
// most likely be unverified, so we cannot get an assertion. // most likely be unverified, so we cannot get an assertion.
if (result && result.verified) { if (result && result.verified) {
return this._getAssertion(aAudience); return this._getAssertion(aAudience, aPrincipal);
} }
return this._error(ERROR_UNVERIFIED_ACCOUNT, { 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 -- // -- API --
signIn: function(aEmail, aPassword) { signIn: function(aEmail, aPassword) {
@ -469,22 +493,31 @@ this.FxAccountsManager = {
* trigger the sign in flow. * trigger the sign in flow.
* else if we were asked to refresh and the grace period is up: * else if we were asked to refresh and the grace period is up:
* trigger the refresh flow. * trigger the refresh flow.
* else ask the core code for an assertion, which might itself * else:
* trigger either the sign in or refresh flows (if our account * request user permission to share an assertion if we don't have it
* changed on the server). * 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: * aOptions can include:
* refreshAuthentication - (bool) Force re-auth. * refreshAuthentication - (bool) Force re-auth.
* silent - (bool) Prevent any UI interaction. * silent - (bool) Prevent any UI interaction.
* I.e., try to get an automatic assertion. * I.e., try to get an automatic assertion.
*/ */
getAssertion: function(aAudience, aOptions) { getAssertion: function(aAudience, aPrincipal, aOptions) {
if (!aAudience) { if (!aAudience) {
return this._error(ERROR_INVALID_AUDIENCE); return this._error(ERROR_INVALID_AUDIENCE);
} }
if (Services.io.offline) { if (Services.io.offline) {
return this._error(ERROR_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( return this.getAccount().then(
user => { user => {
if (user) { if (user) {
@ -506,21 +539,42 @@ this.FxAccountsManager = {
if (aOptions.silent) { if (aOptions.silent) {
return this._error(ERROR_NO_SILENT_REFRESH_AUTH); 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) { 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 // Third case: we are all set *locally*. Probably we just return
// the assertion, but the attempt might lead to the server saying // the assertion, but the attempt might lead to the server saying
// we are deleted or have a new password, which will trigger a flow. // 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"); log.debug("No signed in user");
if (aOptions && aOptions.silent) { if (aOptions && aOptions.silent) {
return Promise.resolve(null); 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 * Register a listener for a given windowID as a result of a call to
* navigator.id.watch(). * navigator.id.watch().
* *
* @param aCaller * @param aRPCaller
* (Object) an object that represents the caller document, and * (Object) an object that represents the caller document, and
* is expected to have properties: * is expected to have properties:
* - id (unique, e.g. uuid) * - id (unique, e.g. uuid)
@ -128,7 +133,9 @@ FxAccountsService.prototype = {
// Log the user in, if possible, and then call ready(). // Log the user in, if possible, and then call ready().
let runnable = { let runnable = {
run: () => { run: () => {
this.fxAccountsManager.getAssertion(aRpCaller.audience, {silent:true}).then( this.fxAccountsManager.getAssertion(aRpCaller.audience,
aRpCaller.principal,
{ silent:true }).then(
data => { data => {
if (data) { if (data) {
this.doLogin(aRpCaller.id, data); this.doLogin(aRpCaller.id, data);
@ -161,10 +168,10 @@ FxAccountsService.prototype = {
* navigator.id.request(). * navigator.id.request().
* *
* @param aRPId * @param aRPId
* (integer) the id of the doc object obtained in .watch() * (integer) the id of the doc object obtained in .watch()
* *
* @param aOptions * @param aOptions
* (Object) options including privacyPolicy, termsOfService * (Object) options including privacyPolicy, termsOfService
*/ */
request: function request(aRPId, aOptions) { request: function request(aRPId, aOptions) {
aOptions = aOptions || {}; aOptions = aOptions || {};
@ -174,12 +181,25 @@ FxAccountsService.prototype = {
return; 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); let options = makeMessageObject(rp);
objectCopy(aOptions, options); objectCopy(aOptions, options);
log.debug("get assertion for " + rp.audience); log.debug("get assertion for " + rp.audience);
this.fxAccountsManager.getAssertion(rp.audience, options).then( this.fxAccountsManager.getAssertion(rp.audience, rp.principal, options)
.then(
data => { data => {
log.debug("got assertion for " + rp.audience + ": " + data); log.debug("got assertion for " + rp.audience + ": " + data);
this.doLogin(aRPId, data); this.doLogin(aRPId, data);
@ -193,6 +213,16 @@ FxAccountsService.prototype = {
} }
this.doError(aRPId, error); this.doError(aRPId, error);
} }
)
.then(
() => {
this.cleanupRPRequest(rp);
}
)
.catch(
() => {
this.cleanupRPRequest(rp);
}
); );
}, },