Add app setting logoutSessionsOnSensitiveChanges

Disable invalidation of access tokens by default to restore backwards
compatibility with older 2.x versions.

Add a new application-wide flag logoutSessionsOnSensitiveChanges
that can be used to explicitly turn on/off the token invalidation.

When the flag is not set, a verbose warning is printed to nudge the user
to make a decision how they want to handle token invalidation.
This commit is contained in:
Miroslav Bajtoš 2017-01-17 10:44:25 +01:00
parent f355f66114
commit f1e31ca50c
13 changed files with 60 additions and 3 deletions

View File

@ -807,6 +807,31 @@ module.exports = function(User) {
UserModel.validatesUniquenessOf('username', {message: 'User already exists'});
}
UserModel.once('attached', function() {
if (UserModel.app.get('logoutSessionsOnSensitiveChanges') !== undefined)
return;
g.warn([
'',
'The user model %j is attached to an application that does not specify',
'whether other sessions should be invalidated when a password or',
'an email has changed. Session invalidation is important for security',
'reasons as it allows users to recover from various account breach',
'situations.',
'',
'We recommend turning this feature on by setting',
'"{{logoutSessionsOnSensitiveChanges}}" to {{true}} in',
'{{server/config.json}} (unless you have implemented your own solution',
'for token invalidation).',
'',
'We also recommend enabling "{{injectOptionsFromRemoteContext}}" in',
'%s\'s settings (typically via common/models/*.json file).',
'This setting is required for the invalidation algorithm to keep ',
'the current session valid.',
''
].join('\n'), UserModel.modelName, UserModel.modelName);
});
return UserModel;
};
@ -832,6 +857,8 @@ module.exports = function(User) {
// Delete old sessions once email is updated
User.observe('before save', function beforeEmailUpdate(ctx, next) {
if (!ctx.Model.app.get('logoutSessionsOnSensitiveChanges')) return next();
var emailChanged;
if (ctx.isNewInstance) return next();
if (!ctx.where && !ctx.instance) return next();
@ -872,6 +899,8 @@ module.exports = function(User) {
});
User.observe('after save', function afterEmailUpdate(ctx, next) {
if (!ctx.Model.app.get('logoutSessionsOnSensitiveChanges')) return next();
if (!ctx.instance && !ctx.data) return next();
if (!ctx.hookState.originalUserData) return next();

View File

@ -590,6 +590,7 @@ function createTestApp(testToken, settings, done) {
}, settings.token);
var app = loopback();
app.set('logoutSessionsOnSensitiveChanges', true);
app.use(cookieParser('secret'));
app.use(loopback.token(tokenSettings));
@ -652,6 +653,7 @@ function createTestApp(testToken, settings, done) {
function givenLocalTokenModel() {
var app = loopback({ localRegistry: true, loadBuiltinModels: true });
app.set('logoutSessionsOnSensitiveChanges', true);
app.dataSource('db', { connector: 'memory' });
var User = app.registry.getModel('User');

View File

@ -718,6 +718,7 @@ describe('app', function() {
beforeEach(function() {
app = loopback();
app.set('logoutSessionsOnSensitiveChanges', true);
app.dataSource('db', {
connector: 'memory'
});
@ -922,6 +923,7 @@ describe('app', function() {
var AUTH_MODELS = ['User', 'ACL', 'AccessToken', 'Role', 'RoleMapping'];
var app = loopback({ localRegistry: true, loadBuiltinModels: true });
require('../lib/builtin-models')(app.registry);
app.set('logoutSessionsOnSensitiveChanges', true);
var db = app.dataSource('db', { connector: 'memory' });
app.enableAuth({ dataSource: 'db' });
@ -937,6 +939,7 @@ describe('app', function() {
it('detects already configured subclass of a required model', function() {
var app = loopback({ localRegistry: true, loadBuiltinModels: true });
app.set('logoutSessionsOnSensitiveChanges', true);
var db = app.dataSource('db', { connector: 'memory' });
var Customer = app.registry.createModel('Customer', {}, { base: 'User' });
app.model(Customer, { dataSource: 'db' });

View File

@ -1,5 +1,6 @@
{
"port": 3000,
"host": "0.0.0.0",
"logoutSessionsOnSensitiveChanges": true,
"legacyExplorer": false
}
}

View File

@ -23,6 +23,7 @@
"disableStackTrace": false
}
},
"logoutSessionsOnSensitiveChanges": true,
"legacyExplorer": false
}

View File

@ -11,5 +11,6 @@
"limit": "8kb"
}
},
"logoutSessionsOnSensitiveChanges": true,
"legacyExplorer": false
}
}

View File

@ -11,5 +11,6 @@
"limit": "8kb"
}
},
"logoutSessionsOnSensitiveChanges": true,
"legacyExplorer": false
}
}

View File

@ -856,6 +856,7 @@ describe.onServer('Remote Methods', function() {
function setupAppAndRequest() {
app = loopback({localRegistry: true, loadBuiltinModels: true});
app.set('logoutSessionsOnSensitiveChanges', true);
app.dataSource('db', {connector: 'memory'});

View File

@ -462,6 +462,7 @@ describe('Replication over REST', function() {
function setupServer(done) {
serverApp = loopback();
serverApp.set('logoutSessionsOnSensitiveChanges', true);
serverApp.enableAuth();
serverApp.dataSource('db', { connector: 'memory' });
@ -514,6 +515,7 @@ describe('Replication over REST', function() {
function setupClient() {
clientApp = loopback();
clientApp.set('logoutSessionsOnSensitiveChanges', true);
clientApp.dataSource('db', { connector: 'memory' });
clientApp.dataSource('remote', {
connector: 'remote',

View File

@ -13,6 +13,7 @@ describe('loopback.rest', function() {
// override the global app object provided by test/support.js
// and create a local one that does not share state with other tests
app = loopback({ localRegistry: true, loadBuiltinModels: true });
app.set('logoutSessionsOnSensitiveChanges', true);
var db = app.dataSource('db', { connector: 'memory' });
MyModel = app.registry.createModel('MyModel');
MyModel.attachTo(db);

View File

@ -23,6 +23,7 @@ describe('role model', function() {
// Use local app registry to ensure models are isolated to avoid
// pollutions from other tests
app = loopback({ localRegistry: true, loadBuiltinModels: true });
app.set('logoutSessionsOnSensitiveChanges', true);
app.dataSource('db', { connector: 'memory' });
ACL = app.registry.getModel('ACL');
@ -735,6 +736,7 @@ describe('role model', function() {
describe('isOwner', function() {
it('supports app-local model registry', function(done) {
var app = loopback({ localRegistry: true, loadBuiltinModels: true });
app.set('logoutSessionsOnSensitiveChanges', true);
app.dataSource('db', { connector: 'memory' });
// attach all auth-related models to 'db' datasource
app.enableAuth({ dataSource: 'db' });

View File

@ -23,6 +23,7 @@ loopback.User.settings.saltWorkFactor = 4;
beforeEach(function() {
this.app = app = loopback();
app.set('logoutSessionsOnSensitiveChanges', true);
// setup default data sources
loopback.setDefaultDataSourceForType('db', {

View File

@ -31,6 +31,7 @@ describe('User', function() {
// override the global app object provided by test/support.js
// and create a local one that does not share state with other tests
app = loopback({ localRegistry: true, loadBuiltinModels: true });
app.set('logoutSessionsOnSensitiveChanges', true);
app.dataSource('db', { connector: 'memory' });
// setup Email model, it's needed by User tests
@ -2326,6 +2327,17 @@ describe('User', function() {
});
});
it('preserves all sessions when logoutSessionsOnSensitiveChanges is disabled',
function(done) {
app.set('logoutSessionsOnSensitiveChanges', false);
user.updateAttributes(
{email: updatedEmailCredentials.email},
function(err, userInstance) {
if (err) return done(err);
assertPreservedTokens(done);
});
});
function assertPreservedTokens(done) {
AccessToken.find({where: {userId: user.id}}, function(err, tokens) {
if (err) return done(err);