27
Mar 11

Nodechat.js continued – authentication, profiles, ponies, and a meaner socket.io

As promised, here is a follow up tutorial on the node.js/socket.io/backbone.js/express/connect/jade/redis stack that powers nodechat.js.

The first nodechat.js tutorial introduced some fancy ideas, like using backbone.js on the server and streaming models over socket.io which is all well and great, but it left some of the more practical questions unanswered. Issues like coordinating authentication between express/connect and socket.io and creating, storing, and retrieving user profiles, are less sexy, but still quite important for real world use. So that is what the next this tutorial will look at. Practical stuff. Besides, everything is more fun when you do it in node, right?

I will also sprinkle you with some other neat wisdom nuggets along the way. If you want.

Overview

First off, nodechat.js has advanced well passed the point that it is clean and clear enough to be used as a tutorial. I have decided to create a new nodechat project that will be used strictly for tutorials. It lives at github also. The code for this tutorial is at the v0.2 tag. nodechat.js and nodechat-tutorial share same underlying design, so if you want to see a demo, come visit nodechat.no.de. There will probably even be someone there to chat with.

Our goal for this tutorial is to put nodechat behind a simple login page, provide a way to sign up new accounts, and make sure that socket.io verifies the accounts before accepting traffic.

Install Stuff

Any good node.js adventure starts off with npm powerups. For this adventure, we will need everything from part one of this tutorial and:

  • sudo npm install connect-redis
  • sudo npm install joose
  • sudo npm install joosex-namespace-depended
  • sudo npm install hash

And just for fun, let’s do: sudo npm update

We will be using connect-redis as our connect session store and hash as our hashing library. The joose modules are there because hash requires them.

Authentication

To authenticate users and retrieve bits of information about them, we will need to:

  1. Create user accounts
  2. Check login information
  3. Retrieve the profile if successful
  4. Kick them out if we are not successful
  5. Do something with the profile information

Let’s start with the easy stuff. We will need a login page and an account creation page. Create the following jade template files in the views/ directory:

login.jade

!!! 5
html(lang="en")
  head
    title nodechat login
  body
    #heading
      h1 nodechat login
    #content
      p
        | Please login or go 
        a(href='/signup') here
        |  to create a new account.
      form(method="post", action="/login")
        p
          label Username:
            input(type='text', name='username')
        p
          label Password:
            input(type='password', name='password')
        P
          input(type='submit', value='send')

Not much to this page. Just a simple login form with a link to our signup page.

signup.jade

!!! 5
html(lang="en")
  head
    title nodechat new user
  body
    #heading
      h1 nodechat new user
    #content
      p
      form(method="post", action="/signup")
        div(style='width:350px; text-align:right')
          label Username:  
            input(type='text', name='username')
          br
          label Password:  
            input(type='text', name='password1')
          br
          label Password (again):  
            input(type='text', name='password2')
          br
          label Email:  
            input(type='text', name='email')
          br
          label Favorite thing about ponies:  
            input(type='text', name='ponies')
          br
          input(type='submit', value='signup')

Again, pretty simple. Just collect the signup information and whatever special information we may *ahem* require from our users.

Routes

Now we need to set up routes to serve our new pages. In core.js, after we have set up express, we want to include several routes.

app.get('/login', function(req, res){
    res.render('login');
});

app.post('/login', function(req, res){
    auth.authenticateUser(req.body.username, req.body.password, function(err, user){
        if (user) {
            req.session.regenerate(function(){
                req.session.cookie.maxAge = 100 * 24 * 60 * 60 * 1000; //Force longer cookie age
                req.session.cookie.httpOnly = false;
                req.session.user = user;

                res.redirect('/');
            });
        } else {
            req.session.error = 'Authentication failed, please check your username and password.';
            res.redirect('back');
        }
    });
});

Setup the routes for GET and POST /login.

  • GET returns the login.jade template
  • POST calls the authentication module to verify login details.
    • Failures are redirected back to the login page.

*We will cover the authentication module in a minute. For now, just know that it does what it sounds like it might do :)

If the authentication module gives us a user object back, we ask connect to regenerate the session and send the client back to index. Note: we specify a long cookie age so users won’t have to log in frequently. We also set the httpOnly flag to false (I know, not so secure) to make the cookie available over Flash Sockets.

app.get('/signup', function(req, res){
    res.render('signup');
});

app.post('/signup', function(req, res) {
    auth.createNewUserAccount(req.body.username, req.body.password1, req.body.password2, req.body.email, req.body.ponies, function(err, user){
        if ((err) || (!user)) {
            req.session.error = 'New user failed, please check your username and password.';
            res.redirect('back');
        }
        else if(user) {
            res.redirect('/login');
        }
    });

});

