javascript

Node.js's Underrated Combo: Passport and CASL

Diogo Souza

Diogo Souza on

Node.js's Underrated Combo: Passport and CASL

It's easy to get lost with dozens of plugins and frameworks when starting a new project that requires basic authentication and authorization capabilities. It doesn't have to be that way.

In this article, we're going to explore two valuable Node.js packages — Passport and CASL — that can help you boost the security of your application by providing both authentication and authorization functionality.

This pair of packages is commonly used, and loved, by many Node.js developers in their backend applications.

At the end of the post, we'll have a better understanding of what each one is capable of through the implementation of a practical sign-in/authorization example app. Let's dive in!

Setting Up Our Node.js Project

We're going to explore authentication and authorization concepts while building them, so let's start off by setting up a new project.

First, create a new Node.js project in a folder of your preference and configure it according to the following package.json:

json
{ "name": "express-passport-casl-example", "version": "0.0.1", "description": "Express app with Passport for authentication and CASL for authorization.", "author": "AppSignal", "dependencies": { "@casl/ability": "^5.2.2", "body-parser": "^1.19.0", "bootstrap": "^4.6.0", "connect-ensure-login": "^0.1.1", "ejs": "^3.1.6", "express": "^4.17.1", "express-session": "^1.17.1", "morgan": "^1.10.0", "passport": "^0.4.1", "passport-local": "^1.0.0" } }

We'll create the app's UI with EJS, and style it with Bootstrap, so we've included these libs.

For the sake of simplicity, we're not going to use a real database. To emulate the data, we'll use an in-memory database with a fake list of users generated from the https://randomuser.me/ open-source website.

For this, create a new folder and file called db/users.js and add the following:

javascript
var users = [ { id: 1, username: "brad", password: "admin", name: { first: "Brad", last: "Gibson", }, address: { street: { number: 12, name: "Start St" }, city: "Kilcoole", postcode: "93027", }, email: "brad.gibson@example.com", phone: "011-962-7516", picture: "https://randomuser.me/api/portraits/men/75.jpg", }, { id: 2, username: "zach", password: "admin", name: { first: "Zachary", last: "Wilson" }, address: { street: { number: 5189, name: "Simcoe St" }, city: "Springfield", postcode: "98448", }, email: "zachary.wilson@example.com", phone: "600-887-7510", picture: "https://randomuser.me/api/portraits/men/4.jpg", }, { id: 3, username: "anonymous", password: "anonymous", name: { first: "Anonymous", }, }, ]; exports.findUser = function (id, callback) { process.nextTick(function () { var idx = id - 1; if (users[idx]) { callback(null, users[idx]); } else { callback(new Error(`User ${id} does not exist`)); } }); }; exports.findUserByUsername = function (username, callback) { process.nextTick(function () { for (var i = 0, len = users.length; i < len; i++) { var user = users[i]; if (user.username === username) { return callback(null, user); } } return callback(null, null); }); };

Notice that some of the data was reorganized to better adapt to a JavaScript object rather than simple JSON.

The password comes in plain text as well, so be aware of that when migrating your example to use a real database.

We've created 3 users to make sure we can later set up three different situations for them, like an anonymous user, for example, that can log into the app but won't have permission to see anything.

Finally, create a file named index.js in the same folder and export the users:

javascript
exports.users = require("./users");

Profile Model

The example app we’re building will have four pages: a sign-in page, the homepage, the profile page, and an errors page.

The profile page will make use of a business entity object that is going to be useful when CASL needs to authorize access to it.

So, create a new folder called models, then add the following to the file profile.js created in the folder:

javascript
class Profile { constructor(props) { Object.assign(this, props); } } module.exports = Profile;

That’s just a model object that represents a profile whose properties are going to be defined dynamically.

Introducing Passport

Passport is a powerful Node.js middleware for Express-based apps that offers a lot of flexibility when dealing with the authentication of requests.

It integrates with all major protocols such as OpenID, OAuth 2.0, etc., and makes use of a concept called strategies to authenticate your requests.

To keep things simple, we're going to focus on the local HTML form strategy, which allows you to handle the authentication of your app by yourself, without interference from external providers.

For our example, the strategy will simply verify the username/password information provided within the requests. However, Passport can handle automatic authentication via Facebook or Google OAuth, for example.

All configs must be set up before the server starts up and Express routes are served. To define Passport's configs, let's create a new folder and file called auth/index.js at the root of the project.

The following code listing shows the file's content:

javascript
const Strategy = require("passport-local").Strategy; const passport = require("passport"); const db = require("../db"); // Passport local strategy passport.use( new Strategy(function (username, password, callback) { db.users.findUserByUsername(username, function (err, user) { if (err) { return callback(err); } if (!user) { return callback(null, false); } if (user.password != password) { return callback(null, false); } return callback(null, user); }); }) ); // Passport authenticated session. passport.serializeUser(function (user, callback) { callback(null, user.id); }); passport.deserializeUser(function (id, callback) { db.users.findUser(id, function (err, user) { if (err) { return callback(err); } callback(null, user); }); }); module.exports = { configure(app) { // Init Passport and restore auth app.use(passport.initialize()); app.use(passport.session()); return passport; }, };

Since Passport’s local strategy is password-based, we always need to receive the username and password as the first and second params of the Strategy object, which are needed to check if the user is properly authenticated. As we don’t have any database layer going on, the check goes directly to the in-memory database list. If everything’s alright, the code calls the callback function that was also passed in as a param, which allows Express to move on with the request.

For the sake of security, we can't pass over the user's credentials every time a new request is performed, however, we still need to authenticate that same user for every request. As you may see at the end of the listing, we make use of Passport's login session, which works by creating a cookie set in the browser on the first successful user login attempt.

This way, all subsequent requests will only contain this cookie which will help Passport identify the right session for that particular user.

To be able to do so, Passport requires that we specify two functions for serializing and deserializing the user's data. As you can see in the code, we only store the user id to avoid over-inflating the session since the id is all we need to find the right user from the in-memory database.

Enabling sessions is optional, but it's highly recommended that you do so to allow Passport to keep the user logged in when they close the window and access it later.

Another good way of handling this is through OAuth 2.0 strategy, whereby, when starting the login flow, the user is redirected to a service provider (it could be a third-party, e.g. Facebook or Google, or an in-house provider) that will authorize access which, if granted, will redirect the user back to your application with a code in hand. Your application then makes use of this code by exchanging it with an access token which, in turn, will be the only information transferring from client to server. Since the user's credentials aren't being transferred, the app's requests are safer. Plus, there's no need for a session anymore. You can read more on Passport's OAuth 2.0 strategy here.

Moving on, let's attach the Passport configs that we talked about to the Express server by creating a new file named server.js at the root folder and add the following code to it:

javascript
const express = require("express"); const app = express(); // Session handling app.use( require("express-session")({ secret: "appsignal secret", resave: false, saveUninitialized: false, }) ); // Logging, body parsing app.use(require("morgan")("combined")); app.use(require("body-parser").urlencoded({ extended: true })); // Passport auth const passport = require(`./auth`).configure(app); const PORT = process.env.PORT || 3000; app.listen(PORT); console.debug(`Server listening on port: ${PORT}`);

To enable Passport session, we also have to make use of Express's session middleware via the express-session package. We pass it three parameters:

  • secret: this is the only required param and, as you may guess, it is the secret that will be used to sign the session ID cookie. You can define whatever string you want (or array of strings), however, remember that this is an example. In real-world apps, you must store it in a safer location such as a remote config file or environment.

  • resave: this param allows Express to force a session to be saved into the session store when new requests arrive regardless of whether the session was modified during the request or not. We're only setting this to false because it defaults to true and, in the case of Passport, it handles store resaving on its own.

  • saveUninitialized: in a similar way to the previous param, this one is responsible for forcing a session in an "uninitialized" (i.e. it is new and untouched) state to be saved back to the store. Again, Passport already takes care of this, so let's default to false.

Simple, isn’t it? We’re also adding some extra features like default request logging via morgan, and the express-session needed by Passport to support user-awareness features.

