Skip to content
269 changes: 147 additions & 122 deletions src/user/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,146 +8,171 @@ const events = require('../events');
const batch = require('../batch');
const utils = require('../utils');

module.exports = function (User) {
User.auth = {};

User.auth.logAttempt = async function (uid, ip) {
if (!(parseInt(uid, 10) > 0)) {
return;
}
const exists = await db.exists(`lockout:${uid}`);
if (exists) {
throw new Error('[[error:account-locked]]');
}
const attempts = await db.increment(`loginAttempts:${uid}`);
if (attempts <= meta.config.loginAttempts) {
return await db.pexpire(`loginAttempts:${uid}`, 1000 * 60 * 60);
}
// Lock out the account
await db.set(`lockout:${uid}`, '');
const duration = 1000 * 60 * meta.config.lockoutDuration;

await db.delete(`loginAttempts:${uid}`);
await db.pexpire(`lockout:${uid}`, duration);
await events.log({
type: 'account-locked',
uid: uid,
ip: ip,
});
async function logAttempt(uid, ip) {
if (!(parseInt(uid, 10) > 0)) {
return;
}
const exists = await db.exists(`lockout:${uid}`);
if (exists) {
throw new Error('[[error:account-locked]]');
};

User.auth.getFeedToken = async function (uid) {
if (!(parseInt(uid, 10) > 0)) {
return;
}
const attempts = await db.increment(`loginAttempts:${uid}`);
if (attempts <= meta.config.loginAttempts) {
return await db.pexpire(`loginAttempts:${uid}`, 1000 * 60 * 60);
}
// Lock out the account
await db.set(`lockout:${uid}`, '');
const duration = 1000 * 60 * meta.config.lockoutDuration;

await db.delete(`loginAttempts:${uid}`);
await db.pexpire(`lockout:${uid}`, duration);
await events.log({
type: 'account-locked',
uid: uid,
ip: ip,
});
throw new Error('[[error:account-locked]]');
}

async function getFeedToken(User, uid) {
if (!(parseInt(uid, 10) > 0)) {
return;
}
const _token = await db.getObjectField(`user:${uid}`, 'rss_token');
const token = _token || utils.generateUUID();
if (!_token) {
await User.setUserField(uid, 'rss_token', token);
}
return token;
}

async function clearLoginAttempts(uid) {
await db.delete(`loginAttempts:${uid}`);
}

async function resetLockout(uid) {
await db.deleteAll([
`loginAttempts:${uid}`,
`lockout:${uid}`,
]);
}

async function getSessions(uid, curSessionId) {
await cleanExpiredSessions(uid);
const sids = await db.getSortedSetRevRange(`uid:${uid}:sessions`, 0, 19);
let sessions = await Promise.all(sids.map(sid => db.sessionStoreGet(sid)));
sessions = sessions.map((sessObj, idx) => {
if (sessObj && sessObj.meta) {
sessObj.meta.current = curSessionId === sids[idx];
sessObj.meta.datetimeISO = new Date(sessObj.meta.datetime).toISOString();
sessObj.meta.ip = validator.escape(String(sessObj.meta.ip));
}
const _token = await db.getObjectField(`user:${uid}`, 'rss_token');
const token = _token || utils.generateUUID();
if (!_token) {
await User.setUserField(uid, 'rss_token', token);
return sessObj && sessObj.meta;
}).filter(Boolean);
return sessions;
}

async function cleanExpiredSessions(uid) {
const sids = await db.getSortedSetRange(`uid:${uid}:sessions`, 0, -1);
if (!sids.length) {
return [];
}

const expiredSids = [];
const activeSids = [];
await Promise.all(sids.map(async (sid) => {
const sessionObj = await db.sessionStoreGet(sid);
const expired = !sessionObj || !sessionObj.hasOwnProperty('passport') ||
!sessionObj.passport.hasOwnProperty('user') ||
parseInt(sessionObj.passport.user, 10) !== parseInt(uid, 10);
if (expired) {
expiredSids.push(sid);
} else {
activeSids.push(sid);
}
return token;
};
}));

User.auth.clearLoginAttempts = async function (uid) {
await db.delete(`loginAttempts:${uid}`);
};
await db.sortedSetRemove(`uid:${uid}:sessions`, expiredSids);
return activeSids;
}

User.auth.resetLockout = async function (uid) {
await db.deleteAll([
`loginAttempts:${uid}`,
`lockout:${uid}`,
]);
};
async function addSession(User, uid, sessionId) {
if (!(parseInt(uid, 10) > 0)) {
return;
}

User.auth.getSessions = async function (uid, curSessionId) {
await cleanExpiredSessions(uid);
const sids = await db.getSortedSetRevRange(`uid:${uid}:sessions`, 0, 19);
let sessions = await Promise.all(sids.map(sid => db.sessionStoreGet(sid)));
sessions = sessions.map((sessObj, idx) => {
if (sessObj && sessObj.meta) {
sessObj.meta.current = curSessionId === sids[idx];
sessObj.meta.datetimeISO = new Date(sessObj.meta.datetime).toISOString();
sessObj.meta.ip = validator.escape(String(sessObj.meta.ip));
}
return sessObj && sessObj.meta;
}).filter(Boolean);
return sessions;
};
const activeSids = await cleanExpiredSessions(uid);
await db.sortedSetAdd(`uid:${uid}:sessions`, Date.now(), sessionId);
activeSids.push(sessionId);
await revokeSessionsAboveThreshold(User, activeSids, uid);
}

async function cleanExpiredSessions(uid) {
const sids = await db.getSortedSetRange(`uid:${uid}:sessions`, 0, -1);
if (!sids.length) {
return [];
async function revokeSessionsAboveThreshold(User, activeSids, uid) {
if (meta.config.maxUserSessions > 0 && activeSids.length > meta.config.maxUserSessions) {
const sessionsToRevoke = activeSids.slice(0, activeSids.length - meta.config.maxUserSessions);
await User.auth.revokeSession(sessionsToRevoke, uid);
}
}

async function revokeSession(sessionIds, uid) {
sessionIds = Array.isArray(sessionIds) ? sessionIds : [sessionIds];
const destroySids = sids => Promise.all(sids.map(db.sessionStoreDestroy));

await Promise.all([
db.sortedSetRemove(`uid:${uid}:sessions`, sessionIds),
destroySids(sessionIds),
]);
}

async function revokeAllSessions(User, uids, except) {
uids = Array.isArray(uids) ? uids : [uids];
const sids = await db.getSortedSetsMembers(uids.map(uid => `uid:${uid}:sessions`));
const promises = [];
uids.forEach((uid, index) => {
const ids = sids[index].filter(id => id !== except);
if (ids.length) {
promises.push(User.auth.revokeSession(ids, uid));
}
});
await Promise.all(promises);
}

const expiredSids = [];
const activeSids = [];
await Promise.all(sids.map(async (sid) => {
const sessionObj = await db.sessionStoreGet(sid);
const expired = !sessionObj || !sessionObj.hasOwnProperty('passport') ||
!sessionObj.passport.hasOwnProperty('user') ||
parseInt(sessionObj.passport.user, 10) !== parseInt(uid, 10);
if (expired) {
expiredSids.push(sid);
} else {
activeSids.push(sid);
}
}));

await db.sortedSetRemove(`uid:${uid}:sessions`, expiredSids);
return activeSids;
}
async function deleteAllSessions() {
await batch.processSortedSet('users:joindate', async (uids) => {
const sessionKeys = uids.map(uid => `uid:${uid}:sessions`);
const sids = _.flatten(await db.getSortedSetRange(sessionKeys, 0, -1));

User.auth.addSession = async function (uid, sessionId) {
if (!(parseInt(uid, 10) > 0)) {
return;
}
await Promise.all([
db.deleteAll(sessionKeys),
...sids.map(sid => db.sessionStoreDestroy(sid)),
]);
}, { batch: 1000 });
}

module.exports = function (User) {
User.auth = {};

User.auth.logAttempt = logAttempt;

const activeSids = await cleanExpiredSessions(uid);
await db.sortedSetAdd(`uid:${uid}:sessions`, Date.now(), sessionId);
await revokeSessionsAboveThreshold(activeSids.push(sessionId), uid);
User.auth.getFeedToken = async function (uid) {
return await getFeedToken(User, uid);
};

async function revokeSessionsAboveThreshold(activeSids, uid) {
if (meta.config.maxUserSessions > 0 && activeSids.length > meta.config.maxUserSessions) {
const sessionsToRevoke = activeSids.slice(0, activeSids.length - meta.config.maxUserSessions);
await User.auth.revokeSession(sessionsToRevoke, uid);
}
}
User.auth.clearLoginAttempts = clearLoginAttempts;

User.auth.revokeSession = async function (sessionIds, uid) {
sessionIds = Array.isArray(sessionIds) ? sessionIds : [sessionIds];
const destroySids = sids => Promise.all(sids.map(db.sessionStoreDestroy));
User.auth.resetLockout = resetLockout;

await Promise.all([
db.sortedSetRemove(`uid:${uid}:sessions`, sessionIds),
destroySids(sessionIds),
]);
};
User.auth.getSessions = getSessions;

User.auth.revokeAllSessions = async function (uids, except) {
uids = Array.isArray(uids) ? uids : [uids];
const sids = await db.getSortedSetsMembers(uids.map(uid => `uid:${uid}:sessions`));
const promises = [];
uids.forEach((uid, index) => {
const ids = sids[index].filter(id => id !== except);
if (ids.length) {
promises.push(User.auth.revokeSession(ids, uid));
}
});
await Promise.all(promises);
User.auth.addSession = async function (uid, sessionId) {
await addSession(User, uid, sessionId);
};

User.auth.deleteAllSessions = async function () {
await batch.processSortedSet('users:joindate', async (uids) => {
const sessionKeys = uids.map(uid => `uid:${uid}:sessions`);
const sids = _.flatten(await db.getSortedSetRange(sessionKeys, 0, -1));
User.auth.revokeSession = revokeSession;

await Promise.all([
db.deleteAll(sessionKeys),
...sids.map(sid => db.sessionStoreDestroy(sid)),
]);
}, { batch: 1000 });
User.auth.revokeAllSessions = async function (uids, except) {
await revokeAllSessions(User, uids, except);
};

User.auth.deleteAllSessions = deleteAllSessions;
};