Setup routes for GET and POST ‘/signup’:

  • GET returns signup.jade
  • POST calls createNewUserAccount() in the auth module.
    • Failures redirect to the same page
    • Success redirects back to /login for the user to formally log in

Next ‘/logout’:

app.get('/logout', function(req, res){
    req.session.destroy(function(){
        res.redirect('home');
    });
});

This route tells connect to destroy the session, which will cause nodechat to require the user to login again if they access the ‘/’ route.

app.get('/*.(js|css)', function(req, res){
    res.sendfile('./'+req.url);
});

app.get('/', restrictAccess, function(req, res){
    res.render('index', {
        locals: { name: req.session.user.name, hashPass: JSON.stringify(req.session.user.hashPass) }
    });
});

function restrictAccess(req, res, next) {
    if (req.session.user) {
        next();
    } else {
        req.session.error = 'Access denied!';
        res.redirect('/login');
    }
};

These last two routes already exist in nodechat v0.1. The only difference here is the reference to restrictAcess in the ‘/’ route. restrictAccess() is an express route middleware. Control is passed to the middleware function before the route function is called. We use restrictAccess() to verify that we have a valid user key in the session (implying that authentication has succeeded) before we send the client the requested route.

If we do not have a valid user object in the session, then we redirect the client to the ‘/login’ route. This effectively locks down our ‘/’ route from unauthenticated access. You could add the restrictAccess() to any route you want to protect.

model

That covers our new routes. Next we will need a backbone.js model for user accounts (well we don’t need a model, but nodechat is backbone.js on the server right?). Add a simple empty model to our models/models.js:

models.User = Backbone.Model.extend({});

That was easy.

auth.js

Let’s turn our attention to the authentication module touched on earlier. Create a new file, lib/auth.js. This module will handle the creation, verification, and profile loading of user accounts. It exposes two public methods: authenticateUser() and createNewUserAccount().

This will be a CommonJS module so we need to start off with some set up:

(function () {
    if (typeof exports !== 'undefined') {
        redis = require('redis');
        rc = redis.createClient();
        models = require('../models/models');

        //joose is required to support the hash lib we are using
        require('joose');
        require('joosex-namespace-depended');
        require('hash');
    } 
    else {
        throw new Error('auth.js must be loaded as a module.');
    }

Here we are checking to see if this code is included as a module. If it is, we go ahead and include our dependencies (in this case, our models lib, redis, and hash + friends). If we are not a module, we may as well explode because the rest of the code won’t run without redis and hash.

exports.authenticateUser = function(name, pass, fn) {
    console.log('[authenticate] Starting auth for ' + name + ' with password ' + pass);

    var rKey = 'user:' + name;
    rc.get(rKey, function(err, data){
        if(err) return fn(new Error('[authenticateUser] SET failed for key: ' + rKey + ' for value: ' + name));

        if (!data) {
            fn(new Error('[authenticateUser] invalid password'));
        }
        else {
            console.log('[authenticateUser] user: ' + name + ' found in store. Verifying password.');
            verifyUserAccount(data, pass, fn);
        }
    });
};

authenticateUser() takes a name, password, and callback. It checks to see if the user exists in redis. If it does, it calls verifyUserAccount().

CommonJS Modules

An aside: if you are unfamiliar with the CommonJS module specification, the basic idea is that you encapsulate your reusable methods and objects in a anonymous function that assigns its “public” methods to the global exports variable. This allows us to have private variables and methods, like verifyUserAccount() below, and to easily reuse our code through require() statements throughout our application. This is a pattern you will see constantly in node.js-world.

var verifyUserAccount = function(foundUserName, pass, fn) {
    var rKey = 'user:' + foundUserName;

    rc.get(rKey + '.salt', function(err, data){
        if(err) return fn(new Error('[verifyUserAccount] GET failed for key: ' + rKey + '.salt')); 

        if(data) {
            var calculatedHash = Hash.sha512(data + '_' + pass);
            rc.get(rKey + '.hashPass', function(err, data) {
                if(err) return fn(new Error('[verifyUserAccount] GET failed for key: ' + rKey + '.hashPass'));

                if (calculatedHash === data) {
                    console.log('[verifyUserAccount] Auth succeeded for ' + foundUserName + ' with password ' + pass);

                    rc.get(rKey + '.profile', function(err, data) {
                        if(err) return fn(new Error('[verifyUserAccount] GET failed for key: ' + rKey + '.profile' + ' for user profile'));

                        var foundUser = new models.User();
                        foundUser.mport(data);
                        foundUser.set({'hashPass': calculatedHash});

                        return fn(null, foundUser);
                    });
                }
                else {
                    return fn(new Error('[verifyUserAccount] invalid password'));
                }
            });
        }
        else {
            return fn(new Error('[verifyUserAccount] salt not found'));
        }
    });
}

verifyUserAccount() takes the same parameters as authenticateUser(). It assumes the passed in user exists in redis (remember, it is only accessible within the module, so that is a safe assumption to make) and then steps through the process of retrieving the salt, calculating the hash of the passed in password, then comparing it to the stored hash in redis. If the comparison is successful, create a new user model and pass it to the callback. Otherwise, any failure along the way means we callback with an error.

exports.createNewUserAccount = function(name, pass1, pass2, email, ponies, fn) {
    if (pass1 !== pass2) return fn(new Error('[createNewUserAccount] Passwords do not match'));

    var newUser = new models.User({name: name, email: email, ponies: ponies});

    var rKey = 'user:' + name;

    rc.set(rKey, name, function(err, data){
        if(err) return fn(new Error('[createNewUserAccount] SET failed for key: ' + rKey + ' for value: ' + name));

        var salt = new Date().getTime();
        rc.set(rKey + '.salt', salt, function(err, data) {
            if(err) return fn(new Error('[createNewUserAccount] SET failed for key: ' + rKey + '.salt' + ' for value: ' + salt));

            var hashPass = Hash.sha512(salt + '_' + pass1);
            rc.set(rKey + '.hashPass', hashPass, function(err, data) {
                if(err) return fn(new Error('[createNewUserAccount] SET failed for key: ' + rKey + '.hashPass' + ' for value: ' + hashPass));

                rc.set(rKey + '.profile', newUser.xport(), function(err, data) {
                    if(err) return fn(new Error('[createNewUserAccount] SET failed for key: ' + rKey + '.profile' + ' for user profile'));

                    newUser.set({'hashPass': hashPass});
                    return fn(null, newUser);
                });

            }); 
        }); 
    }); 
}

createNewUserAccount() verifies that the two passwords match, then uses the current timestamp to salt a hash of the password. It stores the hashed password and the passed in information about email and ponies in a user model which we will save as a poor man’s profile–if everything succeeds. Any failure along the way means we callback with an error. We are using SHA512 which has to be good, because it was invented by the NSA.

})()

