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:
parent
f355f66114
commit
f1e31ca50c
|
@ -807,6 +807,31 @@ module.exports = function(User) {
|
||||||
UserModel.validatesUniquenessOf('username', {message: 'User already exists'});
|
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;
|
return UserModel;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -832,6 +857,8 @@ module.exports = function(User) {
|
||||||
|
|
||||||
// Delete old sessions once email is updated
|
// Delete old sessions once email is updated
|
||||||
User.observe('before save', function beforeEmailUpdate(ctx, next) {
|
User.observe('before save', function beforeEmailUpdate(ctx, next) {
|
||||||
|
if (!ctx.Model.app.get('logoutSessionsOnSensitiveChanges')) return next();
|
||||||
|
|
||||||
var emailChanged;
|
var emailChanged;
|
||||||
if (ctx.isNewInstance) return next();
|
if (ctx.isNewInstance) return next();
|
||||||
if (!ctx.where && !ctx.instance) 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) {
|
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.instance && !ctx.data) return next();
|
||||||
if (!ctx.hookState.originalUserData) return next();
|
if (!ctx.hookState.originalUserData) return next();
|
||||||
|
|
||||||
|
|
|
@ -590,6 +590,7 @@ function createTestApp(testToken, settings, done) {
|
||||||
}, settings.token);
|
}, settings.token);
|
||||||
|
|
||||||
var app = loopback();
|
var app = loopback();
|
||||||
|
app.set('logoutSessionsOnSensitiveChanges', true);
|
||||||
|
|
||||||
app.use(cookieParser('secret'));
|
app.use(cookieParser('secret'));
|
||||||
app.use(loopback.token(tokenSettings));
|
app.use(loopback.token(tokenSettings));
|
||||||
|
@ -652,6 +653,7 @@ function createTestApp(testToken, settings, done) {
|
||||||
|
|
||||||
function givenLocalTokenModel() {
|
function givenLocalTokenModel() {
|
||||||
var app = loopback({ localRegistry: true, loadBuiltinModels: true });
|
var app = loopback({ localRegistry: true, loadBuiltinModels: true });
|
||||||
|
app.set('logoutSessionsOnSensitiveChanges', true);
|
||||||
app.dataSource('db', { connector: 'memory' });
|
app.dataSource('db', { connector: 'memory' });
|
||||||
|
|
||||||
var User = app.registry.getModel('User');
|
var User = app.registry.getModel('User');
|
||||||
|
|
|
@ -718,6 +718,7 @@ describe('app', function() {
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
app = loopback();
|
app = loopback();
|
||||||
|
app.set('logoutSessionsOnSensitiveChanges', true);
|
||||||
app.dataSource('db', {
|
app.dataSource('db', {
|
||||||
connector: 'memory'
|
connector: 'memory'
|
||||||
});
|
});
|
||||||
|
@ -922,6 +923,7 @@ describe('app', function() {
|
||||||
var AUTH_MODELS = ['User', 'ACL', 'AccessToken', 'Role', 'RoleMapping'];
|
var AUTH_MODELS = ['User', 'ACL', 'AccessToken', 'Role', 'RoleMapping'];
|
||||||
var app = loopback({ localRegistry: true, loadBuiltinModels: true });
|
var app = loopback({ localRegistry: true, loadBuiltinModels: true });
|
||||||
require('../lib/builtin-models')(app.registry);
|
require('../lib/builtin-models')(app.registry);
|
||||||
|
app.set('logoutSessionsOnSensitiveChanges', true);
|
||||||
var db = app.dataSource('db', { connector: 'memory' });
|
var db = app.dataSource('db', { connector: 'memory' });
|
||||||
|
|
||||||
app.enableAuth({ dataSource: 'db' });
|
app.enableAuth({ dataSource: 'db' });
|
||||||
|
@ -937,6 +939,7 @@ describe('app', function() {
|
||||||
|
|
||||||
it('detects already configured subclass of a required model', function() {
|
it('detects already configured subclass of a required model', function() {
|
||||||
var app = loopback({ localRegistry: true, loadBuiltinModels: true });
|
var app = loopback({ localRegistry: true, loadBuiltinModels: true });
|
||||||
|
app.set('logoutSessionsOnSensitiveChanges', true);
|
||||||
var db = app.dataSource('db', { connector: 'memory' });
|
var db = app.dataSource('db', { connector: 'memory' });
|
||||||
var Customer = app.registry.createModel('Customer', {}, { base: 'User' });
|
var Customer = app.registry.createModel('Customer', {}, { base: 'User' });
|
||||||
app.model(Customer, { dataSource: 'db' });
|
app.model(Customer, { dataSource: 'db' });
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"port": 3000,
|
"port": 3000,
|
||||||
"host": "0.0.0.0",
|
"host": "0.0.0.0",
|
||||||
|
"logoutSessionsOnSensitiveChanges": true,
|
||||||
"legacyExplorer": false
|
"legacyExplorer": false
|
||||||
}
|
}
|
|
@ -23,6 +23,7 @@
|
||||||
"disableStackTrace": false
|
"disableStackTrace": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"logoutSessionsOnSensitiveChanges": true,
|
||||||
"legacyExplorer": false
|
"legacyExplorer": false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,5 +11,6 @@
|
||||||
"limit": "8kb"
|
"limit": "8kb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"logoutSessionsOnSensitiveChanges": true,
|
||||||
"legacyExplorer": false
|
"legacyExplorer": false
|
||||||
}
|
}
|
|
@ -11,5 +11,6 @@
|
||||||
"limit": "8kb"
|
"limit": "8kb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"logoutSessionsOnSensitiveChanges": true,
|
||||||
"legacyExplorer": false
|
"legacyExplorer": false
|
||||||
}
|
}
|
|
@ -856,6 +856,7 @@ describe.onServer('Remote Methods', function() {
|
||||||
|
|
||||||
function setupAppAndRequest() {
|
function setupAppAndRequest() {
|
||||||
app = loopback({localRegistry: true, loadBuiltinModels: true});
|
app = loopback({localRegistry: true, loadBuiltinModels: true});
|
||||||
|
app.set('logoutSessionsOnSensitiveChanges', true);
|
||||||
|
|
||||||
app.dataSource('db', {connector: 'memory'});
|
app.dataSource('db', {connector: 'memory'});
|
||||||
|
|
||||||
|
|
|
@ -462,6 +462,7 @@ describe('Replication over REST', function() {
|
||||||
|
|
||||||
function setupServer(done) {
|
function setupServer(done) {
|
||||||
serverApp = loopback();
|
serverApp = loopback();
|
||||||
|
serverApp.set('logoutSessionsOnSensitiveChanges', true);
|
||||||
serverApp.enableAuth();
|
serverApp.enableAuth();
|
||||||
|
|
||||||
serverApp.dataSource('db', { connector: 'memory' });
|
serverApp.dataSource('db', { connector: 'memory' });
|
||||||
|
@ -514,6 +515,7 @@ describe('Replication over REST', function() {
|
||||||
|
|
||||||
function setupClient() {
|
function setupClient() {
|
||||||
clientApp = loopback();
|
clientApp = loopback();
|
||||||
|
clientApp.set('logoutSessionsOnSensitiveChanges', true);
|
||||||
clientApp.dataSource('db', { connector: 'memory' });
|
clientApp.dataSource('db', { connector: 'memory' });
|
||||||
clientApp.dataSource('remote', {
|
clientApp.dataSource('remote', {
|
||||||
connector: 'remote',
|
connector: 'remote',
|
||||||
|
|
|
@ -13,6 +13,7 @@ describe('loopback.rest', function() {
|
||||||
// override the global app object provided by test/support.js
|
// override the global app object provided by test/support.js
|
||||||
// and create a local one that does not share state with other tests
|
// and create a local one that does not share state with other tests
|
||||||
app = loopback({ localRegistry: true, loadBuiltinModels: true });
|
app = loopback({ localRegistry: true, loadBuiltinModels: true });
|
||||||
|
app.set('logoutSessionsOnSensitiveChanges', true);
|
||||||
var db = app.dataSource('db', { connector: 'memory' });
|
var db = app.dataSource('db', { connector: 'memory' });
|
||||||
MyModel = app.registry.createModel('MyModel');
|
MyModel = app.registry.createModel('MyModel');
|
||||||
MyModel.attachTo(db);
|
MyModel.attachTo(db);
|
||||||
|
|
|
@ -23,6 +23,7 @@ describe('role model', function() {
|
||||||
// Use local app registry to ensure models are isolated to avoid
|
// Use local app registry to ensure models are isolated to avoid
|
||||||
// pollutions from other tests
|
// pollutions from other tests
|
||||||
app = loopback({ localRegistry: true, loadBuiltinModels: true });
|
app = loopback({ localRegistry: true, loadBuiltinModels: true });
|
||||||
|
app.set('logoutSessionsOnSensitiveChanges', true);
|
||||||
app.dataSource('db', { connector: 'memory' });
|
app.dataSource('db', { connector: 'memory' });
|
||||||
|
|
||||||
ACL = app.registry.getModel('ACL');
|
ACL = app.registry.getModel('ACL');
|
||||||
|
@ -735,6 +736,7 @@ describe('role model', function() {
|
||||||
describe('isOwner', function() {
|
describe('isOwner', function() {
|
||||||
it('supports app-local model registry', function(done) {
|
it('supports app-local model registry', function(done) {
|
||||||
var app = loopback({ localRegistry: true, loadBuiltinModels: true });
|
var app = loopback({ localRegistry: true, loadBuiltinModels: true });
|
||||||
|
app.set('logoutSessionsOnSensitiveChanges', true);
|
||||||
app.dataSource('db', { connector: 'memory' });
|
app.dataSource('db', { connector: 'memory' });
|
||||||
// attach all auth-related models to 'db' datasource
|
// attach all auth-related models to 'db' datasource
|
||||||
app.enableAuth({ dataSource: 'db' });
|
app.enableAuth({ dataSource: 'db' });
|
||||||
|
|
|
@ -23,6 +23,7 @@ loopback.User.settings.saltWorkFactor = 4;
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
this.app = app = loopback();
|
this.app = app = loopback();
|
||||||
|
app.set('logoutSessionsOnSensitiveChanges', true);
|
||||||
|
|
||||||
// setup default data sources
|
// setup default data sources
|
||||||
loopback.setDefaultDataSourceForType('db', {
|
loopback.setDefaultDataSourceForType('db', {
|
||||||
|
|
|
@ -31,6 +31,7 @@ describe('User', function() {
|
||||||
// override the global app object provided by test/support.js
|
// override the global app object provided by test/support.js
|
||||||
// and create a local one that does not share state with other tests
|
// and create a local one that does not share state with other tests
|
||||||
app = loopback({ localRegistry: true, loadBuiltinModels: true });
|
app = loopback({ localRegistry: true, loadBuiltinModels: true });
|
||||||
|
app.set('logoutSessionsOnSensitiveChanges', true);
|
||||||
app.dataSource('db', { connector: 'memory' });
|
app.dataSource('db', { connector: 'memory' });
|
||||||
|
|
||||||
// setup Email model, it's needed by User tests
|
// 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) {
|
function assertPreservedTokens(done) {
|
||||||
AccessToken.find({where: {userId: user.id}}, function(err, tokens) {
|
AccessToken.find({where: {userId: user.id}}, function(err, tokens) {
|
||||||
if (err) return done(err);
|
if (err) return done(err);
|
||||||
|
|
Loading…
Reference in New Issue