Merge pull request #1199 from thelounge/xpaw/safer-auth-flow

Implement multiple sessions for users
This commit is contained in:
Jérémie Astori 2017-08-04 00:33:00 -04:00 committed by GitHub
commit 5a6591112b
12 changed files with 167 additions and 99 deletions

View File

@ -99,7 +99,7 @@
</div> </div>
<div class="col-xs-12"> <div class="col-xs-12">
<label class="remember"> <label class="remember">
<input type="checkbox" name="remember" id="sign-in-remember" checked> <input type="checkbox" name="remember" value="on" id="sign-in-remember" checked>
Stay signed in Stay signed in
</label> </label>
</div> </div>

View File

@ -562,8 +562,12 @@ $(function() {
}); });
sidebar.on("click", "#sign-out", function() { sidebar.on("click", "#sign-out", function() {
socket.emit("sign-out", storage.get("token"));
storage.remove("token"); storage.remove("token");
location.reload();
if (!socket.connected) {
location.reload();
}
}); });
sidebar.on("click", ".close", function() { sidebar.on("click", ".close", function() {

View File

@ -7,6 +7,7 @@ const storage = require("../localStorage");
socket.on("auth", function(data) { socket.on("auth", function(data) {
const login = $("#sign-in"); const login = $("#sign-in");
let token; let token;
const user = storage.get("user");
login.find(".btn").prop("disabled", false); login.find(".btn").prop("disabled", false);
@ -17,22 +18,23 @@ socket.on("auth", function(data) {
error.show().closest("form").one("submit", function() { error.show().closest("form").one("submit", function() {
error.hide(); error.hide();
}); });
} else { } else if (user) {
token = storage.get("token"); token = storage.get("token");
if (token) { if (token) {
$("#loading-page-message").text("Authorizing…"); $("#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 (user) {
if (input.val() === "") { login.find("input[name='user']").val(user);
input.val(storage.get("user") || "");
} }
if (token) { if (token) {
return; return;
} }
$("#sidebar, #footer").find(".sign-in")
$("#footer").find(".sign-in")
.trigger("click", { .trigger("click", {
pushState: false, pushState: false,
}) })

View File

@ -2,7 +2,6 @@
const $ = require("jquery"); const $ = require("jquery");
const socket = require("../socket"); const socket = require("../socket");
const storage = require("../localStorage");
socket.on("change-password", function(data) { socket.on("change-password", function(data) {
const passwordForm = $("#change-password"); 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 passwordForm
.find("input") .find("input")
.val("") .val("")

View File

@ -16,3 +16,4 @@ require("./quit");
require("./sync_sort"); require("./sync_sort");
require("./topic"); require("./topic");
require("./users"); require("./users");
require("./sign_out");

View File

@ -17,10 +17,8 @@ socket.on("init", function(data) {
render.renderNetworks(data); render.renderNetworks(data);
} }
if (data.token && $("#sign-in-remember").is(":checked")) { if (data.token) {
storage.set("token", data.token); storage.set("token", data.token);
} else {
storage.remove("token");
} }
$("body").removeClass("signed-out"); $("body").removeClass("signed-out");

View File

@ -0,0 +1,9 @@
"use strict";
const socket = require("../socket");
const storage = require("../localStorage");
socket.on("sign-out", function() {
storage.remove("token");
location.reload();
});

View File

@ -57,6 +57,7 @@
"semver": "5.4.1", "semver": "5.4.1",
"socket.io": "1.7.4", "socket.io": "1.7.4",
"spdy": "3.4.7", "spdy": "3.4.7",
"ua-parser-js": "0.7.12",
"urijs": "1.18.10" "urijs": "1.18.10"
}, },
"devDependencies": { "devDependencies": {

View File

@ -10,6 +10,7 @@ var Msg = require("./models/msg");
var Network = require("./models/network"); var Network = require("./models/network");
var ircFramework = require("irc-framework"); var ircFramework = require("irc-framework");
var Helper = require("./helper"); var Helper = require("./helper");
const UAParser = require("ua-parser-js");
module.exports = Client; module.exports = Client;
@ -81,12 +82,6 @@ function Client(manager, name, config) {
var client = this; var client = this;
if (client.name && !client.config.token) {
client.updateToken(function(token) {
client.manager.updateUser(client.name, {token: token});
});
}
var delay = 0; var delay = 0;
(client.config.networks || []).forEach((n) => { (client.config.networks || []).forEach((n) => {
setTimeout(function() { setTimeout(function() {
@ -95,6 +90,10 @@ function Client(manager, name, config) {
delay += 1000; delay += 1000;
}); });
if (typeof client.config.sessions !== "object") {
client.config.sessions = {};
}
if (client.name) { if (client.name) {
log.info(`User ${colors.bold(client.name)} loaded`); log.info(`User ${colors.bold(client.name)} loaded`);
} }
@ -286,34 +285,51 @@ Client.prototype.connect = function(args) {
client.save(); client.save();
}; };
Client.prototype.updateToken = function(callback) { Client.prototype.generateToken = function(callback) {
var client = this; crypto.randomBytes(48, (err, buf) => {
crypto.randomBytes(48, function(err, buf) {
if (err) { if (err) {
throw 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) { Client.prototype.setPassword = function(hash, callback) {
var client = this; var client = this;
client.updateToken(function(token) { client.manager.updateUser(client.name, {
client.manager.updateUser(client.name, { password: hash
token: token, }, function(err) {
password: hash if (err) {
}, function(err) { log.error("Failed to update password of", client.name, err);
if (err) { return callback(false);
log.error("Failed to update password of", client.name, err); }
return callback(false);
}
client.config.password = hash; client.config.password = hash;
return callback(true); return callback(true);
});
}); });
}; };

View File

@ -25,14 +25,8 @@ ClientManager.prototype.init = function(identHandler, sockets) {
} }
}; };
ClientManager.prototype.findClient = function(name, token) { ClientManager.prototype.findClient = function(name) {
for (var i in this.clients) { return this.clients.find((u) => u.name === name);
var client = this.clients[i];
if (client.name === name || (token && token === client.config.token)) {
return client;
}
}
return false;
}; };
ClientManager.prototype.autoloadUsers = function() { ClientManager.prototype.autoloadUsers = function() {
@ -105,7 +99,8 @@ ClientManager.prototype.addUser = function(name, password, enableLog) {
password: password || "", password: password || "",
log: enableLog, log: enableLog,
awayMessage: "", awayMessage: "",
networks: [] networks: [],
sessions: {},
}; };
fs.writeFileSync( fs.writeFileSync(
Helper.getUserConfigPath(name), Helper.getUserConfigPath(name),

View File

@ -25,7 +25,7 @@ program
return; return;
} }
user.password = Helper.password.hash(password); user.password = Helper.password.hash(password);
user.token = null; // Will be regenerated when the user is loaded user.sessions = {};
fs.writeFileSync( fs.writeFileSync(
file, file,
JSON.stringify(user, null, "\t") JSON.stringify(user, null, "\t")

View File

@ -17,7 +17,6 @@ const net = require("net");
const Identification = require("./identification"); const Identification = require("./identification");
var manager = null; var manager = null;
var authFunction = localAuth;
module.exports = function() { module.exports = function() {
log.info(`The Lounge ${colors.green(Helper.getVersion())} \ log.info(`The Lounge ${colors.green(Helper.getVersion())} \
@ -94,10 +93,6 @@ module.exports = function() {
in ${config.public ? "public" : "private"} mode`); in ${config.public ? "public" : "private"} mode`);
}); });
if (!config.public && (config.ldap || {}).enable) {
authFunction = ldapAuth;
}
var sockets = io(server, { var sockets = io(server, {
serveClient: false, serveClient: false,
transports: config.transports transports: config.transports
@ -105,7 +100,7 @@ in ${config.public ? "public" : "private"} mode`);
sockets.on("connect", function(socket) { sockets.on("connect", function(socket) {
if (config.public) { if (config.public) {
auth.call(socket); auth.call(socket, {});
} else { } else {
init(socket); init(socket);
} }
@ -178,7 +173,7 @@ function index(req, res, next) {
res.render("index", data); res.render("index", data);
} }
function init(socket, client) { function init(socket, client, generateToken) {
if (!client) { if (!client) {
socket.emit("auth", {success: true}); socket.emit("auth", {success: true});
socket.on("auth", auth); socket.on("auth", auth);
@ -248,8 +243,7 @@ function init(socket, client) {
const obj = {}; const obj = {};
if (success) { if (success) {
obj.success = "Successfully updated your password, all your other sessions were logged out"; obj.success = "Successfully updated your password";
obj.token = client.config.token;
} else { } else {
obj.error = "Failed to update your password"; obj.error = "Failed to update your password";
} }
@ -300,12 +294,47 @@ function init(socket, client) {
} }
}); });
socket.join(client.id); socket.on("sign-out", (token) => {
socket.emit("init", { delete client.config.sessions[token];
active: client.lastActiveChannel,
networks: client.networks, client.manager.updateUser(client.name, {
token: client.config.token || null 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) { 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) { if (!client || !password) {
return callback(false); return callback(false);
} }
// If this user has no password set, fail the authentication
if (!client.config.password) { if (!client.config.password) {
log.error(`User ${colors.bold(user)} with no local password set tried to sign in. (Probably a LDAP user)`); log.error(`User ${colors.bold(user)} with no local password set tried to sign in. (Probably a LDAP user)`);
return callback(false); return callback(false);
@ -345,6 +377,7 @@ function localAuth(client, user, password, callback) {
} }
}); });
} }
callback(matching); callback(matching);
}).catch((error) => { }).catch((error) => {
log.error(`Error while checking users password. Error: ${error}`); log.error(`Error while checking users password. Error: ${error}`);
@ -376,50 +409,64 @@ function ldapAuth(client, user, password, callback) {
} }
function auth(data) { function auth(data) {
var socket = this; const socket = this;
var client; 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) { if (Helper.config.public) {
client = new Client(manager); client = new Client(manager);
manager.clients.push(client); manager.clients.push(client);
socket.on("disconnect", function() { socket.on("disconnect", function() {
manager.clients = _.without(manager.clients, client); manager.clients = _.without(manager.clients, client);
client.quit(); client.quit();
}); });
if (Helper.config.webirc) {
reverseDnsLookup(socket, client); initClient();
} else {
init(socket, client); 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 { } else {
client = manager.findClient(data.user, data.token); localAuth(client, data.user, data.password, authCallback);
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);
}
} }
} }