However, this authentication isn’t useful if we don’t have endpoints exposed in our Express app. Before we fix that, let’s understand what CASL’s about!

Introducing CASL

CASL is a JavaScript authorization library that restricts the resources a given user is allowed to access. It’s a very powerful and broad framework since it allows integration with many major ecosystems such as React, Angular, Vue, integration via a database with Mongoose, as well as API-based integration for Express apps.

To add authorization capabilities to your APIs, CASL makes use of the abilities concept, which is a mapping between users and their permissions.

After you set up the abilities, which can also be configured dynamically or even retrieved from the database, the other layers that need to check for permission can turn to CASL for it.

Let’s now configure CASL by creating another folder and file called authz/abilities.js and adding the following to it:

javascript
const { AbilityBuilder, Ability } = require("@casl/ability"); function defineRulesFor(user) { const { can, rules } = new AbilityBuilder(Ability); // If no user, no rules if (!user) return new Ability(rules); switch (user.id) { case 1: can("manage", "all"); break; case 2: can("read", "Profile", { id: user.id, }); break; default: // anonymous users can't do anything can(); break; } return new Ability(rules); } module.exports = { defineRulesFor, };

Here, we’re hardcoding the authorizations. It’s just a way to simply demonstrate how CASL’s mapping works and how you can associate a Profile, for instance, to a user (e.g. the user with id 2).

CASL comes with some pre-built permissions, such as manage, that states that a user has permission to all actions in the system, i.e., they're an admin user.

Other common permissions relate to the usual CRUD operations: create, read, update, and delete. However, you can create custom app-level permissions, such as publish, log, etc.

The function can() defines this mapping process by receiving the permission (or list of permissions) as the first param, and the permission target (or list of targets) as the second param. The targets are the objects we’re stating that a given user has permission to.

To ensure that each logged-in user has the right set of permissions, we may call this config every time a user signs in. For this, create a new index.js file in the authz folder and add the following code into it:

javascript
const abilities = require("./abilities"); module.exports = { configure(app) { app.use((req, _, next) => { req.ability = abilities.defineRulesFor(req.user); next(); }); }, };

Call the code in the server.js file, right after Passport config:

javascript
... // Passport auth const passport = require(`./auth`).configure(app); // CASL authz require(`./authz`).configure(app); ...

Profile API

We now need to set up the routes for our Express app. After all, there’s no use for authenticating users if not to access endpoints. The same goes for authorization.

To do this, let’s first create a new folder called routes at the root of the project. Then, add the following code for the routes mapping into a new file named index.js:

javascript
const { ForbiddenError } = require("@casl/ability"); const Profile = require("../models/profile"); module.exports = { configure(passport, app) { app.get("/", function (req, res) { res.render("home", { user: req.user }); }); app.get("/login", function (req, res) { res.render("login"); }); app.post( "/login", passport.authenticate("local", { failureRedirect: "/login" }), function (_req, res) { res.redirect("/"); } ); app.get("/logout", function (req, res) { req.logout(); res.redirect("/"); }); app.get( "/profile", require("connect-ensure-login").ensureLoggedIn(), function (req, res) { const profile = new Profile({ ...req.user, }); ForbiddenError.from(req.ability).throwUnlessCan("read", profile); res.render("profile", profile); } ); app.put( "/profile", require("connect-ensure-login").ensureLoggedIn(), function (req, res) { const profile = new Profile({ ...req.user, }); ForbiddenError.from(req.ability).throwUnlessCan("update", profile); res.render("profile", profile); } ); }, };

This is a crucial file in our project. This is where we gather both Passport and CASL settings to determine all the app endpoints and their respective auth and authz features.

The first four endpoints take care of authenticating the users, providing a login and logout entry point to the system, and returning the proper EJS views for each scenario. The /login endpoint, specifically, also needs to call Passport’s authenticate method passing it the chosen strategy (local) and a fallback URL in case login fails.

This is important because you can easily set up the page or even parameters that Passport will pass to your Express fail route in case anything bad happens.

The last two endpoints take care of the /profile URL for both retrieving and updating a profile. Note that we’re always calling the connect-ensure-login’s method to ensure the user is still authenticated based on the current session. This is all done automatically by Passport.

Their functions are, however, checking CASL's ability to see if the logged-in user has access to read or update on that profile for every request. The class ForbiddenError helps us with the authorization check, throwing an exception in case access isn't allowed there.

Speaking of exceptions, CASL doesn’t have any way of letting EJS know that an authorization exception has occurred other than throwing an error. Because of that, we’ll have to implement an error-handling strategy, otherwise, the UI will have a response similar to the one shown below:

Forbidden error
Forbidden error thrown by CASL checks

We can easily solve that by creating a new file called error-handler.js at the root folder with the following code:

javascript
const { ForbiddenError } = require("@casl/ability"); module.exports = function errorHandler(error, _req, res, _next) { if (error instanceof ForbiddenError) { return res.render("error", { error: `You don't permission to ${error.action} this ${error.subjectType}`, }); } };

That’s a great way to have customized error handling that won’t break the UI.

Finally, make sure your server.js configs match the following:

javascript
const express = require("express"); const app = express(); const errorHandler = require("./error-handler"); // EJS templates app.set("views", __dirname + "/views"); app.set("view engine", "ejs"); // Logging, parsing, session handling app.use(require("morgan")("combined")); app.use(require("body-parser").urlencoded({ extended: true })); app.use( require("express-session")({ secret: "appsignal secret", resave: false, saveUninitialized: false, }) ); // Passport auth const passport = require(`./auth`).configure(app); // CASL authz require(`./authz`).configure(app); // Models require(`./routes`).configure(passport, app); // Must be after the routes app.use(errorHandler); const PORT = process.env.PORT || 3000; app.listen(PORT); console.debug(`Server listening on port: ${PORT}`);

Testing

Before we test the app, we need the views. Views are known to be verbose due to the amount of HTML, CSS, and external code. So, to simplify our post, I’ll let you copy them from the GitHub repo since the EJS files don’t have anything special.

Make sure that all Node.js dependencies were already downloaded via npm install, and run the following command to start the app:

bash
npm run start

When it starts up, go to http://localhost:3000/ and the following screen will appear:

Welcome page
Welcome page at localhost

When you click to log in, the screen below will show up. Remember that they don’t have any authentication or authorization checks because they are public pages.

Login page
Login page at localhost

Let’s try to log in with Brad, the first user on our in-memory database whose username is brad and password’s admin. When you hit the submit button, you’ll be redirected to the following screen:

Homepage
Homepage at localhost

It’s important to note that the backend holds the session. So, if you close your browser window and open it again, you’ll see that you’re still logged in, which is the behavior we want.

The homepage isn’t verifying any permissions. The permission check starts at the profile page. Let’s test Brad’s permissions by clicking on the View your profile button. You’ll be redirected to the following screen:

Profile page
Brad’s profile page with user’s information

Let’s log out now and test what happens when we try the same flow with the user anonymous and password anonymous that we added in our in-memory database list, remember?. The following will be the outcome for accessing the profile page:

Forbidden page
image_tooltip
Forbidden page at localhost

Conclusion

You can find the source code for this tutorial here.

You can adjust the error handling and permission control as much as you want. As we’ve seen, both Passport and CASL give you enough flexibility to handle various use cases.

Authenticating and authorizing users doesn’t have to be painful, you just need to match the right pieces of the puzzle. Now’s your turn. Fork this project, and add new features, such as database integration. That’s a great start!

Looking forward to seeing what you guys come up with!

P.S. If you liked this post, subscribe to our new JavaScript Sorcery list for a monthly deep dive into more magical JavaScript tips and tricks.

P.P.S. If you need an APM for your Node.js app, go and check out the AppSignal APM for Node.js.

Diogo Souza

Diogo Souza

Diogo Souza has been passionate about clean code, software design and development for more than ten years. If he is not programming or writing about these things, you'll usually find him watching cartoons.

All articles by Diogo Souza

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps