Node Tutorial Part 5

06 Dec 2010 | By Alex Young | Tags server node tutorials lmawa nodepad

Welcome to part 5 of Let’s Make a Web App, a tutorial series about building a web app with Node. This series will walk you through the major areas you’ll need to face when building your own applications. These tutorials are tagged with lmawa.

Previous tutorials:

Before starting this tutorial remember to start up a mongo daemon if your computer doesn’t run one automatically.

Authentication

We’ve built a serviceable app, but it’s not that useful without some kind of authentication system. Most production systems and client projects require authentication, and even though there are interesting initiatives like OpenID and OAuth, most commercial projects are likely to have their own login system.

This is usually achieved with a session:

  • A user fills out a form with their username and password
  • The password is encrypted with a hashing algorithm and salt
  • This value is compared with the user’s record in the database
  • If it matches, a session key is generated that identifies the user

We need the following things to manage users and sessions:

  • Users in the database
  • Sessions that can store the logged-in user ID
  • Password encryption
  • A way of restricting access to routes that require a logged-in user

Sessions in Express

Express relies on Connect’s session middleware, which is backed by a data storage mechanism. There’s a memory-based store, and third-party stores including connect-redis and connect-mongodb. An alternative option is cookie-sessions which stores session data inside the user’s cookie.

A session can be configured like this:

app.use(express.cookieDecoder());
app.use(express.session());

It’s very important to place these configuration options in the right place during configuration, else the session variable won’t appear in request objects. I’ve put it after bodyDecoder and before methodOverride. Refer to the full source on GitHub to see this in context.

Now our HTTP responders will have access to req.session:

app.get('/item', function(req, res) {
  req.session.message = 'Hello World';
});

MongoDB Sessions

Install connect-mongodb with npm install connect-mongodb.

connect-mongodb works like any other session store. During application configuration we need to specify our connection details:

app.configure('development', function() {
  app.set('db-uri', 'mongodb://localhost/nodepad-development');
});

var db = mongoose.connect(app.set('db-uri'));

function mongoStoreConnectionArgs() {
  return { dbname: db.db.databaseName,
           host: db.db.serverConfig.host,
           port: db.db.serverConfig.port,
           username: db.uri.username,
           password: db.uri.password };
}

app.use(express.session({
  store: mongoStore(mongoStoreConnectionArgs())
}));

Most of this code wouldn’t be required if the API authors could decide on a standard format for connection options. I’ve written a function that extracts the connection details from Mongoose. In this example, db holds a Mongoose connection instance. Mongoose expects connection details to be provided through URIs, which I like because it’s easy to remember the format. I’ve stored environment-specific connection strings using app.set.

When writing Express applications it’s a good idea to use app.set('name', 'value'). Just remember that app.set('name') is used to access the setting, rather than app.get.

Running db.sessions.find() in the mongo console will now return any sessions that have been created.

Access Control Middleware

Express provides an elegant way of restricting access to logged-in users. When HTTP handlers are defined, an optional route middleware parameter can be specified:

function loadUser(req, res, next) {
  if (req.session.user_id) {
    User.findById(req.session.user_id, function(user) {
      if (user) {
        req.currentUser = user;
        next();
      } else {
        res.redirect('/sessions/new');
      }
    });
  } else {
    res.redirect('/sessions/new');
  }
}

app.get('/documents.:format?', loadUser, function(req, res) {
  // ...
});

Now every route that requires a logged-in user can be handled by adding loadUser to the route. The middleware itself gets the usual route parameters, and also next — this can be used to run the route handler based on arbitrary logic. In our project a user is loaded using a user_id in the session; if the user cannot be found next simply isn’t called and the browser is redirected to the login screen.

RESTful Session Modelling

I’ve modelled sessions in a similar way to documents. There are new, create, and delete routes:

// Sessions
app.get('/sessions/new', function(req, res) {
  res.render('sessions/new.jade', {
    locals: { user: new User() }
  });
});

app.post('/sessions', function(req, res) {
  // Find the user and set the currentUser session variable
});

app.del('/sessions', loadUser, function(req, res) {
  // Remove the session
  if (req.session) {
    req.session.destroy(function() {});
  }
  res.redirect('/sessions/new');
});

User Model

The User model is more complicated than the Document model because it has to contain authentication-related code. The strategy I’ve used, which you’ve probably seen before in OO web frameworks, is:

  • Passwords are stored in an encrypted format with a salt
  • Authentication can be performed by comparing an encrypted plain text password with the password in the database for a given user
  • A ‘virtual’ password property exposes a plain text password for the convenience of registration and login forms
  • This property has a setter which automatically encrypts the password before saving
  • A unique index is used to ensure there’s only one user for each email address

The password encryption uses Node’s standard crypto library:

var crypto = require('crypto');

mongoose.model('User', {
  methods: {
    encryptPassword: function(password) {
      return crypto.createHmac('sha1', this.salt).update(password).digest('hex');
    }
  }
});

encryptPassword is an instance method which returns a sha1-hashed password with a salt. The salt is generated before encrypting the password in the password setter:

mongoose.model('User', {
  // ...

  setters: {
    password: function(password) {
      this._password = password;
      this.salt = this.makeSalt();
      this.hashed_password = this.encryptPassword(password);
    }
  },

  methods: {
    authenticate: function(plainText) {
      return this.encryptPassword(plainText) === this.hashed_password;
    },

    makeSalt: function() {
      return Math.round((new Date().valueOf() * Math.random())) + '';
    },

    // ...

The salt could be anything you like, I’ve generated a fairly random string here.

Saving Users and Registration

Mongoose makes it easy to do things when records are saved by overriding the save method:

mongoose.model('User', {
  // ...
  methods: {
    // ...

    save: function(okFn, failedFn) {
      if (this.isValid()) {
        this.__super__(okFn);
      } else {
        failedFn();
      }
    }

    // ...

I’ve overridden save to allow for a failed save method. This makes the failed registration handling simpler:

app.post('/users.:format?', function(req, res) {
  var user = new User(req.body.user);

  function userSaved() {
    switch (req.params.format) {
      case 'json':
        res.send(user.__doc);
      break;

      default:
        req.session.user_id = user.id;
        res.redirect('/documents');
    }
  }

  function userSaveFailed() {
    res.render('users/new.jade', {
      locals: { user: user }
    });
  }

  user.save(userSaved, userSaveFailed);
});

No error messages are displayed right now; I’ll get to that in another tutorial.

Even though this validation is pretty dumb, the index is critical to this application:

mongoose.model('User', {
  // ...

  indexes: [
    [{ email: 1 }, { unique: true }]
  ],

  // ...
});

This will prevent duplicate users from being saved. The format is the same as MongoDB’s ensureIndex.

Conclusion

As of commit 03fe9b2 we now have:

  • MongoDB sessions
  • User model, with support for sha1 password encryption
  • Routing middleware for controlling access to documents
  • User registration and login
  • Session management

I’ve updated the Jade templates to include a basic login form.

There are a few things missing from this version though:

  • Documents don’t take into account the owner
  • The tests don’t work properly because I’m having trouble figuring out session handling in Expresso tests

I’ll get to these things in future parts of this tutorial series.


blog comments powered by Disqus