The old days of throwing up a CGI app protected by an .htpasswd file are long gone. While you could still do it, more sophisticated apps serving increasingly demanding users will have requirements that make it untenable.
For example, what if you have a suite of apps and don’t want users to have to login again for each one? What if you don’t want to manage all those .htpasswd files, or users want to login with their existing social identities?
Keycloak is an open source IAM (Identity and Access Management) gateway. It supports all of the features called out above (SSO, social login) as well as many more including identity brokering (OIDC or SAML), multi-directory support (internal users can be sourced from several Active Directory and/or LDAP directories in preferred order) and customizable themes (configurable per-client for consistent UX).
Aside from the client authentication features you need, Keycloak is also easy to manage (you can use an administrative web console or API for management tasks, and there is a Dockerized version for local development and testing), highly performant and scalable (it supports HA clustering and can be deployed via your favorite container orchestration platform).
The rest of this article will focus on securing a Node.js app using Keycloak. For more information on Keycloak’s features, visit the official website. For technical details including installation options, refer to the Server Administration Guide.
Our focus is on app integration, so I’m not going to repeat a lot of things which can be gleaned from the documentation. To get started, we do need a few things…
First, a “client” in Keycloak parlance may not be what comes to mind when you
think of the typical “client/server” application. It is an abstraction
representing your service, so an “OIDC client” or “SAML client” is essentially
configuration on the Keycloak server. Our app is technically a client to the
Keycloak server, but we need to configure “Keycloak clients” for our
applications to use. This is done from the admin dashboard via Clients > Create > Provide a name and root URL
(localhost works for testing) then Save
(the default “openid-connect” is good). You can do more (as we’ll see below), but assuming
you’ve ran through the installation instructions and have a realm configured
it’s that easy to get started.
Once you have a client, we need a small piece of associated configuration so the Keycloak middleware we’ll see below knows how to work. Again from the admin dashboard go to Clients > the client you created above > Installation and select “Keycloak OIDC JSON”. Copy or download the provided JSON file and place it at the root of your project directory. You should commit this to version control along with the rest of your source code.
{
"realm": "yourRealm",
"auth-server-url": "https://your.server.tld/auth",
"ssl-required": "none",
"resource": "your-client-name",
"public-client": true,
"verify-token-audience": true,
"use-resource-role-mappings": true,
"confidential-port": 0
}
In another series where we’re working to deploy a containerized Node.js app on Amazon’s ECS, we use a simple “hello world” web service. I’m going to borrow that and show how to add authentication. Here’s our starting point:
'use strict';
const express = require('express');
const PORT = 8080;
const HOST = '0.0.0.0';
const app = express();
app.get('/', (req, res) => {
res.send('Hello World');
});
app.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);
The first thing we need to do is pull in the Keycloak middleware.
This provides several useful
methods we can use to control access to our services. We’ll see that in a bit,
first be sure to npm i keycloak-connect
, and then add the dependency:
const Keycloak = require('keycloak-connect')
Before we get into configuring or using Keycloak,
we also need a couple more dependencies for this to be useful to our users…
First, we need a session store to track our authenticated users. Since the
backing store included with express-session
is not production worthy and we want
to keep this as lightweight as possible (read: not pull in another dependency
like Redis), we also need a replacement memory store. With npm i express-session memorystore
and a couple lines of code we’re ready to go:
const session = require('express-session')
const MemoryStore = require('memorystore')(session)
Before we configure and enable Keycloak, we need
to configure the session storage. While the documented example configures
memorystore
as part of instantiating express-session
, we’ll break it out so it’s
easier to re-use with Keycloak:
const store = new MemoryStore({
checkPeriod: 86400000, // 24 hrs
max: 1000, // items in cache
ttl: 28800000, // 8 hrs
})
app.use(
session({
name: 'helloWorldApp', // ensure this is unique
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 86400000, // 24 hrs
},
store,
})
)
Now we have user sessions, as well as a memorystore
instance we can easily
reference when configuring the Keycloak middleware:
const keycloak = new Keycloak({ store })
app.use(keycloak.middleware())
We are finally ready to see the full power of Keycloak! The easiest way to do that is to use the protect() method like any other middleware. In our simple app we can wrap our lone endpoint in the no-argument version to ensure users must be authenticated:
app.get('/', keycloak.protect(), (req, res) => {
res.send('Hello World');
});
The simple case outlined above is intended for typical (interactive) web logins. A dialog will be presented (either one of the defaults or a custom theme you define in the client configuration) supporting user/password authentication against a configured user directory. This is 80% of what we typically need out of the box!
Keycloak offers a lot of flexibility to support additional use cases, but those require more configuration… For example, let’s say you have different types of users (e.g. normal users and admins) and want to restrict parts of the app accordingly. Another similar scenario would be a service exposing a web front-end that talks to a ReST back-end with different authentication requirements.
The solution to that is client roles which provides more granular RBAC. In our
contrived case we could create hello-world-user
and hello-world-admin
roles then
refer to them when calling the Keycloak middleware:
app.get('/', keycloak.protect('hello-world-user'), (req, res) => {
res.send('Hello World');
});
app.get('/admin', keycloak.protect('hello-world-admin'), (req, res) => {
res.send('Hello World');
});
Keycloak makes it easy to define groups and include users from your backing directories that map to our defined client roles for fine-grained access control to the public and admin areas.
Another common use case is service-to-service communication. For that you can use OIDC client service accounts. Simply set your client access type to “confidential” and toggle “Service Accounts Enabled” to “ON”. You can then retrieve and generate shared secrets under the “Credentials” tab and control exactly what can be accessed via client roles and scopes.
Real-world apps are often more complex then our sample. Often you will have many sets of endpoints associated with certain areas of your application or split by functionality. When you import these to stitch together the larger service, a useful pattern is defining arrays grouping the sets of endpoints by client role:
const fooRouter = require('./routers/foo')
const barRouter = require('./routers/bar')
const bazRouter = require('./routers/baz')
const quxRouter = require('./routers/qux')
const userRole = [fooRouter, barRouter, bazRouter]
const adminRole = [quxRouter]
// ...
app.use(keycloak.protect('hello-world-user'), userRole)
app.use(keycloak.protect('hello-world-admin'), adminRole)
We intentionally kept this article focused on the code… As you can see, it’s easy to integrate a Node.js app with Keycloak as an authentication provider.
So as not to belie the pre-requisite work, I want to be fully transparent and admit our assumptions. Namely that you have a user directory of some sort (internal, LDAP, Active Directory, social logins), a running Keycloak instance (a local dockerized version works for testing), and optionally configured client roles, scopes and groups (if you want to follow all the examples). Everything you need to know for those steps and more can be found in the following documentation: