Skip to content

Commit

Permalink
Adds cookie based login, also called "remember me"-functionality.
Browse files Browse the repository at this point in the history
This commit fixes issue #284 by adding cookies/authentication tokens at the
client side and validating these in the DB. When a user either registers or
logs in, without already having a cookie, a sha1-hash is generated using the
username and a secret key. This hash is stored in a DB-table calles 'Session'
and client side using the jquery-cookie plugin. When closing and opening the
application again we check if the users have a 'auth_token' in their cookies,
if this is the case we check its validity in the DB. If everything is OK, we
'jump through the hoops' and sets the user as logged in, restores his/hers
connections and render the chat_application. If it is not valid we delete the
cookie at the client and render the overview page.

As I've never actually implemented a cookie-based login system using javascript
before, I do not know if this solution is optimal and I'm more than happy to
discuss alternative approaches or restructure the code.
  • Loading branch information
hermansc committed Feb 16, 2014
1 parent f29aa5f commit 45fd5e2
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 9 deletions.
25 changes: 25 additions & 0 deletions assets/js/client.js
Expand Up @@ -4,6 +4,7 @@
//= require 'libs/ICanHaz.min.js'
//= require 'libs/bootstrap.min.js'
//= require 'libs/ircparser.min.js'
//= require 'libs/jquery.cookie.min.js'
//= require 'utils.js'
//= require 'models.js'
//= require 'collections.js'
Expand Down Expand Up @@ -51,6 +52,14 @@ $(function() {
}
});

irc.delete_session = function() {
// Deletes the session cookie at both server and client side
if ($.cookie('auth_token')) {
irc.socket.emit('session_delete', { auth_token: $.cookie('auth_token') });
$.removeCookie('auth_token');
}
}

