From c14f7da1b29f0726c9380a7e544da7e3f274a157 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Wed, 21 Jun 2017 10:58:29 +0300 Subject: [PATCH] Generate unique tokens for each login and session --- client/index.html | 2 +- client/js/lounge.js | 6 +- client/js/socket-events/auth.js | 14 +- client/js/socket-events/change_password.js | 5 - client/js/socket-events/index.js | 1 + client/js/socket-events/init.js | 4 +- client/js/socket-events/sign_out.js | 9 ++ package.json | 1 + src/client.js | 62 +++++---- src/clientManager.js | 13 +- src/command-line/reset.js | 2 +- src/server.js | 147 ++++++++++++++------- 12 files changed, 167 insertions(+), 99 deletions(-) create mode 100644 client/js/socket-events/sign_out.js diff --git a/client/index.html b/client/index.html index 4a32be51..d8d39618 100644 --- a/client/index.html +++ b/client/index.html @@ -99,7 +99,7 @@
diff --git a/client/js/lounge.js b/client/js/lounge.js index 8b4845e2..90666f0d 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -562,8 +562,12 @@ $(function() { }); sidebar.on("click", "#sign-out", function() { + socket.emit("sign-out", storage.get("token")); storage.remove("token"); - location.reload(); + + if (!socket.connected) { + location.reload(); + } }); sidebar.on("click", ".close", function() { diff --git a/client/js/socket-events/auth.js b/client/js/socket-events/auth.js index 41e8cfd5..ad3539da 100644 --- a/client/js/socket-events/auth.js +++ b/client/js/socket-events/auth.js @@ -7,6 +7,7 @@ const storage = require("../localStorage"); socket.on("auth", function(data) { const login = $("#sign-in"); let token; + const user = storage.get("user"); login.find(".btn").prop("disabled", false); @@ -17,22 +18,23 @@ socket.on("auth", function(data) { error.show().closest("form").one("submit", function() { error.hide(); }); - } else { + } else if (user) { token = storage.get("token"); if (token) { $("#loading-page-message").text("Authorizing…"); - socket.emit("auth", {token: token}); + socket.emit("auth", {user: user, token: token}); } } - const input = login.find("input[name='user']"); - if (input.val() === "") { - input.val(storage.get("user") || ""); + if (user) { + login.find("input[name='user']").val(user); } + if (token) { return; } - $("#sidebar, #footer").find(".sign-in") + + $("#footer").find(".sign-in") .trigger("click", { pushState: false, }) diff --git a/client/js/socket-events/change_password.js b/client/js/socket-events/change_password.js index 3d248ea1..732aecd4 100644 --- a/client/js/socket-events/change_password.js +++ b/client/js/socket-events/change_password.js @@ -2,7 +2,6 @@ const $ = require("jquery"); const socket = require("../socket"); -const storage = require("../localStorage"); socket.on("change-password", function(data) { const passwordForm = $("#change-password"); @@ -22,10 +21,6 @@ socket.on("change-password", function(data) { }); } - if (data.token && storage.get("token") !== null) { - storage.set("token", data.token); - } - passwordForm .find("input") .val("") diff --git a/client/js/socket-events/index.js b/client/js/socket-events/index.js index b6545192..23a5e961 100644 --- a/client/js/socket-events/index.js +++ b/client/js/socket-events/index.js @@ -16,3 +16,4 @@ require("./quit"); require("./sync_sort"); require("./topic"); require("./users"); +require("./sign_out"); diff --git a/client/js/socket-events/init.js b/client/js/socket-events/init.js index 31c1eb2c..cf8b464b 100644 --- a/client/js/socket-events/init.js +++ b/client/js/socket-events/init.js @@ -17,10 +17,8 @@ socket.on("init", function(data) { render.renderNetworks(data); } - if (data.token && $("#sign-in-remember").is(":checked")) { + if (data.token) { storage.set("token", data.token); - } else { - storage.remove("token"); } $("body").removeClass("signed-out"); diff --git a/client/js/socket-events/sign_out.js b/client/js/socket-events/sign_out.js new file mode 100644 index 00000000..df16dbf5 --- /dev/null +++ b/client/js/socket-events/sign_out.js @@ -0,0 +1,9 @@ +"use strict"; + +const socket = require("../socket"); +const storage = require("../localStorage"); + +socket.on("sign-out", function() { + storage.remove("token"); + location.reload(); +}); diff --git a/package.json b/package.json index 5cac4679..559e1d12 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "semver": "5.4.1", "socket.io": "1.7.4", "spdy": "3.4.7", + "ua-parser-js": "0.7.12", "urijs": "1.18.10" }, "devDependencies": { diff --git a/src/client.js b/src/client.js index 56b92dec..de8fe7ff 100644 --- a/src/client.js +++ b/src/client.js @@ -10,6 +10,7 @@ var Msg = require("./models/msg"); var Network = require("./models/network"); var ircFramework = require("irc-framework"); var Helper = require("./helper"); +const UAParser = require("ua-parser-js"); module.exports = Client; @@ -81,12 +82,6 @@ function Client(manager, name, config) { var client = this; - if (client.name && !client.config.token) { - client.updateToken(function(token) { - client.manager.updateUser(client.name, {token: token}); - }); - } - var delay = 0; (client.config.networks || []).forEach((n) => { setTimeout(function() { @@ -95,6 +90,10 @@ function Client(manager, name, config) { delay += 1000; }); + if (typeof client.config.sessions !== "object") { + client.config.sessions = {}; + } + if (client.name) { log.info(`User ${colors.bold(client.name)} loaded`); } @@ -286,34 +285,51 @@ Client.prototype.connect = function(args) { client.save(); }; -Client.prototype.updateToken = function(callback) { - var client = this; - - crypto.randomBytes(48, function(err, buf) { +Client.prototype.generateToken = function(callback) { + crypto.randomBytes(48, (err, buf) => { if (err) { throw err; } - callback(client.config.token = buf.toString("hex")); + callback(buf.toString("hex")); }); }; +Client.prototype.updateSession = function(token, ip, request) { + const client = this; + const agent = UAParser(request.headers["user-agent"] || ""); + let friendlyAgent = ""; + + if (agent.browser.name.length) { + friendlyAgent = `${agent.browser.name} ${agent.browser.major}`; + } else { + friendlyAgent = "Unknown browser"; + } + + if (agent.os.name.length) { + friendlyAgent = ` on ${agent.os.name} ${agent.os.version}`; + } + + client.config.sessions[token] = { + lastUse: Date.now(), + ip: ip, + agent: friendlyAgent, + }; +}; + Client.prototype.setPassword = function(hash, callback) { var client = this; - client.updateToken(function(token) { - client.manager.updateUser(client.name, { - token: token, - password: hash - }, function(err) { - if (err) { - log.error("Failed to update password of", client.name, err); - return callback(false); - } + client.manager.updateUser(client.name, { + password: hash + }, function(err) { + if (err) { + log.error("Failed to update password of", client.name, err); + return callback(false); + } - client.config.password = hash; - return callback(true); - }); + client.config.password = hash; + return callback(true); }); }; diff --git a/src/clientManager.js b/src/clientManager.js index 95fa7054..c50ccf92 100644 --- a/src/clientManager.js +++ b/src/clientManager.js @@ -25,14 +25,8 @@ ClientManager.prototype.init = function(identHandler, sockets) { } }; -ClientManager.prototype.findClient = function(name, token) { - for (var i in this.clients) { - var client = this.clients[i]; - if (client.name === name || (token && token === client.config.token)) { - return client; - } - } - return false; +ClientManager.prototype.findClient = function(name) { + return this.clients.find((u) => u.name === name); }; ClientManager.prototype.autoloadUsers = function() { @@ -105,7 +99,8 @@ ClientManager.prototype.addUser = function(name, password, enableLog) { password: password || "", log: enableLog, awayMessage: "", - networks: [] + networks: [], + sessions: {}, }; fs.writeFileSync( Helper.getUserConfigPath(name), diff --git a/src/command-line/reset.js b/src/command-line/reset.js index 8be036e7..018b73d5 100644 --- a/src/command-line/reset.js +++ b/src/command-line/reset.js @@ -25,7 +25,7 @@ program return; } user.password = Helper.password.hash(password); - user.token = null; // Will be regenerated when the user is loaded + user.sessions = {}; fs.writeFileSync( file, JSON.stringify(user, null, "\t") diff --git a/src/server.js b/src/server.js index 2d78192a..c029789e 100644 --- a/src/server.js +++ b/src/server.js @@ -17,7 +17,6 @@ const net = require("net"); const Identification = require("./identification"); var manager = null; -var authFunction = localAuth; module.exports = function() { log.info(`The Lounge ${colors.green(Helper.getVersion())} \ @@ -94,10 +93,6 @@ module.exports = function() { in ${config.public ? "public" : "private"} mode`); }); - if (!config.public && (config.ldap || {}).enable) { - authFunction = ldapAuth; - } - var sockets = io(server, { serveClient: false, transports: config.transports @@ -105,7 +100,7 @@ in ${config.public ? "public" : "private"} mode`); sockets.on("connect", function(socket) { if (config.public) { - auth.call(socket); + auth.call(socket, {}); } else { init(socket); } @@ -178,7 +173,7 @@ function index(req, res, next) { res.render("index", data); } -function init(socket, client) { +function init(socket, client, generateToken) { if (!client) { socket.emit("auth", {success: true}); socket.on("auth", auth); @@ -248,8 +243,7 @@ function init(socket, client) { const obj = {}; if (success) { - obj.success = "Successfully updated your password, all your other sessions were logged out"; - obj.token = client.config.token; + obj.success = "Successfully updated your password"; } else { obj.error = "Failed to update your password"; } @@ -300,12 +294,47 @@ function init(socket, client) { } }); - socket.join(client.id); - socket.emit("init", { - active: client.lastActiveChannel, - networks: client.networks, - token: client.config.token || null + socket.on("sign-out", (token) => { + delete client.config.sessions[token]; + + client.manager.updateUser(client.name, { + sessions: client.config.sessions + }, (err) => { + if (err) { + log.error("Failed to update sessions for", client.name, err); + } + }); + + socket.emit("sign-out"); }); + + socket.join(client.id); + + const sendInitEvent = (token) => { + socket.emit("init", { + active: client.lastActiveChannel, + networks: client.networks, + token: token + }); + }; + + if (generateToken) { + client.generateToken((token) => { + client.updateSession(token, getClientIp(socket.request), socket.request); + + client.manager.updateUser(client.name, { + sessions: client.config.sessions + }, (err) => { + if (err) { + log.error("Failed to update sessions for", client.name, err); + } + }); + + sendInitEvent(token); + }); + } else { + sendInitEvent(null); + } } } @@ -324,10 +353,13 @@ function reverseDnsLookup(socket, client) { } function localAuth(client, user, password, callback) { + // If no user is found, or if the client has not provided a password, + // fail the authentication straight away if (!client || !password) { return callback(false); } + // If this user has no password set, fail the authentication if (!client.config.password) { log.error(`User ${colors.bold(user)} with no local password set tried to sign in. (Probably a LDAP user)`); return callback(false); @@ -345,6 +377,7 @@ function localAuth(client, user, password, callback) { } }); } + callback(matching); }).catch((error) => { log.error(`Error while checking users password. Error: ${error}`); @@ -376,50 +409,64 @@ function ldapAuth(client, user, password, callback) { } function auth(data) { - var socket = this; - var client; + const socket = this; + let client; + + const initClient = () => { + // If webirc is enabled and we do not know this users IP address, + // perform reverse dns lookup + if (Helper.config.webirc !== null && !client.config.ip) { + reverseDnsLookup(socket, client); + } else { + init(socket, client, data.remember === "on"); + } + }; + if (Helper.config.public) { client = new Client(manager); manager.clients.push(client); + socket.on("disconnect", function() { manager.clients = _.without(manager.clients, client); client.quit(); }); - if (Helper.config.webirc) { - reverseDnsLookup(socket, client); - } else { - init(socket, client); + + initClient(); + + return; + } + + const authCallback = (success) => { + // Authorization failed + if (!success) { + socket.emit("auth", {success: false}); + return; } + + // If authorization succeeded but there is no loaded user, + // load it and find the user again (this happens with LDAP) + if (!client) { + manager.loadUser(data.user); + client = manager.findClient(data.user); + } + + initClient(); + }; + + client = manager.findClient(data.user); + + // We have found an existing user and client has provided a token + if (client && data.token && typeof client.config.sessions[data.token] !== "undefined") { + client.updateSession(data.token, getClientIp(socket.request), socket.request); + + authCallback(true); + return; + } + + // Perform password checking + if (!Helper.config.public && Helper.config.ldap.enable) { + ldapAuth(client, data.user, data.password, authCallback); } else { - client = manager.findClient(data.user, data.token); - var signedIn = data.token && client && data.token === client.config.token; - var token; - - if (client && (data.remember || data.token)) { - token = client.config.token; - } - - var authCallback = function(success) { - if (success) { - if (!client) { - // LDAP just created a user - manager.loadUser(data.user); - client = manager.findClient(data.user); - } - if (Helper.config.webirc !== null && !client.config.ip) { - reverseDnsLookup(socket, client); - } else { - init(socket, client, token); - } - } else { - socket.emit("auth", {success: success}); - } - }; - - if (signedIn) { - authCallback(true); - } else { - authFunction(client, data.user, data.password, authCallback); - } + localAuth(client, data.user, data.password, authCallback); } }