Authentication in Node.js with Passport.js

In this tutorial we will build a basic authentication system using Node.js, Passport.js and Redis. By the end of this tutorial you’ll have a simple system in which you can manage your user sessions and store any information you want in their sessions.

Really basic authentication

Whenever we start building something new, it is always a good idea to start simple. Once we have a simple Hello World program, then we take it further step by step. In Passport.js a simple authentication looks like this:

// server.ts
import * as passport from "passport";
import * as pl from "passport-local";
import * as bodyParser from "body-parser";

const LocalStrategy: any = pl.Strategy;

app.use(passport.initialize());
app.use(bodyParser.urlencoded({ extended: true }));

var expectedUser = {
  username: "jacob",
  password: "password",
};

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

passport.deserializeUser(function (id, done) {
  done(null, id);
});

passport.use(
  new LocalStrategy((username: string, password: string, done: any) => {
    if (
      username === expectedUser.username &&
      password === expectedUser.password
    ) {
      return done(null, { username: "gabor", password: "password" });
    } else {
      return done(null, false, { message: "Incorrect credentials" });
    }
  })
);

app.post(
  "/login",
  passport.authenticate("local", { failureRedirect: "/login" }),
  (req: express.Request, res: express.Response) => {
    res.redirect("/test");
  }
);

It might look a little scary, but don’t worry, it’s actually pretty simple. This is how the code above works: if we send a POST to /login, redirect to /test if the authentication succeeds, otherwise redirect to /login. That’s all it does. The reason I didn’t want to involve any database and complicated session management, because even this simple example has some gotchas that you should be aware of.

  1. bodyParser package is required for this module to work . Where is it documented? Well, at the time this post is being written, nowhere. Surely, you can find it in one of the examples in the official documentation, but it is among 6 other packages which we actually don’t need. Fortunately Stackoverflow always helps.
  2. This solution uses the passport-local package, which provides us the LocalStrategy strategy. This strategy is referenced by the local string which is being passed to passport.authenticate('local'). As we can read it in the documentation of the package, this string is not optional! This is something we can completely overlook if we just follow the Passport.js documentation or any tutorials we googled.
  3. The signature of the LocalStrategy callback comes from a form, where the input fields have to have the name='username' and name='password' attributes. This was documented in most of the tutorials I read, but reading the code it is less than obvious.
  4. In this example I did not want to deal with sessions. However, apparently passport does some session management in the background; therefore we need to serialize and deserialize, no matter what (see documentation under Sessions).

One step further: sessions

Now that we can authenticate our users by validating the data they are sending with login forms, time to set up a session management system. We will use 3 packages for that, let’s have a look at them, one-by-one:

express-session

Why do we need sessions and what problems do they solve? This chapter called Sessions in Express.js from the book Express Web Application Development sums it up pretty well. Because HTTP is stateless, we need a way to store user data between HTTP requests in order to associate one request to another. There are basically two main ways to do that: we can use cookies however it might be not the best solution as this data will be exposed to the client and more importantly: it can be altered. Therefore another approach is preferred: keeping user data on the server side, and associate these data-sets with an ID. That ID is what is going to be kept on the client and be sent with each request. To handle and manage these data-sets we will use what is called a Session store.

Here is a simple page visit example:

npm install express-session @types/express-session --save
//server.ts
import * as session from "express-session";

app.use(
  session({
    secret: "sshhhhhhhhh",
  })
);

app.get("/", (req, res) => {
  var sess = req.session;
  if (!sess.counter) {
    sess.counter = 0;
  }

  sess.counter++;

  res.render("home", { counter: sess.counter });
});

Express comes with a built-in store called the MemoryStore (this is what we are using by default in the example above), however it is strongly suggested not to use it other than only for test purposes, because of memory leaks and data loss. We can easily define another store which we are going to do. We are going to use RedisStore.

RedisStore

First, we have to install Redis on our computer of course. Then, we will install the connect-redis package which is designed for session management.

npm install connect-redis @types/connect-redis --save

We need to pass the session object to the RedisStore object when initializing.

import * as session from "express-session";
import * as rs from "connect-redis";

const RedisStore: rs.RedisStore = rs(session);
const app: express.Express = express();

