justjs: node.js tutorials

New here? You might want to start at the beginning.

5/28 '12

Are these the droids we're looking for? Finding out with gmail authentication

Since the last installment, our blog is looking a whole lot nicer. But right now, anyone can post. They don't need jedi mind tricks. We have no security whatsoever.

Let's fix that by taking advantage of the passport module and gmail authentication. We can easily lock the "New Post" feature down either to one person or to anyone with an email address in an entire domain, as long as that domain is hosted with Google Apps for Business (aka "gmail for domains"). If you're already logged into gmail, all you have to do is check a box to grant permission. Logging in separately is not required.

"Hang on there. Are you telling me I can only use gmail authentication? I don't even use gmail." No, not at all. Passport actually supports many, many authentication methods, including Facebook, Twitter, and yes, an old fashioned local database of accounts just for your site. Using gmail as an authentication provider is just one example.

So why did I pick gmail for this demo? Because so many individuals and companies do use gmail accounts or host entire domains with gmail. And because in the latter case we can easily grant access to everyone at our domain. Trust me, when your title is "web developer" but your responsibilities also include system administration, you want to do this. Managing accounts yourself when your users already have them via Google is just an unnecessary hassle and a security risk.

So if you want to use Facebook, Twitter or another method, don't worry. The technique I demonstrate here is still 100% relevant. Passport keeps these things as consistent as possible between authentication methods. You can find the details for configuring other authentication strategies on the passport site.

Adding passport to the app.js module

As always, you can cheat by copying the code from github.

Copy your blog-4 project to a new blog-5 folder and we'll get started by loading the passport module in app.js. We'll also load a nifty module called minimatch.

First make sure you install the npm modules you need. As always, cd to your node-apps/blog-5 folder and use npm install:

npm install passport
npm install passport-google
npm install minimatch

Now we're ready to bring the modules we need into app.js. We can do this at the top of the module where the rest of the "require" calls are:

var passport = require('passport');
var minimatch = require('minimatch');

"What's this minimatch thing?" The minimatch module lets us compare email addresses against simple patterns like:

*@punkave.com

To match all users in that domain - more convenient and less error-prone than using regular expressions.

To use Passport we'll need to turn on a few other features in Express. Passport needs to remember that the user is logged in, which requires a user "session" that retains information about this particular user from one page view to the next. In order for user sessions to work, an HTTP cookie has to be sent to the browser containing a session identifier.

To enable these things we'll activate the standard express.cookieParser() middleware, which makes cookies available and updateable as properties of a req.cookies object, and also enable sessions in Express. Enabling sessions requires us to set a session secret. The session secret is used to cryptographically hash the session identifiers to verify that they really came from our app, so that they can't be easily guessed and faked. Just add these lines to the init() method of app.js, just before the express.static middleware is added:

context.app.use(express.cookieParser());
context.app.use(express.session({ secret: context.settings.sessionSecret }));

You'll need to add a value for the sessionSecret property to your settings.js file. Any secure password will do.

The rest of our code relating to passport could follow right here, but that would clutter up the init() method. So instead we'll call:

configurePassport()

Right after adding the express.static middleware.

Now let's check out the configurePassport() function, which we'll add at the end of the init() method. Keeping it nested inside init() allows us to see convenient local variables like "app".

The configurePassport function begins by setting up GoogleStrategy, a module included with passport that implements gmail authentication. We'll pass in appropriate settings and a callback function that creates a simple user object from the profile information that gmail gives back to us. This user object will become available as "req.user" once a user logs in:

      var GoogleStrategy = require('passport-google').Strategy;
      passport.use(new GoogleStrategy(
        context.settings.google,
        function(identifier, profile, done) {
          var user = { 'email': profile.emails[0].value, 'displayName': profile.displayName };
          done(null, user);
        }
      ));

Our callback function ends by calling done(null, user) to pass the new user object back to passport. null signifies that no error took place.

So far, so good. But what should context.settings.google be?

In your settings.js file you'll need to add a "google" property. This contains two properties of its own, "returnURL" and "realm". returnURL is the URL we want the user to be sent back to after they successfully authenticate with Google. "realm" represents the domain for our blog (not the user's email domain). If you're currently testing the site as http://localhost:3000/, then reasonable settings would be:

  google: {
    returnURL: 'http://localhost:3000/auth/google/callback',
    realm: 'http://localhost:3000/'
  },