If you have actually been cutting and pasting, you will need to end your file with this little snippet per the CommonJS spec.

Salting and Hashing

Another aside: if you are like me, you probably do not often mess around with actually storing usernames and passwords. Most frameworks will handle this for you quite nicely. I had to do some google-fu to figure out a decent way of doing this. Crypto experts: Please don’t email me telling me how the NSA could crack this in 5 minutes. It’s a chat program. I am also aware that there are probably five thousand node modules on npm that will do this for you as well. This is supposed to be fun dammit! Leave me alone.

The basic concept behind cryptographic hashing is to take a string of any length, in our case a password, and map it into a fixed length string, using an algorithm. This computation is inexpensive if you know the password up front, but doing the reverse–finding all the possible passwords that can map to the hash value–is computationally very expensive. Since we only store the hash, if our redis instance is ever compromised, an attacker would have to compute an enormous number potential passwords (and try each one) to figure out which one is the real password.

Hashing on its own can be susceptible to rainbow tables–precomputed reverse hash values. The attacker simply looks up the hash in the table and finds the list of corresponding possible passwords. The tables can be precomputed in advance and are easily obtained online by those who may be so inclined. This is where the salt comes in. The salt is a randomly generated string (I am just using a timestamp, but close enough) that is attached to the password before the cryptographic hash computation. We store the hash in the database as well; we will need it to verify passwords later on.

The salt mitigates the ability of an attacker to pre-compute the hash space. Since each user has a different salt, the attacker would have to compute the entire hash space for each user, and that is assuming the know the salt! If they don’t have the salt, the task becomes insurmountable.

nodechat.js uses the following salt/hash algorithm:

Account Creation

  1. On account set up, get the current time in milliseconds. This is our salt.
  2. Prepend the salt to the password separated by an underscore: salt_password.
  3. Calculate the SHA512 hash of the salt_password string. This is our password hash.
  4. Store the salt and the password hash for the user.

Account Verification

  1. On an attempt to login to an existing account, retrieve the salt for the requested user.
  2. Prepend the salt to the attempted password, separated by an underscore: salt_attemptedpassword.
  3. Calculate the SHA512 hash of the salt_attemptedpassword string. This is our attempted password hash.
  4. Retrieve the stored password hash and compare it to the attempted password hash. If both hash values match, then assume the passwords match. Otherwise, they do not.

Enough crypto for today. Back to node stuff.

Making it work with socket.io

