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 class="col-xs-12">
<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
</label>
</div>

View file

@ -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() {

View file

@ -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,
})

View file

@ -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("")

View file

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

View file

@ -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");

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",
"socket.io": "1.7.4",
"spdy": "3.4.7",
"ua-parser-js": "0.7.12",
"urijs": "1.18.10"
},
"devDependencies": {

View file

@ -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);
});
};

View file

@ -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),

View file

@ -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")

View file

@ -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);
}
}