mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Add token expiration test and fix bugs so it passes.
Also make all token-expiration-related times configurable via Accounts.config.
This commit is contained in:
@@ -93,18 +93,24 @@ Accounts.callLoginMethod = function (options) {
|
||||
} else {
|
||||
Meteor.connection.onReconnect = function() {
|
||||
reconnected = true;
|
||||
Accounts.callLoginMethod({
|
||||
methodArguments: [{resume: result.token}],
|
||||
// Reconnect quiescence ensures that the user doesn't see an
|
||||
// intermediate state before the login method finishes. So we don't
|
||||
// need to show a logging-in animation.
|
||||
_suppressLoggingIn: true,
|
||||
userCallback: function (error) {
|
||||
if (error) {
|
||||
makeClientLoggedOut();
|
||||
}
|
||||
options.userCallback(error);
|
||||
}});
|
||||
// XXX A DDP disconnect message would be helpful here, to know if our
|
||||
// connection got closed because of an expired token.
|
||||
if (! result.tokenExpires)
|
||||
result.tokenExpires = Accounts._tokenExpiration(new Date());
|
||||
if (! Accounts._tokenExpiresSoon(result.tokenExpires)) {
|
||||
Accounts.callLoginMethod({
|
||||
methodArguments: [{resume: result.token}],
|
||||
// Reconnect quiescence ensures that the user doesn't see an
|
||||
// intermediate state before the login method finishes. So we don't
|
||||
// need to show a logging-in animation.
|
||||
_suppressLoggingIn: true,
|
||||
userCallback: function (error) {
|
||||
if (error) {
|
||||
makeClientLoggedOut();
|
||||
}
|
||||
options.userCallback(error);
|
||||
}});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -23,11 +23,15 @@ Accounts._options = {};
|
||||
// Seconds until a login token expires.
|
||||
// - _tokenExpirationInterval {Number}
|
||||
// How often (in seconds) to check for expired tokens
|
||||
// - _minTokenLifetime {Number}
|
||||
// The minimum number of seconds until a token expires in order for the
|
||||
// client to be willing to connect with that token.
|
||||
//
|
||||
Accounts.config = function(options) {
|
||||
// validate option keys
|
||||
var VALID_KEYS = ["sendVerificationEmail", "forbidClientAccountCreation",
|
||||
"_tokenLifetime", "_tokenExpirationInterval"];
|
||||
"_tokenLifetime", "_tokenExpirationInterval",
|
||||
"_minTokenLifetime"];
|
||||
_.each(_.keys(options), function (key) {
|
||||
if (!_.contains(VALID_KEYS, key)) {
|
||||
throw new Error("Accounts.config: Invalid key: " + key);
|
||||
@@ -41,6 +45,8 @@ Accounts.config = function(options) {
|
||||
throw new Error("Can't set `" + key + "` more than once");
|
||||
} else {
|
||||
Accounts._options[key] = options[key];
|
||||
if (key === "_tokenExpirationInterval" && Meteor.isServer)
|
||||
initExpireTokenInterval();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -72,10 +78,19 @@ Accounts.LoginCancelledError.prototype = new Error();
|
||||
Accounts.LoginCancelledError.prototype.name = 'Accounts.LoginCancelledError';
|
||||
|
||||
// how long (in seconds) until a login token expires
|
||||
// XXX maybe should be configurable
|
||||
DEFAULT_TOKEN_LIFETIME = 604800; // one week
|
||||
// We don't try to auto-login with a token that is going to expire within
|
||||
// MIN_TOKEN_LIFETIME seconds, to avoid abrupt disconnects from expiring tokens.
|
||||
var DEFAULT_MIN_TOKEN_LIFETIME = 3600; // one hour
|
||||
|
||||
Accounts._tokenExpiration = function (when) {
|
||||
var tokenLifetime = Accounts._options._tokenLifetime ||
|
||||
DEFAULT_TOKEN_LIFETIME;
|
||||
return new Date(when.getTime() + tokenLifetime * 1000);
|
||||
};
|
||||
|
||||
Accounts._tokenExpiresSoon = function (when) {
|
||||
var minLifetime = Accounts._options._minTokenLifetime ||
|
||||
DEFAULT_MIN_TOKEN_LIFETIME;
|
||||
return new Date() > (new Date(when) - minLifetime * 1000);
|
||||
};
|
||||
|
||||
@@ -175,9 +175,8 @@ var removeLoginToken = function (userId, loginToken) {
|
||||
|
||||
// Deletes expired tokens from the database and closes all open connections
|
||||
// associated with these tokens.
|
||||
// Exported for tests.
|
||||
var expireTokens = Accounts._expireTokens = function () {
|
||||
var tokenLifetime = Accounts._options.tokenLifetime || DEFAULT_TOKEN_LIFETIME;
|
||||
var expireTokens = function () {
|
||||
var tokenLifetime = Accounts._options._tokenLifetime || DEFAULT_TOKEN_LIFETIME;
|
||||
var oldestValidDate = new Date(new Date() - tokenLifetime * 1000);
|
||||
var usersWithExpiredTokens = Meteor.users.find({
|
||||
"services.resume.loginTokens.when": { $lt: oldestValidDate }
|
||||
@@ -186,6 +185,8 @@ var expireTokens = Accounts._expireTokens = function () {
|
||||
var oldTokens = [];
|
||||
usersWithExpiredTokens.forEach(function (user) {
|
||||
_.each(user.services.resume.loginTokens, function (token) {
|
||||
if (typeof token.when === "number")
|
||||
token.when = new Date(token.when);
|
||||
if (token.when < oldestValidDate)
|
||||
oldTokens.push(token.token);
|
||||
});
|
||||
@@ -199,16 +200,21 @@ var expireTokens = Accounts._expireTokens = function () {
|
||||
when: { $lt: oldestValidDate }
|
||||
}
|
||||
}
|
||||
});
|
||||
}, { multi: true });
|
||||
|
||||
Meteor.server._closeAllForTokens(oldTokens);
|
||||
};
|
||||
|
||||
Meteor.users._ensureIndex("services.resume.loginTokens.when", { sparse: true });
|
||||
var expireInterval = Accounts._options.tokenExpirationInterval ||
|
||||
DEFAULT_EXPIRE_TOKENS_INTERVAL;
|
||||
Meteor.setInterval(expireTokens, expireInterval * 1000);
|
||||
|
||||
var expireTokenInterval;
|
||||
initExpireTokenInterval = function () {
|
||||
if (expireTokenInterval)
|
||||
Meteor.clearInterval(expireTokenInterval);
|
||||
var expirePeriod = Accounts._options._tokenExpirationInterval ||
|
||||
DEFAULT_EXPIRE_TOKENS_INTERVAL;
|
||||
expireTokenInterval = Meteor.setInterval(expireTokens, expirePeriod * 1000);
|
||||
};
|
||||
initExpireTokenInterval();
|
||||
|
||||
///
|
||||
/// CREATE USER HOOKS
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
// browser.
|
||||
|
||||
var lastLoginTokenWhenPolled;
|
||||
// We don't try to auto-login with a token that is going to expire within
|
||||
// MIN_TOKEN_LIFETIME seconds, to avoid abrupt disconnects from expiring tokens.
|
||||
var MIN_TOKEN_LIFETIME = 3600; // one hour
|
||||
|
||||
// Login with a Meteor access token. This is the only public function
|
||||
// here.
|
||||
@@ -76,8 +73,7 @@ var storedUserId = function() {
|
||||
|
||||
var unstoreLoginTokenIfExpiresSoon = function () {
|
||||
var tokenExpires = Meteor._localStorage.getItem(loginTokenExpiresKey);
|
||||
if (tokenExpires &&
|
||||
new Date() > (new Date(tokenExpires) - MIN_TOKEN_LIFETIME * 1000))
|
||||
if (tokenExpires && Accounts._tokenExpiresSoon(tokenExpires))
|
||||
unstoreLoginToken();
|
||||
};
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ Accounts.registerLoginHandler(function (options) {
|
||||
|
||||
return {
|
||||
token: stampedLoginToken.token,
|
||||
tokenExpires: Accounts._tokenExpires(stampedLoginToken.when),
|
||||
tokenExpires: Accounts._tokenExpiration(stampedLoginToken.when),
|
||||
id: user._id
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
Accounts.config({
|
||||
_tokenLifetime: 5,
|
||||
_tokenExpirationInterval: 5,
|
||||
_minTokenLifetime: 1
|
||||
});
|
||||
|
||||
if (Meteor.isClient) (function () {
|
||||
|
||||
// XXX note, only one test can do login/logout things at once! for
|
||||
@@ -255,6 +261,7 @@ if (Meteor.isClient) (function () {
|
||||
test.equal(result, null);
|
||||
}));
|
||||
},
|
||||
logoutStep,
|
||||
function(test, expect) {
|
||||
var expectLoginError = expect(function (err) {
|
||||
test.isTrue(err);
|
||||
@@ -267,8 +274,35 @@ if (Meteor.isClient) (function () {
|
||||
Meteor.loginWithToken(token, expectLoginError);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
logoutStep,
|
||||
function(test, expect) {
|
||||
// Test that login tokens get expired. We should get logged out when a
|
||||
// token expires, and not be able to log in again with the same token.
|
||||
var expectLoggedOut = expect(function () {
|
||||
test.equal(Meteor.user(), undefined);
|
||||
});
|
||||
var expectLoginError = expect(function (err) {
|
||||
test.isTrue(err);
|
||||
});
|
||||
var expectNoError = expect(function (err) {
|
||||
test.equal(err, undefined);
|
||||
});
|
||||
var expectToken = expect(function (token) {
|
||||
test.isTrue(token);
|
||||
});
|
||||
var token;
|
||||
Meteor.loginWithPassword(username, password2, function (error) {
|
||||
expectNoError(error);
|
||||
token = Accounts._storedLoginToken();
|
||||
});
|
||||
Meteor.setTimeout(function () {
|
||||
expectToken(token);
|
||||
expectLoggedOut();
|
||||
Meteor.loginWithToken(token, expectLoginError);
|
||||
}, 10*1000);
|
||||
},
|
||||
logoutStep
|
||||
]);
|
||||
|
||||
}) ();
|
||||
|
||||
@@ -238,7 +238,6 @@ var Session = function (server, version) {
|
||||
self._universalSubs = [];
|
||||
|
||||
self.userId = null;
|
||||
self.loginToken = null;
|
||||
|
||||
// Per-connection scratch area. This is only used internally, but we
|
||||
// should have real and documented API for this sort of thing someday.
|
||||
@@ -666,8 +665,8 @@ _.extend(Session.prototype, {
|
||||
|
||||
_setLoginToken: function (newToken) {
|
||||
var self = this;
|
||||
var oldToken = self.loginToken;
|
||||
self.loginToken = newToken;
|
||||
var oldToken = self.sessionData.loginToken;
|
||||
self.sessionData.loginToken = newToken;
|
||||
self.server._loginTokenChanged(self, newToken, oldToken);
|
||||
},
|
||||
|
||||
@@ -1371,13 +1370,21 @@ _.extend(Server.prototype, {
|
||||
var self = this;
|
||||
_.each(tokens, function (token) {
|
||||
if (_.has(self.sessionsByLoginToken, token)) {
|
||||
var destroyedIds = [];
|
||||
_.each(self.sessionsByLoginToken[token], function (sessionId) {
|
||||
if (_.indexOf(excludeSessions, sessionId) === -1) {
|
||||
self.sessions[sessionId].cleanup();
|
||||
self.sessions[sessionId].destroy();
|
||||
delete self.sessions[sessionId];
|
||||
destroyedIds.push(sessionId);
|
||||
}
|
||||
});
|
||||
self.sessionsByLoginToken[token] = _.filter(
|
||||
self.sessionsByLoginToken[token],
|
||||
function (sessionId) {
|
||||
return _.indexOf(destroyedIds, sessionId) === -1;
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user