Remembering the User in the Session

I mentioned that passport doesn't force us to use a particular authentication module. Actually, passport doesn't force us to do much of anything. In particular, it doesn't force us to store the current user object in the session in a particular way. So we need to provide our own code to store the user in the session and to pull them out again.

We do this by calling the passport.serializeUser and passport.deserializeUser functions and passing in callback functions (yes! However did you guess?) that turn the user object into a string and back to a user object. For some projects, you might choose to do this by storing user objects in a database, and returning just the id of the user object. But since an email address and a display name don't occupy much space, we'll just turn them into a JSON version of themselves (that is, we'll turn them into simple JavaScript data, not unlike our settings.js file). This guarantees that they can be stored in a database if we choose to keep sessions in a database later.

Here's the code to serialize and deserialize our users using the JSON.stringify and JSON.parse methods. These calls are short, and they'd be even shorter if I didn't bother to check for bad data in the session (hey, you never know):

      passport.serializeUser(function(user, done) {
        done(null, JSON.stringify(user));
      });

      passport.deserializeUser(function(json, done) {
        var user = JSON.parse(json);
        if (user)
        {
          done(null, user);
        }
        else
        {
          done(new Error("Bad JSON string in session"), null);
        }
      });

Hooking Up to Google

Now we're ready to initialize passport and turn on session support:

      app.use(passport.initialize());
      app.use(passport.session());

We're making good progress! The next step is to provide a route that forces the user to log in, and a route that actually accepts a successful login when authentication information is returned from Google. The first route, /auth/google, is convenient because we can redirect to it in order to force users to log in to continue. The second route, /auth/google/callback, sounds scary but passport does all the hard work for us.

In both cases, the passport.authenticate() function does the labor. When we pass it the name of a valid strategy (such as 'google'), it redirects the user to Google where they may choose whether to log into the blog. When we pass it an object containing successRedirect and failureRedirect URLs, it processes the authentication information sent back by Google and redirects appropriately.

Here's the code:

      app.get('/auth/google', passport.authenticate('google'));

      app.get('/auth/google/callback',
        passport.authenticate('google',
          {
            successRedirect: '/',
            failureRedirect: '/'
          }));

Notice that we configured /auth/google/callback as our callbackURL property in settings.js earlier. These do need to be consistent.

You Can Check In Any Time You Like, But You Can't Never Leave

One more detail: it's nice to be able to log out! Let's add a /logout route:

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

Passport adds a logOut method to the request object, so we can just call that method and then redirect the user back to the home page.

You're logged in! Oh, you wanted to SEE that?

If you fire up your blog right now and visit:

http://localhost:3000/auth/google

You'll experience the Google authentication process. If you approve the authentication you'll be returned to the site successfully... but it won't make a whit of difference, because we haven't started checking whether you have permission to post yet!

We'll move on to that in a moment. But first, we'll need a way for page templates to distinguish between logged-in users and logged-out users, and also to display the user's name.

The simplest way to do that is to inject the 'user' object into the 'slots' property of the data object that we pass to the view.page method. Then we can easily write code like:

<% if (slots.user) { %>
<%= slots.user.displayName %>
<% } %>

We could tackle this by passing this data in every call to view.page. But that would become very tedious very quickly. We could also alter the view module to do it. But that would require marrying it to the app module more closely by passing in the req object.

A better way, one that saves work rather than making our routes more complicated, is to refactor a little bit by creating a "page" function in the app module. Here's is that function:

    function page(req, res, template, data)
    {
      _.defaults(data, { slots: {} });
      _.defaults(data.slots, { user: req.user, session: req.session });
      res.send(view.page(template, data));
    }

This function accepts the req and res objects, a template name, and a data object to be passed to the page template. Then it adds both the user object (as slots.user) and the session object (as slots.session) to the data object. And then it calls view.page for us and passes the result to res.send() to finish the job.

Now we can rewrite each of our existing calls that look like this:

res.send(view.page(template, {posts: posts}))

To look like this:

page(req, res, 'index', {posts: posts});

The result is a bit shorter, and also better because each page template (and the partials invoked by it) now has access to slots.user and slots.session.

It's time to add a login button! Let's make an addition to the section of layout.js that currently displays the "brand" at the top of the page:

      <div class="container">
        <a class="brand" href="/">justjs</a>
        <%- partial('login', {}) %>
      </div>

The new part, of course, is the partial() call to display the login partial.

The login partial takes advantage of req.user to decide whether a login button or a logout button is appropriate and show the user's name. Here's the code for login.ejs:

<div class="btn-group pull-right">
    <% if (slots.user) { %>
        <a class="btn" href="/logout">Log Out</a>
    <% } else { %>
        <a class="btn" href="/auth/google">Log In</a>
    <% } %>
</div>
<% if (slots.user) { %>
  <div class="pull-right display-name">
      <%= slots.user.displayName %>
  </div>
<% } %>

To line up the user's name properly I had to add a tiny bit of CSS to justjs.css. If you know a better way to line text up with buttons in Boostrap, please tell me all about it:

.display-name
{
    padding: 10px;
}

Nice Lock. Pity There's No Door

Fire up the app again and you'll discover that you can log in and out by clicking a button. But it still doesn't matter. Let's fix that. Check out what I've added to the beginning of the /new route:

    app.get('/new', function(req, res) {
      if (!validPoster(req, res))
      {
        return;
      }
      newPost(req, res);
    });

Notice I've also added the "req" parameter to the "newPost" function. That was necessary to make sure it can call the new page() function properly.

The purpose of the validPoster function is to force the user to log in before they continue, and then to make sure they actually have permission to post (every gmail user in the world should not be allowed to post to your blog).

But where is the validPoster function? We'll add that new function at the end of init(), after configurePassport().

Our strategy here is simple:

1. If the user is not logged in (req.user is not defined), then redirect to /auth/google.

2. If the user is logged in, use the minimatch module to check whether they match our pattern for allowed email addresses of posters (found in context.settings.posters). If they do, great! If not, set a session property called req.session.error to a message explaining that although the user is logged in, they are not awesome enough to post to the blog.

In a few moments we'll see how to display this error message attractively using Bootstrap's excellent alert feature.

Here's the validPoster code:

    function validPoster(req, res)
    {
      if (!req.user)
      {
        res.redirect('/auth/google');
        return false;
      }
      if (!minimatch(req.user.email, context.settings.posters))
      {
        req.session.error = "Sorry, you do not have permission to post to this blog.";
        res.redirect('/');
        return false;
      }
      return true;
    }

For this code to work you'll need to add a "posters" property to settings.js. If you set it like this:

posters: 'myemail@gmail.com'

Then only the user myemail@gmail.com is allowed to post. If you set it like this:

posters: '*@punkave.com'

Then anyone in my office can post - a trick which only works if that company's email is handled by Google Apps for Business, of course. (Note: that service has a free tier as well as an educational version which should also work.)

"Why didn't that work?" "Who knows?"

Go ahead, fire up the blog and take a peek. You should be able to log in and out as before, and more importantly, you won't be able to post unless you log in properly. For now though, there is no error message when you try to post and don't have posting privileges - you just get sent back to the home page. Let's fix that.

We stashed the error message for logged-in gmail users who are not quite awesome enough to post to the blog in the session. We'd like to be able to display an error, if there is one, on the next page - whatever that page might be. So let's add a new partial() call right after the breadcrumb trail:

    <%- partial('crumbs', {}) %>
    <%- partial('error', {}) %>

The "error" partial can take advantage of the fact that the session is available in req.session. So we'll use Bootstrap's nifty alert component to display an attractive error message that users recognize. Here's the code for error.ejs:

<% if (slots.session.error) { %>
    <div class="row">
      <div class="span12">
          <div class="alert alert-error">
              <button class="close" data-dismiss="alert">×</button>
              <%- slots.session.error %>
              <% slots.session.error = undefined %>
          </div>
      </div>
    </div>
<% } %>

This code makes certain there is an error, then uses standard Bootstrap layout CSS and a Bootstrap alert box to present the error message nicely.

"MVC VIOLATION IN SECTOR TWELVE! CONTAIN AND DEACTIVATE MR. BOUTELL!"

We also do one sneaky thing here: we remove the error from the session. Since JavaScript objects are always passed by reference, we can modify them anywhere, including in a partial. So when we write:

        <% slots.session.error = undefined %>

We zap the error right out of the session. Refresh the page and you won't see it again.

Many would say that you should not modify the session in a template, because it violates the separation of the controller layer and the view layer. And honestly, those folks are right in the long run. It would be better if clearing the error message by clicking the "x" sent an AJAX request to the webserver, asking nicely to remove the error. That way the error would keep displaying as the user moved from page to page until they paid attention to it, read it, and cleared it. We'll look at how to implement this in a future installment. For the time being, though, this is a very handy hack.

Making that "x" do something

Speaking of which, clicking that "x" doesn't do anything yet. We can fix that easily, thanks to Bootstrap. All we have to do is load Bootstrap's standard JavaScript in our layout. Then Bootstrap will spot the data-dismiss="alert" attribute on the "x" button and arrange for the alert to disappear as soon as the "x" is clicked.

Just add bootstrap's JavaScript right at the end of the body element in layout.ejs, right after jQuery:

  <script type="text/javascript" src="/static/js/jquery-1.7.2.min.js"></script>
  <script type="text/javascript" src="/static/bootstrap/js/bootstrap.min.js"></script>

Restart the app and behold! Users who are logged in but not awesome enough to post get a reasonable error message that they can dismiss. Nothing left to complain about, right?

Why Don't My Sessions Persist?

"DUDE. I definitely have a complaint. I have to log in again every time I restart the app. This is getting old fast."

Yes, that is annoying. It happens because we're using passport's default session storage.

By default, passport just stores sessions in memory in our node process. This is convenient because we don't have to lift a finger to store sessions in the database, encrypt them into a cookie or attach them to messenger pigeons. And it's fast, of course, as storing things right in RAM always is.

But there are drawbacks. You have just discovered the most obvious drawback: restarting our node app discards our sessions. Another, more important drawback: when an app gets popular (and I mean seriously POPULAR, not just "this is a neat blog" popular) a single CPU just won't do. For a truly large-scale app, you will someday need to run multiple Node processes (one to take advantage of each CPU core in the server), or even multiple Node servers. And when that happens, you'll discover that these processes can't see each other's sessions at all. You'll be logged in on one CPU, logged out on another. Not a pretty picture.

Fortunately Express doesn't limit us to storing sessions in memory. We have lots of other options: storing them in a Redis database, storing them in our MongoDB database, and so on. We'll look into it in a future installment, I promise. If you're in a hurry, check out the session documentation on the Express site-

"No, seriously, this is REALLY annoying. Can't we just fix it now?"

Oh all right. Here's how to store our sessions in MongoDB so that your login persists when the app is restarted.

The express module is built on connect, and connect makes it really easy to write custom session storage handlers. Easy enough that there are at least three for MongoDB. I'll save you some time: connect-mongodb is the right choice, because it lets you reuse the database connection you already have rather than creating a separate one.

You can install it with npm as usual:

npm install connect-mongodb

To hook it up we'll need access to our MongoDB connection. That object is created in our db.js module and, so far, we don't make it available in the context. Let's fix that by adding this line right after we call "new mongo.Db":

context.mongoConnection = dbConnection;

Now, in app.js, locate this line:

    context.app.use(express.session({ secret: context.settings.sessionSecret }));

We'll expand on this a little to take advantage of the connect-mongodb module to store sessions in MongoDB:

    var connectMongoDb = require('connect-mongodb');
    var mongoStore = new connectMongoDb({ db: context.mongoConnection });
    context.app.use(express.session({ secret: context.settings.sessionSecret, store: mongoStore }));

What's going on here? First we use require to load the connect-mongodb module. This returns a constructor function we can use to make session storage handlers. So our next step is to create one, passing in the existing database connection that we saved in the context object. FInally we call express.session much as before, but with the addition of a "store" property pointing to the storage handler we just created.

Stop the app and restart it, then log in just one more time. Now restart the app again, click refresh and... yes! You are still logged in.

Oh I'm Sorry, Did You Want Paragraphs?

We've brought the blog a long way. It looks good, it stores posts in a proper database, and we have access control. But the actual blog posts are... well... unconsidered, as the designers in my office would put it. Right now all you can type is an amorphous blob of text that shows up as a single run-on paragraph. We can do better than that. In the next installment, we'll do a lot better by integrating a rich text editor into the app while also validating the markup so that no funny stuff sneaks through.

 

blog comments powered by Disqus