Skip to content

Commit fa310d5

Browse files
committed
Invalidate sessions after email change
1 parent 14b8426 commit fa310d5

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
@@ -657,6 +657,34 @@ module.exports = function(User) {
657657
next();
658658
});
659659

660+
// Delete old sessions once email is updated
661+
UserModel.observe('before save', function beforeEmailUpdate(ctx, next) {
662+
if (ctx.isNewInstance) return next();
663+
if (!ctx.where && !ctx.instance) return next();
664+
var where = ctx.where || { id: ctx.instance.id };
665+
ctx.Model.find({ where: where }, function(err, userInstances) {
666+
if (err) return next(err);
667+
ctx.hookState.originalUserData = userInstances.map(function(u) {
668+
return { id: u.id, email: u.email };
669+
});
670+
next();
671+
});
672+
});
673+
674+
UserModel.observe('after save', function afterEmailUpdate(ctx, next) {
675+
if (!ctx.Model.relations.accessTokens) return next();
676+
var AccessToken = ctx.Model.relations.accessTokens.modelTo;
677+
var newEmail = (ctx.instance || ctx.data).email;
678+
if (!ctx.hookState.originalUserData) return next();
679+
var idsToExpire = ctx.hookState.originalUserData.filter(function(u) {
680+
return u.email !== newEmail;
681+
}).map(function(u) {
682+
return u.id;
683+
});
684+
if (!idsToExpire.length) return next();
685+
AccessToken.deleteAll({ userId: { inq: idsToExpire }}, next);
686+
});
687+
660688
UserModel.remoteMethod(
661689
'login',
662690
{

test/user.test.js

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

4343
// attach User and related models
44+
// forceId is set to false for the purpose of updating the same affected user within the
45+
// `Email Update` test cases.
4446
User = app.registry.createModel('TestUser', {}, {
4547
base: 'User',
4648
http: { path: 'test-users' },
49+
forceId: false,
4750
});
4851
app.model(User, { dataSource: 'db' });
4952

@@ -1808,6 +1811,316 @@ describe('User', function() {
18081811
});
18091812
});
18101813

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

0 commit comments

Comments
 (0)