Securing Node.js Applications with Kerberos Authentication

Introduction

Node.js has become a popular platform for building web applications and services, thanks to its event-driven architecture, rich ecosystem of packages, and ability to handle high concurrency. However, as with any web-facing software, security is a critical consideration. One way to protect access to Node.js applications is by integrating with an enterprise authentication system like Kerberos.

In this article, we‘ll take a deep dive into how to implement Kerberos authentication in a Node.js application using the express-kerberos module. We‘ll cover the prerequisites, walk through sample code, and discuss how to extend the basic authentication with authorization logic and dynamic request routing.

While the examples focus on using Node.js to secure a Kibana deployment, the concepts are applicable to providing SSO authentication and smart proxying for any kind of Node.js application. By the end of this article, you‘ll have a solid understanding of how to leverage Kerberos to protect your Node.js apps and services.

Kerberos Authentication Flow

Before we get into the specifics of implementation in Node.js, let‘s review at a high level how Kerberos authentication works.

Kerberos is a network authentication protocol that uses "tickets" to allow nodes to prove their identity over a non-secure network in a secure manner. It is commonly used in Windows Active Directory environments to enable single sign-on for enterprise applications.

The key components of a Kerberos system are:

  • Key Distribution Center (KDC) – a trusted third party that issues encrypted tickets used for authentication. In a Windows environment, this is an Active Directory domain controller.

  • Client – the user or application requesting access to a resource or service.

  • Server – the application or service the client wants to access. This is where we‘ll be integrating Kerberos authentication.

The simplified authentication flow looks like this:

  1. Client requests access to the Server, sending its identity to the KDC.

  2. KDC verifies the Client identity and sends back a Ticket Granting Ticket (TGT) encrypted with the Client‘s password hash.

  3. Client decrypts the TGT with its password hash, proving the identity of both the Client and KDC.

  4. Client sends the TGT back to the KDC requesting a Service Ticket (ST) for access to the specific Server.

  5. KDC sends the Client an ST encrypted with the Server‘s secret key.

  6. Client forwards the ST to the Server.

  7. Server decrypts the ST with its secret key, thus verifying the Client‘s identity and allowing access.

The whole process happens transparently to the user after their initial log in to the Windows domain. The TGT and ST tickets are attached to HTTP requests in an Authorization header, which is how a Node.js application would receive them.

With this background in mind, let‘s look at how to set up a Node.js application to accept and validate Kerberos tickets.

Setting Up Node.js with Kerberos

Prerequisites

To integrate Kerberos authentication into a Node.js application, you‘ll need to have a few things in place:

  • An Active Directory domain with a Domain Controller acting as the KDC
  • A service account in AD for the application
  • An SPN (Service Principal Name) mapped to the service account
  • A keytab file containing the secret key for the SPN

The specifics of setting these up are outside the scope of this article, but in short:

The SPN should match the URL that the application is accessed by. For example, if users will access the application via http://app.domain.com, then the SPN would be HTTP/app.domain.com. This SPN needs to be registered in Active Directory and mapped to the service account that will run the Node.js process.

The keytab file is generated from the KDC using the ktpass utility and the service account credentials. It contains the encrypted secret key that allows the application to decrypt the Service Ticket presented by the client.

Required Node.js Modules

With the Active Directory pieces in place, we can now set up our Node.js application. We‘ll be using the following modules:

  • express – web application framework
  • express-session – session middleware
  • express-kerberos – Kerberos authentication middleware
  • activedirectory – for looking up group information in AD
  • express-http-proxy – for proxying authenticated requests to a backend service

You can install these into a new Node.js project with npm:

npm install express express-session express-kerberos activedirectory express-http-proxy

Note that express-kerberos currently only works with version 0.0.24 or lower of the underlying kerberos module, so you may need to specify that explicitly in package.json.

Basic Authentication

With the dependencies in place, we can add Kerberos authentication to an Express application with just a few lines of code:

const express = require(‘express‘);
const kerberos = require(‘express-kerberos‘);
const app = express();

app.use(kerberos.default());

app.use(‘/‘, (req, res) => {
  res.send(`Hello ${req.userPrincipalName}!`);
});

app.listen(80);

Here‘s what‘s happening:

  1. Create a new express app
  2. Add the kerberos default middleware – this will intercept requests, validate the Kerberos ticket, and add the authenticated user principal to the request object.
  3. Add a request handler that simply returns a greeting to the authenticated user
  4. Start the server on port 80

The kerberos middleware assumes the keytab file is located at /etc/krb5.keytab. You can override this by setting the KRB5_KTNAME environment variable to the keytab file path.

With this simple set up, the application is now protected with Kerberos authentication. Only users who have a valid TGT from the KDC will be able to access it. Unauthenticated requests will receive a 401 Unauthorized response.

Adding Authorization

While authenticating users is important, most real-world applications also need to authorize access based on the user‘s role or permissions. A common approach is to base this on Active Directory group membership.

