node-openam-agent: Your App’s New Friend

This blog is posted on behalf of Zoltan Tarcsay.


As you may know, the purpose of OpenAM Policy Agents is to enforce authentication and authorization for web resources. But while OpenAM itself has been becoming ever more feature-rich and easy to use over the years, the Policy Agents have stayed roughly the same. The ways that web resources are built and accessed today demand new enforcement strategies. The openam-agent module for Node.js takes a new approach to addressing these concerns.

The Old Ways

It sometimes feels like Policy Agents are remnants of an era when all that people had for web content was static (or server generated) HTML pages with fixed URLs, and possibly some SOAP web services.

There are two things that a web policy agent can do (OK, 3):

  • Enforce the validity of a user’s SSO session ID (which is sent in a Cookie header)
  • Enforce authorization for requested URLs served by the web container.
  • In addition, Java agents allow you to use JAAS and the OpenAM client SDK in your Java application.

If you’ve ever tried to use the OpenAM client SDK for Java, you will probably agree that it’s somewhat complicated and time consuming. Also, it doesn’t give you much control over the agent itself (think of caching, event handling, communication with OpenAM). And if you ever tried to use an OpenAM client SDK with anything other than Java, you probably found that there isn’t one (OK, there’s one for C).

So for those whose website are powered by JavaScript, Ruby, Python, PHP or anything else, there are two options:

  • Having a web agent on a web proxy server which enforces URL policies
  • Integrating with OpenAM directly by writing custom code (i.e. a policy agent)

Good news: it turns out that writing a policy agent is not so difficult. It has to do three things:

  • Intercept requests when some resource is being accessed
  • Get an access control decision based on the request (from OpenAM)
  • Throw an error or let the request pass

Now that we know that agents are not that big of a deal, it seems a little unreasonable that the existing ones are so opinionated about how people should use them. I mean, they can’t even be extended with custom functionality, unless you add some C code and recompile them…

Your New Friend

What you are about to see is a new approach to how agents should behave, most importantly, from the developer’s point of view. This groundbreaking new idea is that, instead of being an arch enemy, the policy agent should be the developer’s friend.

As an experiment, a JavaScript policy agent for Node.js was born. It is meant to be a developer-friendly, hackable, light-weight, transparent utility that acts as your app’s spirit guide to OpenAM. Everything about it is extensible and all of its functionality is exposed to your Node.js code through public APIs. It also comes with some handy features like OAuth2 token validation or pluggable backends for caching session data.

It has of the following parts:

  • OpenAMClient
    • This is a class that knows how to talk to OpenAM
  • PolicyAgent
    • Talks to OpenAM through a pluggable OpenAMClient to get decisions, identity data, etc.
    • Has its own identity and session
    • Receives notifications from OpenAM (e.g. about sessions)
    • Has a pluggable cache for storing stuff (e.g. identity information)
    • Can intercept requests and run it through pluggable enforcement strategies (i.e. Shields)
    • You can have as many as you want (more on this later)
  • Shield
    • A particular enforcement strategy (e.g. checking an OAuth2 access_token)
    • Gets a request, runs a check, then fails or succeeds
    • Can be used with any agent within the app
  • Cache
    • An interface to some backend where the agent can store its session data

Getting Started

OK, let’s look at some code.

First, create a new Node.js project and install the dependencies:

mkdir my-app && cd my-app
 npm init -y
 npm install --save express openam-agent
 touch index.js

Next, let’s add some code to index.js:

var express = require('express'),
 openamAgent = require('openam-agent'),
 app = express(),
 agent = openamAgent({openamUrl: 'https://openam.example.com/openam'});

app.get('/', agent.shield(openamAgent.cookieShield({getProfiles: true})), function (req, res) {
 res.send('Hello, ' + req.session.userName);
 });
 app.listen(1337);

Done, you have a web application with a single route that is protected by a cookie shield (it checks your session cookie). The cookie shield also put the user’s profile data into the req object, so you can use it in your own middleware.

Express

It’s important to note here that openam-agent currently only works with the Express framework, but the plan is to make it work with just regular Node.js requests and responses as well.

In the example above, the variable app will be your Express application. An express app is a collection of routes (URL paths) and middleware (functions that handle requests that are sent to the routes). One route can have multiple middleware, i.e. a requests can be sent through a chain of middleware functions before sending a response.

The agent fits beautifully in this architecture: the agent’s agent.shield(someShield) function returns a middleware function for Express to handle the request. Which means that you can use any enforcement strategy with any agent with any route, as you see fit.

Policies

You can do things like this:

var policyShieldFoo = openamAgent.policyShield({application: 'foo'}),
 policyShieldBar = openamAgent.policyShield({application: 'bar'});

app.get('/my/awesome/api/foo', agent.shield(policyShieldFoo));
 app.get('/my/awesome/api/foo/oof', function (req, res) {
 // this is a sub-resource, so it's protected by the foo shield
 });

app.get('/my/awesome/api/bar', agent.shield(policyShieldBar));
 app.get('/my/awesome/api/bar', function (req, res) {
 // this middleware is called after the bar shield on the same path, so it's protected
 });

In this case you have two Shields, both using a different application (or policy set) in OpenAM; you can then use one for one route, and the other for the other route. Whether the policy shield will apply to the incoming request is determined by path and the order in which you mounted your middleware functions.

Note that the agent needs special privileges for getting policy decisions from OpenAM, so it will need some credentials (typically an agent profile) in OpenAM:

var agent = openamAgent({
 openamUrl: 'https://openam.example.com/openam',
 username: 'my-agent',
 password: 'secret12'
 })

When the agent tries to get a policy decision for the first time, it will create a session in OpenAM for itself.

Note that a policy decision needs a subject, so the request will need to contain a valid session ID.

OAuth2

This is how you enforce a valid OAuth2 token:

app.use('/my/mobile/content', agent.shield(openamAgent.oauth2Shield()), function (req, res) {
 // the OAuth2 token info is in req.session.data
 // if you wanted to check the scopes against something, you could write a shield to do it
 });

Notifications and CDSSO

There are cases when the agent needs to be able to accept data from OpenAM. One example is notifications (e.g. when a user logs out, OpenAM can notify the agents so they can clear that session from their cache). The node-openam-agent lets you mount a notification route to your app as such:

var agent = openamAgent({notificationsEnabled: true});
 app.use(agent.notifications('/some/path/to/the/notifications/endpoint'));

CDSSO is also possible (although it becomes tricky when your original request is anything other than GET, because of the redirects):

var agent = openamAgent({notificationsEnabled: true});
 app.use(agent.cdsso('/some/path/to/the/cdsso/client/endpoint'));

Note: OpenAM needs to know that you want to use the cdcservlet after you log in (this servlet creates a SAML1.1 assertion containing the user’s session ID, which is then POSTed to the agent through the client’s browser). For this, you will need to create a web agent profile and enable CDSSO.

Extensions

The current features add some extra functionality to the classic agent behavior, but there is so much more that can be done, some of it will be very specific to each application and how people use OpenAM.

Extensibility is at the heart of this agent, and it is meant to be very simple. Here’s an example of a custom Shield.

First, extend the Shield class:

var util = require('util'),
 Shield = require('openam-agent').Shield;

/**
 * @constructor
 */
 function UnicornShield(options) {
 this.options = options;
 }

UnicornShield.prototype.evaluate = function (req, success, fail) {
 // check if this request has a unicorn in it
 // (we could also use this.agent to talk to OpenAM)
 if (this.options.foo && req.headers.unicorn) {
 success();
 } else {
 fail();
 }
 };

And then use it in your app:

app.use(agent.shield(new UnicornShield({foo: true})));

There’s all sorts of docs (API and otherwise) in the wiki if you’re interested in extending the agent.

More stuff

There is much more to show and tell about this agent, especially when it comes to specific use cases, but it doesn’t all fit in one blog post. Stay tuned for more stuff!

Contributions

node-openam-agent is a community driven open source project on GitHub, and it is not owned or sponsored by ForgeRock. The software comes with an MIT license and is free to use without any restrictions but comes without any warranty or official support. Contributions are most welcome, please read the wiki, open issues and feel free to submit pull requests.

©2019 ForgeRock - we provide an identity and access platform to secure every online relationship for the enterprise market, educational sector and even entire countries. Click to view our privacy policy and terms of use.

Log in with your credentials

Forgot your details?