diff --git a/packages/node_modules/@node-red/editor-api/lib/auth/tokens.js b/packages/node_modules/@node-red/editor-api/lib/auth/tokens.js index daa058787..31c12d228 100644 --- a/packages/node_modules/@node-red/editor-api/lib/auth/tokens.js +++ b/packages/node_modules/@node-red/editor-api/lib/auth/tokens.js @@ -25,27 +25,39 @@ function generateToken(length) { var storage; - var sessionExpiryTime - var sessions = {}; - var loadedSessions = null; - var apiAccessTokens; +var sessionExpiryListeners = []; +var expiryTimeout; function expireSessions() { + if (expiryTimeout) { + clearTimeout(expiryTimeout); + expiryTimeout = null; + } + var nextExpiry = Number.MAX_SAFE_INTEGER; var now = Date.now(); var modified = false; for (var t in sessions) { if (sessions.hasOwnProperty(t)) { var session = sessions[t]; if (!session.hasOwnProperty("expires") || session.expires < now) { + sessionExpiryListeners.forEach(listener => { listener(session) }) delete sessions[t]; modified = true; + } else { + if (session.expires < nextExpiry) { + nextExpiry = session.expires; + } } } } + if (nextExpiry < Number.MAX_SAFE_INTEGER) { + // Allow 5 seconds grace + expiryTimeout = setTimeout(expireSessions,(nextExpiry - Date.now()) + 5000) + } if (modified) { return storage.saveSessions(sessions); } else { @@ -65,6 +77,9 @@ function loadSessions() { module.exports = { init: function(adminAuthSettings, _storage) { storage = _storage; + + sessionExpiryListeners = []; + sessionExpiryTime = adminAuthSettings.sessionExpiryTime || 604800; // 1 week in seconds // At this point, storage will not have been initialised, so defer loading // the sessions until there's a request for them. @@ -112,6 +127,11 @@ module.exports = { expires: accessTokenExpiresAt }; sessions[accessToken] = session; + + if (!expiryTimeout) { + expiryTimeout = setTimeout(expireSessions,(accessTokenExpiresAt - Date.now()) + 5000) + } + return storage.saveSessions(sessions).then(function() { return { accessToken: accessToken, @@ -125,5 +145,8 @@ module.exports = { delete sessions[token]; return storage.saveSessions(sessions); }); + }, + onSessionExpiry: function(callback) { + sessionExpiryListeners.push(callback); } } diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/comms.js b/packages/node_modules/@node-red/editor-api/lib/editor/comms.js index adca1602d..8326ffa3b 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/comms.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/comms.js @@ -40,11 +40,19 @@ function init(_server,_settings,_runtimeAPI) { settings = _settings; runtimeAPI = _runtimeAPI; Tokens = require("../auth/tokens"); + Tokens.onSessionExpiry(handleSessionExpiry); Users = require("../auth/users"); Permissions = require("../auth/permissions"); } - +function handleSessionExpiry(session) { + activeConnections.forEach(connection => { + if (connection.token === session.accessToken) { + connection.ws.send(JSON.stringify({auth:"fail"})); + connection.ws.close(); + } + }) +} function generateSession(length) { var c = "ABCDEFGHIJKLMNOPQRSTUZWXYZabcdefghijklmnopqrstuvwxyz1234567890"; var token = []; @@ -88,7 +96,7 @@ function CommsConnection(ws) { // handleRemoteSubscription(ws,msg.subscribe); } } else { - var completeConnection = function(userScope,sendAck) { + var completeConnection = function(userScope,session,sendAck) { try { if (!userScope || !Permissions.hasPermission(userScope,"status.read")) { ws.send(JSON.stringify({auth:"fail"})); @@ -96,6 +104,7 @@ function CommsConnection(ws) { } else { pendingAuth = false; addActiveConnection(self); + self.token = msg.auth; if (sendAck) { ws.send(JSON.stringify({auth:"ok"})); } @@ -113,29 +122,29 @@ function CommsConnection(ws) { if (user) { self.user = user; log.audit({event: "comms.auth",user:self.user}); - completeConnection(client.scope,true); + completeConnection(client.scope,msg.auth,true); } else { log.audit({event: "comms.auth.fail"}); - completeConnection(null,false); + completeConnection(null,null,false); } }); } else { log.audit({event: "comms.auth.fail"}); - completeConnection(null,false); + completeConnection(null,null,false); } }); } else { if (anonymousUser) { log.audit({event: "comms.auth",user:anonymousUser}); self.user = anonymousUser; - completeConnection(anonymousUser.permissions,false); + completeConnection(anonymousUser.permissions,null,false); //TODO: duplicated code - pull non-auth message handling out if (msg.subscribe) { self.subscribe(msg.subscribe); } } else { log.audit({event: "comms.auth.fail"}); - completeConnection(null,false); + completeConnection(null,null,false); } } } diff --git a/packages/node_modules/@node-red/editor-client/src/js/comms.js b/packages/node_modules/@node-red/editor-client/src/js/comms.js index f4c082466..5f6274cbd 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/comms.js +++ b/packages/node_modules/@node-red/editor-client/src/js/comms.js @@ -75,29 +75,39 @@ RED.comms = (function() { } ws.onmessage = function(event) { var message = JSON.parse(event.data); - for (var m = 0; m < message.length; m++) { - var msg = message[m]; - if (pendingAuth && msg.auth) { - if (msg.auth === "ok") { + if (message.auth) { + if (pendingAuth) { + if (message.auth === "ok") { pendingAuth = false; completeConnection(); - } else if (msg.auth === "fail") { + } else if (message.auth === "fail") { // anything else is an error... active = false; RED.user.login({updateMenu:true},function() { connectWS(); }) } + } else if (message.auth === "fail") { + // Our current session has expired + active = false; + RED.user.login({updateMenu:true},function() { + connectWS(); + }) } - else if (msg.topic) { - for (var t in subscriptions) { - if (subscriptions.hasOwnProperty(t)) { - var re = new RegExp("^"+t.replace(/([\[\]\?\(\)\\\\$\^\*\.|])/g,"\\$1").replace(/\+/g,"[^/]+").replace(/\/#$/,"(\/.*)?")+"$"); - if (re.test(msg.topic)) { - var subscribers = subscriptions[t]; - if (subscribers) { - for (var i=0;i