// Registration (server joined)
irc.socket.on('registered', function(data) {
var window = irc.chatWindows.getByName('status');
Expand All @@ -69,6 +78,9 @@ $(function() {

irc.socket.on('login_success', function(data) {
window.irc.loggedIn = true;

$.cookie('auth_token', data.auth_token, { expires: 7 });

if(data.exists){
irc.socket.emit('connect', {});
} else {
Expand All @@ -77,17 +89,28 @@ $(function() {
});

irc.socket.on('disconnect', function() {
// The server probably went down.
irc.connected = false;
irc.delete_session();
alert('You were disconnected from the server.');
$('.container-fluid').css('opacity', '0.5');
});


irc.socket.on('register_success', function(data) {
window.irc.loggedIn = true;
$.cookie('auth_token', data.auth_token, { expires: 7 });
irc.appView.overview.render({currentTarget: {id: "connection"}});
});

irc.socket.on('session_not_found', function(data) {
// The client has a session cookie, but it is not found server-side.
// Delete it at the client, as something is not in sync, and render the overview page.
console.log("A session was found at client, but not in the server.");
irc.delete_session();
irc.appView.overview.render();
});

irc.socket.on('restore_connection', function(data) {
irc.me = new User({nick: data.nick, server: data.server});
irc.connected = true;
Expand Down Expand Up @@ -283,6 +306,7 @@ $(function() {
channel.stream.add(quitMessage);
}
}
irc.delete_session();
});

irc.socket.on('names', function(data) {
Expand Down Expand Up @@ -347,6 +371,7 @@ $(function() {
});

irc.socket.on('reset', function(data) {
irc.delete_session();
irc.chatWindows = new WindowList();
irc.connected = false;
irc.loggedIn = false;
Expand Down
2 changes: 2 additions & 0 deletions assets/js/libs/jquery.cookie.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion assets/js/views/overview.js
@@ -1,6 +1,14 @@
var OverviewView = Backbone.View.extend({
initialize: function() {
this.render();
// Check if we are logged in with a cookie.
if ($.cookie('auth_token')){
irc.socket.emit('session_check', {
auth_token: $.cookie('auth_token'),
});
} else {
// If not, we render the overview page.
this.render();
}
},

events: {
Expand Down
8 changes: 7 additions & 1 deletion config.js
Expand Up @@ -33,5 +33,11 @@ module.exports = {
use_polling: process.env.USE_POLLING || false, // Use polling if websockets aren't supported

// limit each user's connection log to this amount of messages (***not implemented yet***)
max_log_size: 4096
max_log_size: 4096,

// How long you want to store a cookie, both server and client side, in hours.
cookie_time: 7 * 24,

// Secret key used to generate a unique and secure session cookie hash.
secret_key: "MY-SUPER-SECRET-KEY"
};
7 changes: 7 additions & 0 deletions lib/models.js
Expand Up @@ -52,6 +52,13 @@ module.exports = function (schema) {
chanserv_password: { type: String }
});

schema.define('Session', {
auth_token: { type: String },
username: { type: String },
user_id: { type: String },
expires: { type: Date }
});

schema.autoupdate();

return schema;
Expand Down
91 changes: 84 additions & 7 deletions lib/socket.js
@@ -1,5 +1,6 @@
var bcrypt = require('bcrypt-nodejs'),
uuid = require('node-uuid');
uuid = require('node-uuid'),
crypto = require('crypto');

function isChannel(name) {
return ['#','&'].indexOf(name[0]) >= 0;
Expand All @@ -23,6 +24,7 @@ module.exports = function(socket, app) {
var Message = app.db.models.Message;
var PM = app.db.models.PM;
var Channel = app.db.models.Channel;
var Session = app.db.models.Session;

// signal an IRC connection belonging to the user
socket.signalIRC = function(connection, event, dict) {
Expand All @@ -49,11 +51,13 @@ module.exports = function(socket, app) {
, password: hash
, joined: Date.now()
}, function (err, user) {
socket.emit('register_success', {username: data.username});
socket.userID = user.user_id;
// subscribe to all online IRC connections using socket.io room
socket.join(socket.userID);
socket.socketIORoom = socket.userID;
socket.session_create(data, user.user_id, function(data, auth_token) {
socket.emit('register_success', {username: data.username, auth_token: auth_token});
socket.userID = user.user_id;
// subscribe to all online IRC connections using socket.io room
socket.join(socket.userID);
socket.socketIORoom = socket.userID;
});
});
});
});
Expand Down Expand Up @@ -87,7 +91,11 @@ module.exports = function(socket, app) {
socket.connID = connections[0].id;
socket.conn = connections[0];
}
socket.emit('login_success', {username: data.username, exists: exists});
socket.session_create(data, user.user_id, function(data, auth_token){
socket.emit('login_success', { username: data.username,
exists: exists,
auth_token: auth_token });
});
});
} else {
socket.emit('login_error', {message: 'Wrong password'});
Expand All @@ -98,6 +106,75 @@ module.exports = function(socket, app) {
}
});
});

socket.session_create = function(data, user_id, callback) {
/*
* Add the successful login to the session database.
*/
// Create a sha1-hash based on username and secret key
var hash = crypto.createHash('sha1');
var auth_token = hash.update(data.username + app.config.secret_key).digest('hex');

// Delete any previous sessions for this user_id.
Session.all({ where: { user_id: user_id } }, function(err, sessions){
sessions.forEach(function(session){
session.destroy();
});
})

var now = new Date();
Session.create({
auth_token: auth_token,
username: data.username,
user_id: user_id,
expires: new Date().setHours(now.getHours() + app.config.cookie_time),
});
callback(data, auth_token);
}

socket.on('session_check', function(data){
/*
* Check if the token provides is valid, that is: in the database and not
* expired. If found we restore the users connections, if any, and log in.
*/
Session.findOne({ where: { auth_token: data.auth_token}}, function(err, session){
var now = new Date();
if(session && session.expires > now) {
// TODO: Join the logic inside here, with the logic inside on('login')
socket.userID = session.user_id;
socket.join(socket.userID);
socket.socketIORoom = socket.userID;

Connection.all({where: { user_id: session.user_id } }, function (err, connections) {
var exists = false;
if (connections.length > 0) {
exists = true;
// TEMPORARY - read note at top of this file on socket.connID
socket.connID = connections[0].id;
socket.conn = connections[0];
}
socket.emit('login_success', { username: session.username, exists: exists })
});
} else {
if (session && session.expires < now) {
// Delete session if it is expired.
session.destroy();
}
// In this situation, which should be rare, the client has a cookie
// which is not in the database. Thus we just want to delete the cookie
// at the client.
socket.emit('session_not_found');
}
})
});

socket.on('session_delete', function(data){
Session.findOne({ where: {auth_token: data.auth_token}}, function(err, session){
if (session) {
session.destroy();
}
});
});

// connection creation/restore
socket.on('connect', function(data) {
Expand Down
2 changes: 2 additions & 0 deletions views/templates.jade
Expand Up @@ -22,12 +22,14 @@ script(id="overview_home", type="text/html")
li.overview_button#settings
img(src="/assets/images/settings.svg")
span Settings
{{^loggedIn}}
li.overview_button#login
img(src="/assets/images/login.svg")
span Login
li.overview_button#register
img(src="/assets/images/register.svg")
span Register
{{/loggedIn}}

script(id="overview", type="text/html")
#overview
Expand Down

0 comments on commit 45fd5e2

Please sign in to comment.