We can extend our basic application to look up the authenticated user‘s group memberships and make authorization decisions based on that:

const express = require(‘express‘);
const kerberos = require(‘express-kerberos‘);
const ActiveDirectory = require(‘activedirectory‘);
const session = require(‘express-session‘);

const app = express();

// Configure express-session
app.use(session({
  secret: ‘supersecret‘,
  resave: false,
  saveUninitialized: true
}));

// Create ActiveDirectory client
const ad = new ActiveDirectory({
  url: ‘ldap://dc.domain.com‘,
  baseDN: ‘dc=domain,dc=com‘
});

// Custom authorization middleware
function authorize(req, res, next) {
  // Don‘t re-authorize if already done
  if (req.session.authorized) return next(); 

  ad.isUserMemberOf(req.userPrincipalName, ‘CN=AppUsers,CN=Users,DC=domain,DC=com‘, (err, isMember) => {
    if (err) return next(err);

    if (!isMember) {
      return res.status(403).send(‘Access Denied‘);
    }

    // Store authorization in session
    req.session.authorized = true;
    next();
  });
}

app.use(kerberos.default());
app.use(authorize);

app.use(‘/‘, (req, res) => {
  res.send(`Hello ${req.userPrincipalName}!`);
});

app.listen(80);

The key changes here are:

  1. Set up express-session to allow storing authorization state across requests
  2. Create an ActiveDirectory client for querying group memberships
  3. Define a custom authorization middleware that uses the ActiveDirectory client to check if the authenticated user is a member of a specific group. If not, it returns a 403 Forbidden.
  4. If authorized, it stores that fact in the session so subsequent requests don‘t need to reauthorize.

With this set up, not only does a user need a valid Kerberos ticket to access the application, but they must also be a member of the "AppUsers" AD group.

You could extend this further to define different authorization levels based on AD group memberships, allowing for fine-grained access control.

Proxying Requests

So far our example application has just returned a simple response. In many cases though, the Node.js process acts as a reverse proxy, forwarding authenticated requests to a backend service.

This is a common pattern for securing applications that don‘t have built-in authentication mechanisms. A popular example is using a Node.js proxy to add SSO authentication in front of an Elastic Kibana deployment.

Here‘s an example of how to extend our application to act as an authenticating reverse proxy:

const express = require(‘express‘);
const kerberos = require(‘express-kerberos‘);
const ActiveDirectory = require(‘activedirectory‘);
const session = require(‘express-session‘);
const proxy = require(‘express-http-proxy‘);

const app = express();

// ... session & ActiveDirectory setup ...
// ... authorization middleware ...

app.use(kerberos.default());
app.use(authorize);

app.use(‘/‘, proxy(‘http://kibana.internal:5601‘, {
  proxyReqOptDecorator: (proxyReqOpts, srcReq) => {
    // Add auth info to backend request
    proxyReqOpts.headers[‘X-Auth-Request-User‘] = srcReq.userPrincipalName;
    proxyReqOpts.headers[‘X-Auth-Request-Groups‘] = srcReq.session.groups.join(‘,‘);
    return proxyReqOpts;
  },
  proxyReqPathResolver: req => req.originalUrl
}));

app.listen(80);

The changes from the authorization example are:

  1. Instead of a simple request handler, we use express-http-proxy to forward requests to a backend service (Kibana in this case).
  2. In the proxy configuration, we add a proxyReqOptDecorator function that modifies the proxied request, adding headers containing the authenticated username and authorized groups. This allows the backend service to make use of that information if needed.
  3. We also specify a proxyReqPathResolver that preserves the original request URL when proxying.

With this setup, Kibana can be accessed by users using their normal Active Directory credentials, without Kibana itself needing to be aware of AD or Kerberos. The Node.js proxy handles authenticating and authorizing users, and can even pass that information through to Kibana.

Of course, you can get even fancier with the proxy behavior – transforming request payloads, aggregating multiple backend services, etc. Express and the Node.js ecosystem provide a wide array of tools for implementing sophisticated proxying behavior.

Conclusion

Integrating Kerberos authentication into Node.js applications is a powerful way to provide single sign-on access and enforce user authorization in an Active Directory environment. The express-kerberos middleware makes it straightforward to implement in an Express-based application.

While we‘ve focused on a specific use case of securing a Kibana instance, the same principles can be applied to providing authentication and smart proxying in front of any kind of backend service or application.

The extensibility of the Express model, combined with the wealth of available Node.js modules, makes it possible to build very sophisticated authenticated proxies that would be difficult to achieve with alternative tools like nginx.

Adding Kerberos support allows Node.js applications to participate fully in an enterprise authentication infrastructure, enabling seamless SSO experiences for users.

Hopefully this deep dive has given you a solid foundation for working with Kerberos in your own Node.js applications! Let me know in the comments if you have any questions or suggestions.

Similar Posts