diff --git a/packages/auth/src/middleware/authenticated.js b/packages/auth/src/middleware/authenticated.js index 9afbb33489..db1fdfacd9 100644 --- a/packages/auth/src/middleware/authenticated.js +++ b/packages/auth/src/middleware/authenticated.js @@ -68,7 +68,7 @@ module.exports = (noAuthPatterns = [], opts) => { clearCookie(ctx, Cookies.Auth) } else { // make sure we denote that the session is still in use - await updateSessionTTL(userId, sessionId) + await updateSessionTTL(session) } } const apiKey = ctx.request.headers["x-budibase-api-key"] diff --git a/packages/auth/src/middleware/passport/google.js b/packages/auth/src/middleware/passport/google.js index c3ef155f1b..ab3ae0bb1a 100644 --- a/packages/auth/src/middleware/passport/google.js +++ b/packages/auth/src/middleware/passport/google.js @@ -62,14 +62,12 @@ async function authenticate(token, tokenSecret, profile, done) { // authenticate const sessionId = newid() - const payload = { + await createASession(dbUser._id, sessionId) + + dbUser.token = jwt.sign({ userId: dbUser._id, sessionId, - } - await createASession(dbUser._id, sessionId, payload) - dbUser.sessionId = sessionId - - dbUser.token = jwt.sign(payload, env.JWT_SECRET) + }, env.JWT_SECRET) return done(null, dbUser) } diff --git a/packages/auth/src/middleware/passport/local.js b/packages/auth/src/middleware/passport/local.js index 5ede7f91a1..a3968f9da6 100644 --- a/packages/auth/src/middleware/passport/local.js +++ b/packages/auth/src/middleware/passport/local.js @@ -34,14 +34,12 @@ exports.authenticate = async function (email, password, done) { // authenticate if (await compare(password, dbUser.password)) { const sessionId = newid() - const payload = { + await createASession(dbUser._id, sessionId) + + dbUser.token = jwt.sign({ userId: dbUser._id, sessionId, - } - await createASession(dbUser._id, sessionId, payload) - dbUser.sessionId = sessionId - - dbUser.token = jwt.sign(payload, env.JWT_SECRET) + }, env.JWT_SECRET) // Remove users password in payload delete dbUser.password diff --git a/packages/auth/src/security/sessions.js b/packages/auth/src/security/sessions.js index 83af6c723c..353b53871a 100644 --- a/packages/auth/src/security/sessions.js +++ b/packages/auth/src/security/sessions.js @@ -4,16 +4,23 @@ const EXPIRY_SECONDS = 86400 async function getSessionsForUser(userId) { const client = await redis.getSessionClient() - return client.scan(userId) + const sessions = await client.scan(userId) + return sessions.map(session => session.value) } function makeSessionID(userId, sessionId) { return `${userId}/${sessionId}` } -exports.createASession = async (userId, sessionId, token) => { +exports.createASession = async (userId, sessionId) => { const client = await redis.getSessionClient() - await client.store(makeSessionID(userId, sessionId), token, EXPIRY_SECONDS) + const session = { + createdAt: (new Date()).toISOString(), + lastAccessedAt: (new Date()).toISOString(), + sessionId, + userId, + } + await client.store(makeSessionID(userId, sessionId), session, EXPIRY_SECONDS) } exports.invalidateSessions = async (userId, sessionId = null) => { @@ -31,9 +38,11 @@ exports.invalidateSessions = async (userId, sessionId = null) => { await Promise.all(promises) } -exports.updateSessionTTL = async (userId, sessionId) => { +exports.updateSessionTTL = async session => { const client = await redis.getSessionClient() - await client.setExpiry(makeSessionID(userId, sessionId), EXPIRY_SECONDS) + const key = makeSessionID(session.userId, session.sessionId) + session.lastAccessedAt = (new Date()).toISOString() + await client.store(key, session, EXPIRY_SECONDS) } exports.endSession = async (userId, sessionId) => { @@ -41,6 +50,8 @@ exports.endSession = async (userId, sessionId) => { await client.delete(makeSessionID(userId, sessionId)) } +exports.getUserSessions = getSessionsForUser + exports.getSession = async (userId, sessionId) => { try { const client = await redis.getSessionClient() @@ -53,5 +64,6 @@ exports.getSession = async (userId, sessionId) => { exports.getAllSessions = async () => { const client = await redis.getSessionClient() - return client.scan() + const sessions = await client.scan() + return sessions.map(session => session.value) } diff --git a/packages/worker/src/api/controllers/admin/sessions.js b/packages/worker/src/api/controllers/admin/sessions.js new file mode 100644 index 0000000000..ce6ccf47e2 --- /dev/null +++ b/packages/worker/src/api/controllers/admin/sessions.js @@ -0,0 +1,33 @@ +const { getAllSessions, getUserSessions, invalidateSessions } = require("@budibase/auth/sessions") + +exports.fetch = async ctx => { + ctx.body = await getAllSessions() +} + +exports.find = async ctx => { + const { userId } = ctx.params + const sessions = await getUserSessions(userId) + ctx.body = sessions.map(session => session.value) +} + +exports.invalidateUser = async ctx => { + const { userId } = ctx.params + await invalidateSessions(userId) + ctx.body = { + message: "User sessions invalidated" + } +} + +exports.selfSessions = async ctx => { + const userId = ctx.user._id + ctx.body = await getUserSessions(userId) +} + +exports.invalidateSession = async ctx => { + const userId = ctx.user._id + const { sessionId } = ctx.params + await invalidateSessions(userId, sessionId) + ctx.body = { + message: "Session invalidated successfully." + } +} diff --git a/packages/worker/src/api/controllers/admin/users.js b/packages/worker/src/api/controllers/admin/users.js index 5b299d9906..f524379266 100644 --- a/packages/worker/src/api/controllers/admin/users.js +++ b/packages/worker/src/api/controllers/admin/users.js @@ -122,13 +122,16 @@ exports.removeAppRole = async ctx => { const db = new CouchDB(GLOBAL_DB) const users = await allUsers() const bulk = [] + const cacheInvalidations = [] for (let user of users) { if (user.roles[appId]) { + cacheInvalidations.push(userCache.invalidateUser(user._id)) delete user.roles[appId] bulk.push(user) } } await db.bulkDocs(bulk) + await Promise.all(cacheInvalidations) ctx.body = { message: "App role removed from all users", } @@ -158,6 +161,7 @@ exports.updateSelf = async ctx => { ...user, ...ctx.request.body, }) + await userCache.invalidateUser(user._id) ctx.body = { _id: response.id, _rev: response.rev, diff --git a/packages/worker/src/api/routes/admin/sessions.js b/packages/worker/src/api/routes/admin/sessions.js new file mode 100644 index 0000000000..f7661e2b49 --- /dev/null +++ b/packages/worker/src/api/routes/admin/sessions.js @@ -0,0 +1,14 @@ +const Router = require("@koa/router") +const controller = require("../../controllers/admin/sessions") +const adminOnly = require("../../../middleware/adminOnly") + +const router = Router() + +router + .get("/api/admin/sessions", adminOnly, controller.fetch) + .get("/api/admin/sessions/self", controller.selfSessions) + .get("/api/admin/sessions/:userId", adminOnly, controller.find) + .delete("/api/admin/sessions/:userId", adminOnly, controller.invalidateUser) + .delete("/api/admin/sessions/self/:sessionId", controller.invalidateSession) + +module.exports = router \ No newline at end of file diff --git a/packages/worker/src/api/routes/index.js b/packages/worker/src/api/routes/index.js index 8b232f7b7c..21ec324880 100644 --- a/packages/worker/src/api/routes/index.js +++ b/packages/worker/src/api/routes/index.js @@ -5,6 +5,7 @@ const templateRoutes = require("./admin/templates") const emailRoutes = require("./admin/email") const authRoutes = require("./admin/auth") const roleRoutes = require("./admin/roles") +const sessionRoutes = require("./admin/sessions") const appRoutes = require("./app") exports.routes = [ @@ -15,5 +16,6 @@ exports.routes = [ appRoutes, templateRoutes, emailRoutes, + sessionRoutes, roleRoutes, ]