Skip to content

Commit 8061d12

Browse files
authored
Merge pull request #2693 from strongloop/sessEmail
Invalidate sessions after email change
2 parents 6752dd3 + bcc2d99 commit 8061d12

File tree

2 files changed

+341
-0
lines changed

2 files changed

+341
-0
lines changed

common/models/user.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,34 @@ module.exports = function(User) {
673673
next();
674674
});
675675

676+
// Delete old sessions once email is updated
677+
UserModel.observe('before save', function beforeEmailUpdate(ctx, next) {
678+
if (ctx.isNewInstance) return next();
679+
if (!ctx.where && !ctx.instance) return next();
680+
var where = ctx.where || { id: ctx.instance.id };
681+
ctx.Model.find({ where: where }, function(err, userInstances) {
682+
if (err) return next(err);
683+
ctx.hookState.originalUserData = userInstances.map(function(u) {
684+
return { id: u.id, email: u.email };
685+
});
686+
next();
687+
});
688+
});
689+
690+
UserModel.observe('after save', function afterEmailUpdate(ctx, next) {
691+
if (!ctx.Model.relations.accessTokens) return next();
692+
var AccessToken = ctx.Model.relations.accessTokens.modelTo;
693+
var newEmail = (ctx.instance || ctx.data).email;
694+
if (!ctx.hookState.originalUserData) return next();
695+
var idsToExpire = ctx.hookState.originalUserData.filter(function(u) {
696+
return u.email !== newEmail;
697+
}).map(function(u) {
698+
return u.id;
699+
});
700+
if (!idsToExpire.length) return next();
701+
AccessToken.deleteAll({ userId: { inq: idsToExpire }}, next);
702+
});
703+
676704
UserModel.remoteMethod(
677705
'login',
678706
{

test/user.test.js

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,12 @@ describe('User', function() {
4545
app.model(Email, { dataSource: 'email' });
4646

4747
// attach User and related models
48+
// forceId is set to false for the purpose of updating the same affected user within the
49+
// `Email Update` test cases.
4850
User = app.registry.createModel('TestUser', {}, {
4951
base: 'User',
5052
http: { path: 'test-users' },
53+
forceId: false,
5154
});
5255
app.model(User, { dataSource: 'db' });
5356

@@ -1804,6 +1807,316 @@ describe('User', function() {
18041807
});
18051808
});
18061809

1810+
describe('Email Update', function() {
1811+
describe('User changing email property', function() {
1812+
var user, originalUserToken1, originalUserToken2, newUserCreated;
1813+
var currentEmailCredentials = { email: '[email protected]', password: 'bar' };
1814+
var updatedEmailCredentials = { email: '[email protected]', password: 'bar' };
1815+
var newUserCred = { email: '[email protected]', password: 'newpass' };
1816+
1817+
beforeEach('create user then login', function createAndLogin(done) {
1818+
async.series([
1819+
function createUserWithOriginalEmail(next) {
1820+
User.create(currentEmailCredentials, function(err, userCreated) {
1821+
if (err) return next(err);
1822+
user = userCreated;
1823+
next();
1824+
});
1825+
},
1826+
function firstLoginWithOriginalEmail(next) {
1827+
User.login(currentEmailCredentials, function(err, accessToken1) {
1828+
if (err) return next(err);
1829+
assert(accessToken1.userId);
1830+
originalUserToken1 = accessToken1.id;
1831+
next();
1832+
});
1833+
},
1834+
function secondLoginWithOriginalEmail(next) {
1835+
User.login(currentEmailCredentials, function(err, accessToken2) {
1836+
if (err) return next(err);
1837+
assert(accessToken2.userId);
1838+
originalUserToken2 = accessToken2.id;
1839+
next();
1840+
});
1841+
},
1842+
], done);
1843+
});
1844+
1845+
it('invalidates sessions when email is changed using `updateAttributes`', function(done) {
1846+
user.updateAttributes(
1847+
{ email: updatedEmailCredentials.email },
1848+
function(err, userInstance) {
1849+
if (err) return done(err);
1850+
assertNoAccessTokens(done);
1851+
});
1852+
});
1853+
1854+
it('invalidates sessions when email is changed using `replaceAttributes`', function(done) {
1855+
user.replaceAttributes(updatedEmailCredentials, function(err, userInstance) {
1856+
if (err) return done(err);
1857+
assertNoAccessTokens(done);
1858+
});
1859+
});
1860+
1861+
it('invalidates sessions when email is changed using `updateOrCreate`', function(done) {
1862+
User.updateOrCreate({
1863+
id: user.id,
1864+
email: updatedEmailCredentials.email,
1865+
password: updatedEmailCredentials.password,
1866+
}, function(err, userInstance) {
1867+
if (err) return done(err);
1868+
assertNoAccessTokens(done);
1869+
});
1870+
});
1871+
1872+
it('invalidates sessions when the email is changed using `replaceById`', function(done) {
1873+
User.replaceById(user.id, updatedEmailCredentials, function(err, userInstance) {
1874+
if (err) return done(err);
1875+
assertNoAccessTokens(done);
1876+
});
1877+
});
1878+
1879+
it('invalidates sessions when the email is changed using `replaceOrCreate`', function(done) {
1880+
User.replaceOrCreate({
1881+
id: user.id,
1882+
email: updatedEmailCredentials.email,
1883+
password: updatedEmailCredentials.password,
1884+
}, function(err, userInstance) {
1885+
if (err) return done(err);
1886+
assertNoAccessTokens(done);
1887+
});
1888+
});
1889+
1890+
it('keeps sessions AS IS if firstName is added using `updateAttributes`', function(done) {
1891+
user.updateAttributes({ 'firstName': 'Janny' }, function(err, userInstance) {
1892+
if (err) return done(err);
1893+
assertUntouchedTokens(done);
1894+
});
1895+
});
1896+
1897+
it('keeps sessions AS IS if firstName is added using `replaceAttributes`', function(done) {
1898+
user.replaceAttributes({
1899+
email: currentEmailCredentials.email,
1900+
password: currentEmailCredentials.password,
1901+
firstName: 'Candy',
1902+
}, function(err, userInstance) {
1903+
if (err) return done(err);
1904+
assertUntouchedTokens(done);
1905+
});
1906+
});
1907+
1908+
it('keeps sessions AS IS if firstName is added using `updateOrCreate`', function(done) {
1909+
User.updateOrCreate({
1910+
id: user.id,
1911+
firstName: 'Loay',
1912+
email: currentEmailCredentials.email,
1913+
password: currentEmailCredentials.password,
1914+
}, function(err, userInstance) {
1915+
if (err) return done(err);
1916+
assertUntouchedTokens(done);
1917+
});
1918+
});
1919+
1920+
it('keeps sessions AS IS if firstName is added using `replaceById`', function(done) {
1921+
User.replaceById(
1922+
user.id,
1923+
{
1924+
firstName: 'Miroslav',
1925+
email: currentEmailCredentials.email,
1926+
password: currentEmailCredentials.password,
1927+
}, function(err, userInstance) {
1928+
if (err) return done(err);
1929+
assertUntouchedTokens(done);
1930+
});
1931+
});
1932+
1933+
it('keeps sessions AS IS if a new user is created using `create`', function(done) {
1934+
async.series([
1935+
function(next) {
1936+
User.create(newUserCred, function(err, newUserInstance) {
1937+
if (err) return done(err);
1938+
newUserCreated = newUserInstance;
1939+
next();
1940+
});
1941+
},
1942+
function(next) {
1943+
User.login(newUserCred, function(err, newAccessToken) {
1944+
if (err) return done(err);
1945+
assert(newAccessToken.id);
1946+
assertPreservedToken(next);
1947+
});
1948+
},
1949+
], done);
1950+
});
1951+
1952+
it('keeps sessions AS IS if a new user is created using `updateOrCreate`', function(done) {
1953+
async.series([
1954+
function(next) {
1955+
User.create(newUserCred, function(err, newUserInstance2) {
1956+
if (err) return done(err);
1957+
newUserCreated = newUserInstance2;
1958+
next();
1959+
});
1960+
},
1961+
function(next) {
1962+
User.login(newUserCred, function(err, newAccessToken2) {
1963+
if (err) return done(err);
1964+
assert(newAccessToken2.id);
1965+
assertPreservedToken(next);
1966+
});
1967+
},
1968+
], done);
1969+
});
1970+
1971+
function assertPreservedToken(done) {
1972+
AccessToken.find({ where: { userId: user.id }}, function(err, tokens) {
1973+
if (err) return done(err);
1974+
expect(tokens.length).to.equal(2);
1975+
expect([tokens[0].id, tokens[1].id]).to.have.members([originalUserToken1,
1976+
originalUserToken2]);
1977+
done();
1978+
});
1979+
};
1980+
1981+
function assertNoAccessTokens(done) {
1982+
AccessToken.find({ where: { userId: user.id }}, function(err, tokens) {
1983+
if (err) return done(err);
1984+
expect(tokens.length).to.equal(0);
1985+
done();
1986+
});
1987+
}
1988+
1989+
function assertUntouchedTokens(done) {
1990+
AccessToken.find({ where: { userId: user.id }}, function(err, tokens) {
1991+
if (err) return done(err);
1992+
expect(tokens.length).to.equal(2);
1993+
done();
1994+
});
1995+
}
1996+
});
1997+
1998+
describe('User not changing email property', function() {
1999+
var user1, user2, user3;
2000+
it('preserves other users\' sessions if their email is untouched', function(done) {
2001+
async.series([
2002+
function(next) {
2003+
User.create({ email: '[email protected]', password: 'u1pass' }, function(err, u1) {
2004+
if (err) return done(err);
2005+
User.create({ email: '[email protected]', password: 'u2pass' }, function(err, u2) {
2006+
if (err) return done(err);
2007+
User.create({ email: '[email protected]', password: 'u3pass' }, function(err, u3) {
2008+
if (err) return done(err);
2009+
user1 = u1;
2010+
user2 = u2;
2011+
user3 = u3;
2012+
next();
2013+
});
2014+
});
2015+
});
2016+
},
2017+
function(next) {
2018+
User.login(
2019+
{ email: '[email protected]', password: 'u1pass' },
2020+
function(err, accessToken1) {
2021+
if (err) return next(err);
2022+
User.login(
2023+
{ email: '[email protected]', password: 'u2pass' },
2024+
function(err, accessToken2) {
2025+
if (err) return next(err);
2026+
User.login({ email: '[email protected]', password: 'u3pass' },
2027+
function(err, accessToken3) {
2028+
if (err) return next(err);
2029+
next();
2030+
});
2031+
});
2032+
});
2033+
},
2034+
function(next) {
2035+
user2.updateAttribute('email', '[email protected]', function(err, userInstance) {
2036+
if (err) return next(err);
2037+
assert.equal(userInstance.email, '[email protected]');
2038+
next();
2039+
});
2040+
},
2041+
function(next) {
2042+
AccessToken.find({ where: { userId: user1.id }}, function(err, tokens1) {
2043+
if (err) return next(err);
2044+
AccessToken.find({ where: { userId: user2.id }}, function(err, tokens2) {
2045+
if (err) return next(err);
2046+
AccessToken.find({ where: { userId: user3.id }}, function(err, tokens3) {
2047+
if (err) return next(err);
2048+
2049+
expect(tokens1.length).to.equal(1);
2050+
expect(tokens2.length).to.equal(0);
2051+
expect(tokens3.length).to.equal(1);
2052+
next();
2053+
});
2054+
});
2055+
});
2056+
},
2057+
], done);
2058+
});
2059+
});
2060+
2061+
it('invalidates sessions after using updateAll', function(done) {
2062+
var userSpecial, userNormal;
2063+
async.series([
2064+
function createSpecialUser(next) {
2065+
User.create(
2066+
{ email: '[email protected]', password: 'pass1', name: 'Special' },
2067+
function(err, specialInstance) {
2068+
if (err) return next (err);
2069+
userSpecial = specialInstance;
2070+
next();
2071+
});
2072+
},
2073+
function createNormaluser(next) {
2074+
User.create(
2075+
{ email: '[email protected]', password: 'pass2' },
2076+
function(err, normalInstance) {
2077+
if (err) return next (err);
2078+
userNormal = normalInstance;
2079+
next();
2080+
});
2081+
},
2082+
function loginSpecialUser(next) {
2083+
User.login({ email: '[email protected]', password: 'pass1' }, function(err, ats) {
2084+
if (err) return next (err);
2085+
next();
2086+
});
2087+
},
2088+
function loginNormalUser(next) {
2089+
User.login({ email: '[email protected]', password: 'pass2' }, function(err, atn) {
2090+
if (err) return next (err);
2091+
next();
2092+
});
2093+
},
2094+
function updateSpecialUser(next) {
2095+
User.updateAll(
2096+
{ name: 'Special' },
2097+
{ email: '[email protected]' }, function(err, info) {
2098+
if (err) return next (err);
2099+
next();
2100+
});
2101+
},
2102+
function verifyTokensOfSpecialUser(next) {
2103+
AccessToken.find({ where: { userId: userSpecial.id }}, function(err, tokens1) {
2104+
if (err) return done (err);
2105+
expect(tokens1.length).to.equal(0);
2106+
next();
2107+
});
2108+
},
2109+
function verifyTokensOfNormalUser(next) {
2110+
AccessToken.find({ userId: userNormal.userId }, function(err, tokens2) {
2111+
if (err) return done (err);
2112+
expect(tokens2.length).to.equal(1);
2113+
next();
2114+
});
2115+
},
2116+
], done);
2117+
});
2118+
});
2119+
18072120
describe('password reset with/without email verification', function() {
18082121
it('allows resetPassword by email if email verification is required and done',
18092122
function(done) {

0 commit comments

Comments
 (0)