We now have all the pieces in place to lock down our express routes and if we were not using socket.io, we would be done. But we are using socket.io, so we’re not. We need to be able to protect socket.io connections the same way we protect our routes. socket.io is not aware of the session and will not have access to the session store the same way express does, so we have to improvise.

Back in core.js we want to add some more code:

function disconnectAndRedirectClient(client, fn) {
    console.log('Disconnecting unauthenticated user');
    client.send({ event: 'disconnect' });
    client.connection.end();
    fn();
    return;
}

First we are going to give socket.io some teeth (get it, meaner socket.io? it bites? yes I am funny). When we have a client that shouldn’t be connected, kick ‘em off!. disconnectAndRedirectClient() takes a client socket.io object and a callback.

socket.on('connection', function(client){
    client.connectSession = function(fn) {
        if (!client.request || !client.request.headers || !client.request.headers.cookie) {
            disconnectAndRedirectClient(client,function() {
               console.log('Null request/header/cookie!');
            });
            return;
        }

        var match = client.request.headers.cookie.match(/connect\.sid=([^;]+)/);
        if (!match || match.length < 2) {
            disconnectAndRedirectClient(client,function() {
                console.log('Failed to find connect.sid in cookie');
            });
            return;
        }

        var sid = unescape(match[1]);

        rc.get(sid, function(err, data) {
            fn(err, JSON.parse(data));
        });
    };

    client.connectSession(function(err, data) {
        if(err) {
            console.log('Error on connectionSession: ' + err);
            return;
        }

        client.user = data.user;

        activeClients += 1;
        client.on('disconnect', function(){clientDisconnect(client)});
        client.on('message', function(msg){chatMessage(client, socket, msg)});

        socket.broadcast({
            event: 'update',
            clients: activeClients
        });

        var ponyWelcome = new models.ChatEntry({name: 'PonyBot', text: 'Hello ' + data.user.name + '. I also feel that ponies ' + data.user.ponies + '. Welcome to nodechat.js'});

        socket.broadcast({
            event: 'chat',
            data: ponyWelcome.xport()
        });
    });
});

There is a lot going on here, but much of this code is unchanged from v0.1. First, we are defining a helper method, connectSession, that will verify a client’s validity by checking for a cookie in the request header, then, if we find it, pulling their session out of redis! We then use the helper method in the ‘connection’ handler for our socket listener. Now instead of just accepting any old user connection, we are going to check that the client has a valid session (meaning they logged in). If they don’t, give them the boot! If they do, then we store a copy of the session data (yay we have access!) in the client object and then set up the rest of the socket events. Finally, send them a welcome message just to prove that we remembered their profile.

connect-redis

One last aside: connect-redis by visionmedia (I love his stuff!) is a connect session store the is backed by redis. It sets a cookie to track the session id, but it stores all other data in redis. This is really nice because a) we don’t have to worry about leaving cookies with critical data on the client, and b) we can grab the session data straight out of redis and give it to socket.io. We still have to parse the cookie to get the session id, but at least that is all we have to worry about.

function chatMessage(client, socket, msg){
    var chat = new models.ChatEntry();
    chat.mport(msg);

    rc.incr('next.chatentry.id', function(err, newId) {
        chat.set({id: newId});
        nodeChatModel.chats.add(chat);

        var expandedMsg = chat.get('id') + ' ' + client.user.name + ': ' + chat.get('text');

        rc.rpush('chatentries', chat.xport(), redis.print);

        socket.broadcast({
            event: 'chat',
            data:chat.xport()
        }); 
    }); 
}

The only material difference in chatMessage() is that we no longer simply believe the client when they tell us their name. Instead we use our newly found session power to tell them what their name is. And prevent fraud and stuff. Oh, and I also removed the call to rc.bgSave(). Because it was crashing during concurrent saves. If you want to persist your chats permanently, use the snapshotting method described by redis here.

Wrapping it up

That is about it. There may be some minor changes in some of the other files, so it never hurts to get the code from the v0.2 tag at github.

I want to close by saying that this method of authentication works and it has worked well enough to run in “production” on nodechat.no.de for several weeks. We do experience some funky issues once clients start falling back to xhr-polling and jsonp-polling, so be warned. This is the bleeding edge folks. Treat it as such.

My plan is to follow up with a different take on the authentication issue, one in which we ditch sessions altogether and use a shared key (like the hash) to sign each message over the socket. I’ll write it up if it works.

Finally, I want to say thanks to everyone who sent me email, tweets, or stopped by nodechat to… chat. It has been fantastic meeting you all and I look forward to seeing all the cool stuff you guys are cranking out.

Follow me on twitter @jdslatts, email me at comments@fzysqr.com, or stop by nodechat.no.de and say hi!

-jslatts