app.use(
  session({
    secret: "yo",
    store: new RedisStore({
      host: "127.0.0.1",
      port: 6379,
      prefix: "sess",
    }),
  })
);

Now our sessions work the same, but now we are using Redis to store session information instead of the built in MemoryStore. How do we know that it works? Easy, just change the the port from 6379 (which is the default port for Redis) to something else and we can see that it will break.

Sessions with Passport.js

Now, let’s connect our previous Passport setup with Redis session management. In the following setup we will be able to not only validate whether the provided credentials are right, but also establish a session which follows the user, ready to be validated anytime.

import * as path from "path"; // Path string management - not relevant for this tut
import * as hbs from "express-handlebars"; // Templating engine - not relevant for this tut
import * as express from "express";
import * as passport from "passport";
import * as pl from "passport-local";
import * as bodyParser from "body-parser";
import * as session from "express-session";
import * as rs from "connect-redis";

const port: number = 5000;
const client: redis.RedisClient = redis.createClient();
const LocalStrategy: any = pl.Strategy;
const app: express.Express = express();
const RedisStore: rs.RedisStore = rs(session);

app.use(session({
    secret: "yo",
    store: new RedisStore({
        host: '127.0.0.1',
        port: 6379,
        prefix: 'sess'
    })
}));

const LocalStrategy: any = pl.Strategy;
const RedisStore: rs.RedisStore = rs(session);

app.use(passport.initialize());
app.use(passport.session());
app.use(bodyParser.urlencoded({ extended:true }));

// Login validator middleware
let isLoggedIn = (req: express.Request, res: express.Response, next: express.NextFunction) => {
    if (req.isAuthenticated()) {
        return next();
    }
    res.redirect('/');
}

// Hardcoded user
var expectedUser = {
    username: "gabor",
    password: "password"
}

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

passport.deserializeUser(function (id, done) {
  done(null, id);
});

// Local validator - username and password is received from the form from name="username" and name="password" inputs
// This strategy is referenced with the 'local' string
passport.use(new LocalStrategy((username: string, password: string, done: any) => {
        if (username === expectedUser.username && password === expectedUser.password) {
            return done(null, {username: "gabor", password: "password"});
        } else {
            return done(null, false, { message: "Incorrect credentials" });
        }
    }
));

// Calls the LocalStrategy authenticator middleware. Redirects to root if fails, redirects to /profile if succeeds
app.post('/login', passport.authenticate('local', { failureRedirect: '/' }), (req: express.Request, res: express.Response) => {
    res.redirect('/profile');
});

// Settings up the Handlebars templating engine
app.engine('hbs', hbs({
    extname: 'hbs',
    defaultLayout: 'main',
    layoutsDir: path.join(__dirname, 'views/layouts')
}));

app.set('view engine', 'hbs');
app.set('views', path.join(__dirname, 'views'));

// Home view - redirect to profile if user is logged in
// Increase a view counter on each visit
app.get('/', (req, res) => {
    if (req.isAuthenticated()) {
        return res.redirect('/profile');
    }

    var sess = req.session;
    if (!sess.counter) {
        sess.counter = 0;
    }

    sess.counter++;

    res.render('home');
}

// Profile view - using the isLoggedin middleware which redirects to root if user is not loggged in
app.get('/profile', isLoggedIn, (req: express.Request, res: express.Response) => {
    res.render('profile', {
        user: {
            name: JSON.stringify(req.session.passport.user.username),
            session: JSON.stringify(req.session)
        }
    });
});

// Logout
app.get('/logout', isLoggedIn, (req: express.Request, res: express.Response) => {
    req.logout();
    res.redirect('/');
})

app.listen(port);

A couple of things to note here, which were not really explained either in the documentation or in the tutorials I have found:

  • isAuthenticated() comes from Passport.js, even though it is not stated in the documentation.
  • When we define our LocalStrategy we return a user object if the authentication succeeds. That object is what gets injected into our session, and that is what’s being evaluated when we call req.isAuthenticated().
  • To kill this user object in the session we can call req.logout(). There will be nothing to evaluate for isAuthenticated(), thus will return false.

The nice thing about this approach is that we only need to evaluate the user credentials when the user logs in, and from then we only have to maintain his/her session, which can be filled up with any kind of data - which will never leave the server, as all we send to the client is a session ID.