Monday, December 1, 2014

Authentication with Passport

Since today's comic is about unauthorized access to a computer system, it seems like as good a time as any to talk about how I do authentication in my Node web apps. I use the Passport authentication middleware, which integrates nicely with the Express framework. It can be a little tricky to get everything configured - I can't remember how many hours I spent on it the first time. Here I'll give you an example of the simplest possible way to set it up.

First, you have to make sure you have all the required dependencies. While Passport won't complain without things like body-parser, it won't actually function properly. To use Passport, you'll need express-session as well, and express-session requires cookie-parser. Additionally, you'll need to grab whatever particular authentication strategy you want to use with Passport. In my example, I use passport-local which is the simplest thing to demonstrate. There are lots of others though - once you see how this works you can experiment with them.

So here's what my package.json looks like for this demo app:

{
  "name": "passport-demo",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "express": "4.10.4",
    "cookie-parser": "1.3.3",
    "body-parser": "1.9.3",
    "express-session": "1.9.2",
    "passport": "0.2.1",
    "passport-local": "1.0.0"
  }
}

I also have 2 really simple HTML pages that go along with this. One is just the index.html that shows some generic welcome text. The other is the login form, form.html. That one is how you can submit your login credentials. Here is the main page:

<!doctype html>

<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Hello</title>
</head>

<body>

<p>Thanks for visiting.</p>

</body>

</html>

and here is the login form:

<!doctype html>

<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Login</title>
</head>

<body>

  <form action="/login" method="post">

    <input type="text" name="username" />
    <input type="password" name="password" />
    <input type="submit" />

  </form>

</body>

</html>

Now for the good stuff. It starts out like any other Node app with all the requires. Then the Express App is created.

var express = require("express");
var bodyParser = require("body-parser");
var cookieParser = require("cookie-parser");
var session = require("express-session");
var passport = require("passport");
var LocalStrategy = require("passport-local").Strategy;

var app = express();

We've come to the Passport-specific items. You have to tell Passport which authentication strategy you want to use. I am going to use the LocalStrategy, which means I will handle all user authentication myself. Typically you do this by looking up users in a database or something like that. When you create a new LocalStrategy, you have to supply it with the function it will call to perform your particular type of authentication. Here's my example:

var auth = function(username, password, done) {

    var err = false; // if an error occurs while you are checking
    var credsGood = true; // if user credentials are good

    if (err) {
        done(new Error("data access error"));
    } else {
        if (credsGood) {
            done(null, { userid: username });
        } else {
            done(null, false, { message: "Invalid credentials" });
        }
    }

};

// --- set up login strategy
passport.use(new LocalStrategy(auth));

As you can see, my simple auth function always returns a positive authentication. In reality, you'd use some mechanism to determine if the supplied username and password are good instead of just assuming they are. There are three possible ways to return from the function. First, if you run into some kind of error (your database is down) you can call done and pass it an Error. If the supplied credentials are good, call done passing in a null for the first argument (no error) and a user object for the second parameter. Finally, if the supplied credentials are not acceptable, call done with a null as the first parameter (no error), a false as the second parameter (authentication failed) and an object containing more information about the failure as the third parameter.

The next things you need to set up are the Passport serializer and deserializer. Basically, the serializer needs to return a unique key given a user object and the deserializer needs to return a user object given that unique key. The user objects need not be in some specific format - they are your objects, whatever you returned from the auth function supplied to the LocalStrategy. In my example, I just use the userid as the unique key returned from the serializer and then recreate the object with that one field in the deserializer. My user objects are so simple I don't need to do anything else, but your application might store more data in them. In that case you probably would want to put your users in some sort of cache when serializing and pull from there when deserializing. So here's my code:

/*
 * this serializer doesn't do much, just returns the userid
 */
passport.serializeUser(function(user, done) {
    done(null, user.userid);
});

/*
 * this deserializer actually performs no user lookup, just
 * recreates the user object. my user object is not really
 * much of anything at this point, just the userid.
 */
passport.deserializeUser(function(id, done) {
    done(null, { userid: id });
});

The rest of the app is fairly standard. I just set up all the middlewares, making sure to do them in the right order. The last ones you do should be passport.initialize() and passport.session(). Here is the code for the remainder of the app:

// used on resources that you have to be authenticated to use
function ensureAuthenticated(req, res, next) {
    if (req.isAuthenticated()) {
        return next();
    }
    res.sendStatus(403);
}

app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser()); // required before session.
app.use(session({ secret: "some secret", resave: false, saveUninitialized: false }));
app.use(passport.initialize());
app.use(passport.session());

app.post("/login", passport.authenticate("local", {
    successRedirect: "/",
    failureRedirect: "/form.html"
}));

app.get("/unprotected", function(req, res, next) {
    res.send("anyone can see this");
});

app.get("/protected", ensureAuthenticated, function(req, res, next) {
    res.send("you have access");
});

// ------------ static content (index.html and form.html)
app.use(express.static("public"));

// ------------ start listening
var server = app.listen(3000, function() {
    console.log("listening on port %d", server.address().port);
});

I set up 3 routes in this app, one for the login credential processing, one unprotected route for an example, and one protected route for an example. The login route is what the form in form.html POSTs to. It simply calls the passport.authenticate function specifying the strategy to use, "local" in this case, and an object defining the success and failure redirects. If authentication is successful, I redirect back to the main page, and if it is unsuccessful I redirect back to the login form page.

You'll note that I have to create a middleware to protect my protected route. My ensureAuthenticated function can simply call req.isAuthenticated(), a function provided by Passport, to see if the user has access. If the user is good, I can just call next(), but otherwise I return a 403 Forbidden. All you have to do to protect any route is add this function to the middleware chain, just like I did when I defined the /protected route.

Put this all together and give it a try. You shouldn't be able to access /protected until after you've been to /form.html and submitted some credentials. I hope this example saves you a few minutes when you are setting up authentication, and make sure you check out the comic link below!

Amphibian.com comic for 1 December 2014

No comments:

Post a Comment