Sunday, June 10, 2012

Securing Node.js and Express with SSL Client-Authentication

In the course of my work using Node.js, I did some research on implementing securing Node.js.  There are a couple of decent articles on the subject, and of course a number of frameworks that performed this work, but nothing I read or evaluated really quite fit my needs:
  • Use strong authentication.
    In my case, I preferred certificate-based (SSL).
  • Remain as unobtrusive as possible.  
    I didn't want security code in my routers or generally entangled in the rest of my application.
  • Be Flexible.
    Let me determine how and when users could see content or perform actions.
I know it's sacrilege to say it, but I really was looking for a capability similar to Spring Security or J2EE container security.  I wanted to constrain access by route, apply arbitrary access rules, and utilize ACL's which could be used to restrict content, or constrain access to data when queries are performed against external data sources.

In this post, I will demonstrate how to setup SSL authentication in Node.js.  

In following posts, I will show you how to write Connect "middleware" components to constrain access by route and enhance user identity with roles (enabling ACLs). 

Enabling SSL Authentication in Node.js

The first step to enabling SSL Authentication is to have the necessary Public-Key Infrastructure (PKI) to generate both server and client certificates, as well as, establishing trust between those certificates by deriving the certificates from a Certificate Authority.  I discuss this topic in detail in the previous post, and will even references the certificate paths used in that discussion.

Once you have the certificates in place, the code to actually enable SSL Authentication in Node.js is actually pretty easy:
var express = require('express')
  , routes = require('./routes')
  , fs = require('fs')

// MAGIC HAPPENS HERE!
var opts = {
  
  // Specify the key file for the server
  key: fs.readFileSync('ssl/server/keys/server1.key'),
  
  // Specify the certificate file
  cert: fs.readFileSync('ssl/server/certificates/server1.crt'),
  
  // Specify the Certificate Authority certificate
  ca: fs.readFileSync('ssl/ca/ca.crt'),
  
  // This is where the magic happens in Node.  All previous
  // steps simply setup SSL (except the CA).  By requesting
  // the client provide a certificate, we are essentially
  // authenticating the user.
  requestCert: true,
  
  // If specified as "true", no unauthenticated traffic
  // will make it to the route specified.
  rejectUnauthorized: true
};

var app = module.exports = express.createServer(opts);

// Configuration 

app.configure(function(){
  app.set('views', __dirname + '/views');
  app.set('view engine', 'jade');
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(app.router);
  app.use(express.static(__dirname + '/public'));
});

app.configure('development', function(){
  app.use(express.errorHandler(
    { dumpExceptions: true, showStack: true })); 
});

app.configure('production', function(){
  app.use(express.errorHandler()); 
});

// Routes
app.get('/', routes.index);

app.listen(8443);

console.log(
   "Express server listening on port %d in %s mode", 
    8443, 
    app.settings.env);
With the exception of the "options" object containing the SSL configuration, the application is just an Express.js application.

Let's start up the application...

When you run the application you will get the "Enter PEM pass phrase:" request before the web server will start.  The PEM is the password you used when creating the Server's key.  This is actually a pretty nice feature when you think about it, since you don't have to submit it when launching the application (making it visible when you use the ps command in POSIX systems).

If you are really not that concerned about security, you can specify the passphrase in the Server options:

var opts = {
  key: fs.readFileSync('ssl/server/keys/server1.key'),
  cert: fs.readFileSync('ssl/server/certificates/server1.crt'),
  ca: fs.readFileSync('ssl/ca/ca.crt'),
  requestCert: true,
  rejectUnauthorized: true,
  
  // This is the password used when generating the server's key
  // that was used to create the server's certificate.
  // And no, I do not use "password" as a password.
  passphrase: "password"
};
No when you start the server, it will not harass you for a password.

Now when a user comes to the website without a certificate, the will get the following "nasty gram":



As opposed to the original route they intended to hit (in this case the "index" route).

Pretty awesome, huh?

If you don't need to deal with users of varying levels of access (administrators, moderaters, etc.), and don't have any anonymous users, you're done.  However, if you want to show a nice "Unauthorized" web page, we will need to change our configuration a little bit.


Allowing Anonymous Access with Custom Unauthorized Pages


One of the settings in our configuration is preventing the unauthenticated browser (Firefox) from reaching our web server.  This may be okay in certain circumstances, but in general, it may be smarter to let the user know the web server is functioning correctly, it's just that they aren't privileged enough to view your content.

