diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index c011f81d27..a65305733e 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -198,10 +198,18 @@ Meteor.methods({changePassword: function (options) { if (!verifier) throw new Meteor.Error(400, "Invalid verifier"); - // XXX this should invalidate all login tokens other than the current one - // (or it should assign a new login token, replacing existing ones) + // It would be better if this removed ALL existing tokens and replaced + // the token for the current connection with a new one, but that would + // be tricky, so we'll settle for just replacing all tokens other than + // the one for the current connection. + var currentToken = Accounts._getLoginToken(this.connection.id); Meteor.users.update({_id: this.userId}, - {$set: {'services.password.srp': verifier}}); + { + $set: { 'services.password.srp': verifier }, + $pull: { + 'services.resume.loginTokens': { hashedToken: { $ne: currentToken } } + } + }); var ret = {passwordChanged: true}; if (serialized) diff --git a/packages/accounts-password/password_tests.js b/packages/accounts-password/password_tests.js index 0d7e7139fc..bf772c5a54 100644 --- a/packages/accounts-password/password_tests.js +++ b/packages/accounts-password/password_tests.js @@ -191,6 +191,65 @@ if (Meteor.isClient) (function () { logoutStep ]); + testAsyncMulti("passwords - changing password logs out other clients", [ + function (test, expect) { + this.username = Random.id(); + this.email = Random.id() + '-intercept@example.com'; + this.password = 'password'; + this.password2 = 'password2'; + Accounts.createUser( + { username: this.username, email: this.email, password: this.password }, + loggedInAs(this.username, test, expect)); + }, + // Log in a second connection as this user. + function (test, expect) { + var self = this; + + // copied from livedata/client_convenience.js + var ddpUrl = '/'; + if (typeof __meteor_runtime_config__ !== "undefined") { + if (__meteor_runtime_config__.DDP_DEFAULT_CONNECTION_URL) + ddpUrl = __meteor_runtime_config__.DDP_DEFAULT_CONNECTION_URL; + } + // XXX can we get the url from the existing connection somehow + // instead? + + self.secondConn = DDP.connect(ddpUrl); + self.secondConn.call('login', + { user: { username: self.username }, password: self.password }, + expect(function (err, result) { + test.isFalse(err); + self.secondConn.setUserId(result.id); + test.isTrue(self.secondConn.userId()); + + self.secondConn.onReconnect = function () { + self.secondConn.apply( + 'login', + [{ resume: result.token }], + { wait: true }, + function (err, result) { + self.secondConn.setUserId(result && result.id || null); + } + ); + }; + })); + }, + function (test, expect) { + var self = this; + Accounts.changePassword(self.password, self.password2, expect(function (err) { + test.isFalse(err); + })); + }, + // Now that we've changed the password, wait until the second + // connection gets logged out. + function (test, expect) { + var self = this; + pollUntil(expect, function () { + return self.secondConn.userId() === null; + }, 10 * 1000, 100); + } + ]); + testAsyncMulti("passwords - new user hooks", [ function (test, expect) {