DailyJS

Node Tutorial Part 5

2010-12-06 00:00:00 +0000 by Alex R. Young

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:

We need the following things to manage users and sessions:

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:

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:

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

There are a few things missing from this version though:

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