To allow unauthenticated browsers through, simply turn the "rejectUnauthorized" property in the options object to "false":
var opts = {
  key: fs.readFileSync('ssl/server/keys/server1.key'),
  cert: fs.readFileSync('ssl/server/certificates/server1.crt'),
  ca: fs.readFileSync('ssl/ca/ca.crt'),
  requestCert: true,

  // Now every one can get through to the web server.
  rejectUnauthorized: false,
};
Now all users can hit the website. Authentication still occurs, but unauthenticated users are given access to everything that an authenticated user gets.  This is obviously not what we want, but it's necessary to at least provide an "Unauthorized Access" webpage to that unauthenticated user.  Since we turned off Node's native way of restricting access, it's our responsibility to control that access in our application.

The next snippet of code demonstrates how to determine whether a user was authenticated and what you can potentially do with this knowledge.  In this example, I will render different content based on this condition.  If the user is unauthenticated, I will render the "Unauthorized" template.  If the user is authorized, I will render an "Authorized" template displaying the user's certificate information.
app.get('/', function(req, res){

  // AUTHORIZED 
  if(req.client.authorized){

    var subject = req.connection
      .getPeerCertificate().subject;
    
    // Render the authorized template, providing
    // the user information found on the certificate
    res.render('authorized', 
      { title:        'Authorized!',
     user:         subject.CN,
     email:        subject.emailAddress,
     organization: subject.O,
     unit:         subject.OU,
     location:     subject.L,
     state:        subject.ST,
     country:      subject.C
   }); 
 
  // NOT AUTHORIZED
  } else {
 
 // Render the unauthorized template.
    res.render('unauthorized', 
  { title: 'Unauthorized!' }); 
  }
});
OK, I've written a little security logic in a route (which I already mentioned was evil) to demonstrate how you can gain access to the authenticated user, as well as, what you can do when based on the authorization status.

I won't show you the Jade templates I used (they really simple; if you want a copy, leave me a comment), but I will show you the results:

Unauthorized Template Rendered
Authorized Template Rendered

That's it for Part 1.  In the next post, I will show you how to create Connect middleware to better separate this security logic from your routes.

Until next time, good luck.

13 comments:

  1. hi, trying to get this working, but I always get a "No data received
    Unable to load the web page because the server sent no data." when trying from chrome. The app is running (listening on port xxx)

    if I create the server with no opts, then the page is accessible. With opts, I get prompted for the server key, but then I cannot access the page

    ReplyDelete
    Replies
    1. Julian,

      Have you been able to get this to work yet? The only time I've ever encountered this problem with Node/Express is when a middleware function fails to call the "next()" function, passing control to the next handler. If you are using Coffeescript, watch out for the indentation (always gets me on the next function. Would you mind posting a link to a Github Gist so I can see the failing code?

      Thanks,

      Richard

      Delete
  2. I can get SSL running with just node, but when I used your example I get
    "curl: (35) Unknown SSL protocol error in connection to localhost:8443 " I get this pretty much no matter what I do.. Any ideas?

    ReplyDelete
    Replies
    1. Zach,

      I've never encountered this problem before. I'm running on a Macbook Pro, so maybe there is a difference in environment? Are you using "rejectUnauthorized: true"? When you do and attempt to access the webserver, you will not get a 501 error; Node will simply reject you abruptly.

      Would you mind posting a GitHub GIST (or some sort) of your app? I'll see if I have the same problem on my end.

      Thanks,

      Richard

      Delete
    2. I was stuck on this for almost the entire day.
      this happens if you do not set the FQDN during the creation of .crt

      write localhost as i did for dev purpose:
      Common Name (e.g. server FQDN or YOUR name) []:localhost

      Delete
    3. @Unknown's comment is correct. The FQDN on the certificate needs to match your hostname.

      Delete
  3. These are deprecated in express 3.1:
    var app = module.exports = express.createServer(opts);
    app.listen(8443);

    Instead use:
    var https = require('https');
    var app = express();
    https.createServer(opts, app).listen(port);

    ReplyDelete
  4. This comment has been removed by the author.

    ReplyDelete
  5. You can now utilize SPKAC handling natively using commit https://github.com/joyent/node/commit/7bf46ba. This will allow you implement the element for users, here is a demo of this new(er) functionality (where I cite your blog post as a reference); https://github.com/jas-/node-spkac

    ReplyDelete