Permalink
Browse files

Adds cookie based login, also called "remember me"-functionality.

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...
1 parent f29aa5f commit 45fd5e27f5db47f213579eee756528b43b17fe46 @hermansc hermansc committed Feb 16, 2014
Showing with 136 additions and 9 deletions.
  1. +25 −0 assets/js/client.js
  2. +2 −0 assets/js/libs/jquery.cookie.min.js
  3. +9 −1 assets/js/views/overview.js
  4. +7 −1 config.js
  5. +7 −0 lib/models.js
  6. +84 −7 lib/socket.js
  7. +2 −0 views/templates.jade
View
@@ -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'
@@ -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');
@@ -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 {
@@ -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;
@@ -283,6 +306,7 @@ $(function() {
channel.stream.add(quitMessage);
}
}
+ irc.delete_session();
});
irc.socket.on('names', function(data) {
@@ -347,6 +371,7 @@ $(function() {
});
irc.socket.on('reset', function(data) {
+ irc.delete_session();
irc.chatWindows = new WindowList();
irc.connected = false;
irc.loggedIn = false;

Some generated files are not rendered by default. Learn more.

Oops, something went wrong.
@@ -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: {
View
@@ -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"
};
View
@@ -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;
View
@@ -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;
@@ -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) {
@@ -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;
+ });
});
});
});
@@ -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'});
@@ -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) {
View
@@ -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

0 comments on commit 45fd5e2

Please sign in to comment.