Sharing micro-service authentication using Nginx, Passport and Redis

Abgeschlossen_1
Wikimedia Commons, Abgeschlossen 1, by Montillona

And we are back with the regularly scheduled programming, and I didn’t talk about micro-services in a while. Here is what is occupying my days now – securing a micro-service system. Breaking down a monolith into a collection of micro-services has some wonderful properties, but also some nasty side-effects. One of them is authentication.

The key problem of a micro-service system is ensuring that its federated nature is transparent to the users. This is easier to accomplish in the areas that are naturally federated (e.g. a collection of API end points). Alas, there are two areas where it is very hard to hide the modular nature of the system: composing Web pages from contributions coming from multiple services, and security. I will cover composition in one of the next posts, which leaves us with the topic of the day.

In a nutshell, we want a secure system but not at the expense of user experience. Think about European Union before 1990. In some parts of Europe, you could sit in a car, drive in any direction and cross five countries before the sunset. In those days, waiting in line at the custom checkpoint would get old fast and could even turn into quite an ordeal in extreme cases.

Contrast it to today – once you enter EU, you just keep driving, passing countries as if they are Canadian provinces. Much better user experience.

We want this experience for our micro-service system – we want to hop between micro-services, be secure yet not being actively aware that the system is not a monolith.

It starts with a proxy

A first step in securing a micro-service system starts at a proxy such as Nginx. Placing a multi-purpose proxy in front of our services have several benefits:

  1. It allows us to practice friendly URL architecture – we can proxy nice front end URLs such as http://foobar.io/users or http://foobar.io/projects to separate micro-services (‘users’ and ‘projects’, respectively). It also goes around the fact that each Node.js service runs on a separate port (something that JEE veterans tend to hate in Node.js, since several apps running in the same JEE container can share ports).
  2. It allows us to enable load balancing – we can proxy the same front end location to a collection of service instances running in different VMs or virtual containers (unless you are using a PaaS, at which point you just need to increment the instance counter).
  3. It represents a single domain to the browser – this is beneficial when it comes to sharing cookies, as well as making Ajax calls without tripping the browser’s ‘same origin’ policy (I know, CORS, but this is much easier)

If we wanted to be really cheap, we could tack on another role for the front end proxy – authentication. Since all the requests are passing through it, we can configure a module to handle authentication as well and make all the micro-services behind it handle only authenticated requests. Basic auth module is readily available for Nginx, but anything more sophisticated is normally not done this way.

Use Passport

Most people will need something better than basic auth, and since we are using Node.js for our micro-service system, Passport is a natural choice. It has support for several different authentication strategies, including OAuth 1.0 and OAuth 2.0, and several well known providers (Twitter, Facebook, LinkedIn). You can easily start with a stock strategy and extend it to handle your unique needs (this will most likely be needed for OAuth 2.0 which is not a single protocol but an authentication framework, much to the dismay of Eran Hammer).

Passport is a module that you insert in your Node.js app as middleware, and hook up to the Express session. You need to expose two endpoints: ‘/auth/<provider>’ and ‘/auth/<provider>/callback’. The former is where you redirect the flow in order to start user login, while the latter is where the Auth server will call back after authenticating the user, bringing in some kind of authorization code. You can use the code to go to the token endpoint and obtain some kind of access token (e.g. bearer token in case of OAuth 2.0). With the access token, you can make authenticated calls to downstream services, or call into the profile service and fetch the user info. Once this data is obtained, Passport will tack it on the request object and also serialize it in the session for subsequent use (so that you don’t need to authenticate for each request).

Passport module has a very nice web site with plenty of examples, so we don’t need to rehash them here. Instead, I want us to focus on our unique situation of securing a number of micro-services working together. What that means is:

  1. Each micro-service needs to be independently configured with Passport. In case of OAuth (1.0 or 2.0), they can share client ID and secret.
  2. Each micro-service can have a different callback URL as long as they all have the same domain. This is where we reap the benefit of the proxy – all the callback URLs do have the same domain thanks to it. In order to make this work, you should register your client’s callback_uri with the authentication provider as the shared root for each services’ callback. An actual callback passed to the authentication endpoint for each micro-service can be longer than the registered callback_uri as long as they all share common root.
  3. Each micro-service should use the same shared authentication strategy and user serialization/deserialization.

Using this approach, we can authenticate paths served by different micro-services, but we still don’t have Single Sign-On. This is because Express session is configured using an in-memory session store by default, which means that each micro-service has its own session.

Shared session cookie

This is not entirely true: since we are using the default session key (or can provide it explicitly when configuring session), and we are using single domain thanks to the proxy, all Node.js micro-services are sharing the same session cookie. Look in the Firebug and you will notice a cookie called ‘connect.sid’ once you authenticate. So this is good – we are getting there. As I said, the problem is that while the session cookie is shared, this cookie is used to store and retrieve session data that is in memory, and this is private to each micro-service instance.

This is not good: even different instances of the same micro-service will not share session data, let alone different micro-services. We will be stuck in a pre-1990 Europe, metaphorically speaking – asked to authenticate over and over as we hop around the site.

Shared session store

In order to fix this problem, we need to configure Express session to use an external session store as a service. Redis is wonderfully easy to set up for this and works well as long as you don’t need to persist your session forever (if you restart Redis, you will lose session data and will need to authenticate again).


var ropts = {
   host: "localhost",
   port: 5556,
   pass: "secret"
}

...

    app.use(express.session({ key: 'foobar.sid',
                             store: new RedisStore(ropts),
                             secret: 'secret'}));
    app.use(passport.initialize());
    app.use(passport.session());

I am assuming here that you are running Redis somewhere (which could range from trivial if you are using a PaaS, to somewhat less trivial if you need to install and configure it yourself).

What we now have is a system joined at both ends – Nginx proxy ensures session cookie is shared between all the micro-service instances it proxies, and Redis store ensures actual session data is shared as well. The corollary of this change is that no matter which service initiated the authentication handshake, the access token and the user profile are stored in the shared session and subsequent micro-services can readily access it.

micro-authentication

Single Sign Off

Since we have Redis already configured, we can also use it for pub/sub to propagate the ‘logout’ event. In case there is state kept in Passport instances in micro-services, a system-wide logout for the session ensures that we don’t have a “partially logged on” system after a log out in one service.

I mentioned Redis just for simplicity – if you are writing a micro-service system, you most likely have some kind of a message broker, and you may want to use it instead of Redis pub/sub for propagating logout events for consistency.

Calling downstream services

Not all micro-services will need full Passport configuration. You can configure services that require access token – they can just look for ‘Authorization’ header and refuse to do anything if it is not present. For example, for OAuth 2.0 authentication, the app will need something like:


Authorization: Bearer 0b79bab50daca910b000d4f1a2b675d604257e42

The app can go back to the authentication server and verify that the token is still valid, or go straight to the profile endpoint and obtain user profile using the token (this doubles as token validation because the profile service will protest if the token is not valid). API services are good candidates for this approach, at least as one of the authentication mechanisms (they normally need another way for app-to-app authentication that does not involve an actual user interacting with the site).

What about Java or Go?

This solution obviously works great if all the micro-services are written in Node.js. In a real-world system, some services may be written using other popular stacks. For example, what will happen if we write a Java service and try to participate?

Obviously, running a proxy to the Java micro-service will ensure it too has access to the same session cookie. Using open source Redis clients like Jedis will allow it to connect to the same session store. However, the picture is marred slightly by the fact that Express session signs the cookie with a combination of HMAC-Sha256 and ‘base64’ digest, plus some additional tweaking. This is obviously a very Express-centric approach and while it can be re-created on the Java side, there is this lingering feeling we created a Node-centric system and not a stack-agnostic one.

Java has its own session management system and you can see the JSESSIONID cookie sitting next to the one created by Express. I will need to study this more to see if I can make Java and Node share the session cookie creation and signing in a more stack-neutral way. In a system that is mostly Node.js with a Java service here and there, signing and unsigning the session cookie the way Express likes it may not be a big deal.

In addition, my current experimentation with Java points at creation of a JEE filter (middleware) that checks for the session cookie and redirects to authentication endpoints if a user is not found in the session. True Java authentication solutions are not used, which may or may not be a problem for you if you are a JEE veteran. JEE filters provide for wrapping HTTP requests so methods such as ‘getRemoteUser()’ can be implemented to provide the expected results to Java servlets.

I mentioned Go because it is an up and coming language that people seem to use for writing micro-services more and more these days. I have no idea how to write the session support for Go in this context so I am tossing this in just as food for thought. If you know more, drop me a line.

Closing thoughts

There are many alternative ways to solve this problem, and I profess my mixed feelings about authentication. For me, it is like broccoli – I understand that it is important and good for me, but I cannot wait to get to the ice cream, or whatever is the ice cream equivalent in writing micro-service systems. Spending time with Passport and OAuth 2.0, I had to learn more than I ever wanted to know about authentication, but I am fairly pleased with how the system works now. What I like the most is its relative simplicity (to the extend that authentication can be). My hope is that by avoiding smart solutions, the chances of the system actually working well and not presenting us with difficult edge cases every day are pretty good. I will report back if I was too naive.

© Dejan Glozic, 2014

18 thoughts on “Sharing micro-service authentication using Nginx, Passport and Redis

Add yours

  1. Great post I wonder if we can convince someone in the lab to build a DataPower module that does OAuth and LDAP translation module to maintain SSO between WAS and Node.

    1. Thanks. I don’t know about WAS, but I have written a Java micro-service that uses Jedis to participate in this system and uses JEE filter to implement OAuth2 dance. A mixed Java/Node micro-service system can definitely be made to work that way.

  2. I’m going to set up a new app and have to solve the same issues with authentication. First of all I would like to thank you for your post, couse it was a good starting point for me.

    I guess there’re several ways in Java to change the name of the cookie that holds the session id, anyway, I found one of them:
    http://www.digizol.com/2010/10/jsessionid-tomcat-cookie-change- default.html

    As far as I know, there is no native session handling in go, but in any case you can access cookies with the “net/http” package. So you can grep the session id and read the data from Redis.

    Are you gained more experience in the meantime that can be helpfull for me?

    Sorry for my wording, I’m a german guy 😉

    1. Alex,

      Well, the default session is ‘jsessionid’ and you can go a long way using the default. Some systems actually check for the presence of it in order to support session affinity, so I would suggest to try to make as much progress without changing it.

      As long as you can access cookies, you can use the session cookie as a key to Redis as you implied, so I don’t see a reason this cannot work with Go (although, who does not have session support these days :-).

      The system defined in the article has held up pretty well so far, we continue to use it without problems.

  3. Looking at the Google Identity Toolkit documentation, if I understood it correctly, they discussed this concept of a second cookie. If there’s a second cookie that all the microservices look for and all the microservices have the same approach to hashing/validating (maybe using the Authorization providers token as part of the hash) in each of the microservices that can with any language) then each microservice can use the second cookie as the key lookup in the Redis Cluster (and if the microservice container cannot configure a different cookie then just have two cookies).

    Thoughts?

      1. JWT is normally used for bearer tokens when OAuth2 authentication is used. However, I prefer not to put the bearer tokens on the client for security reasons. As described in the article, I would make ajax calls to the server side where bearer tokens (alongside user object) could be retrieved based on the session cookie. I prefer this approach because the session cookie itself is normally signed and encrypted, but does not contain the bearer token itself, and this bearer token is normally needed to make authenticated downstream API calls.

  4. Thanks for this useful post!
    I’m try to implement something similar, inspired by this, and I’m running into issues with sharing the session-cookie between the different node apps.
    I have two apps behind proxies with reds for shared serialized user profiles based on session ID. I successfully got to a state that I sign-in in one app, and when I refresh in the second app I see that I’m also signed in there – great success!
    For some reason, though, once I refresh the page (of any of the two apps), I get a new session ID from the server (I see this in the set-cookie header response, followed by a new connect.sid cookie), and naturally I lose my signed-in status. This happens even though the request went out with the correct connect.sid value on the way out…
    Any idea what’s going on?

    1. This is surprising. Your cookie is managed by express session middleware, and should have the same value as long as the session lasts (this cookie should be marked as ‘session’ when you look at it in Chrome resources). I am not sure why express lost the cookie and had to create a new one on refreshing the browser page. Did you play with session options around the cookie?

      1. Where should this “session” marking appear? I have just one cookie on this domain, named “connect.sid”, and it says “Session” in the “Expires” column of the Chrome resources. Is this what you mean?

        Here are some snippets of how I set up the middleware in both apps:

        app.use(session({
        secret: ‘some secret’,
        resave: false,
        saveUninitialized: true,
        store: new RedisStore({ host: REDIS_HOST, port: REDIS_PORT }),
        cookie: { secure: false, domain: ‘.mydomain.com’ }
        }));
        app.use(passport.initialize());
        app.use(passport.session());

        One difference from what you described is that my proxy serves different apps on different sub-domains of “mydomain.com” (that’s the reason I explicitly set the cookie domain option to the top level domain), as opposed to different URI’s under the same domain, but I don’t see any reason this should cause express to lose the session upon receiving new requests…

        An interesting observation, unrelated to passport:

        If I refresh one app multiple times, I get the same session ID every time, but once I switch apps, I lose the session ID… Maybe this can give you an idea about what I’m doing wrong?

      2. Yeah, the architecture in my example has NGINX proxy in front of the two apps exactly so that there is only one domain, and sessions cookies are forwarded down to all locations that are served via ‘proxy_pass’ directive. It looks like you have two domains and while you are registering the cookie for a subdomain, I wander if that actually works. That’s about the only difference I can notice.

      3. I’m using nginx reverse proxy too, just proxying to subdomains instead of sub-URI’s. I’m pretty sure this part works OK, because I do get the first refresh right, and I see in the node console that I got the cookie I expected to get from the second app on the first refresh, but then express decided to override it.

        I found this StackOverflow thead – http://stackoverflow.com/questions/13617471/shared-sessions-between-node-apps .
        It looks like express ties the session cookie to the full domain, without respecting the cookie domain option, which explains why it overrides the session cookie when I refresh from the second app.

        Assuming I’m not going to change my subdomains layout, I think I have two options:
        1. Patching express to behave as I want it, w.r.t. to domain-session relations. Do you know if this can be done with a middleware, and if such a middleware exists?
        2. Don’t rely on the session at all – use something else instead, that I can control, and share between the apps. Not sure how to do that, but that can make it simpler to extend the approach to non-node apps as well.

  5. I forgot to ask which version of Node (or better, of session middleware) – the SO link you provided above does mention that this seems to have been fixed in newer ‘Node’ whatever that means. Maybe try with 4.2.x stable Node version and see if that changes anything.

    1. I used node 4.0.0 as well as 5.1.1, both with express 4.13.3 and express-session 1.12.latest.

      Here’s a “solution” I’m so far OK with:

      – Only one of the apps takes care of authentication (which means only that app needs passport, and that’s the only place I need to configure passport). I call that app “identity service”.

      – The identity service exposes a simple API for getting logged in user data. This API is protected, such that only other backends can access it. Here is its handler:

      app.get(‘/profile’, function (req, res) {
      if (req.user) {
      res.send(req.user);
      } else {
      res.status(401).send(‘no logged in user’);
      }
      });

      – Only the identity service is configured with express-session middleware, with the cookie domain set to the top-level domain, so the session cookie is shared between all apps, but not overridden when other apps are used, because they are not configured with session middleware.

      – To share login info, a consumer app queries the identity services /profile API, using the session cookie if it exists. Here’s the middleware(s) I used to accomplish that:

      app.use(cookieParser());
      app.use(function (req, res, next) {
      if (req.cookies[‘connect.sid’]) {
      // already got shared session configured – make it more accessible
      req.mySession = req.cookies[‘connect.sid’];
      }
      next();
      });
      app.use(function (req, res, next) {
      if (req.mySession) {
      request.get({
      url: `${IDENT_SERVICE}/profile`,
      headers: { ‘Cookie’: `connect.sid=${req.mySession}` }
      }, function (err, resp, body) {
      if (resp && resp.statusCode == 200 && body) {
      req.user = JSON.parse(body);
      req.isAuthenticated = function() { return true; };
      } else {
      console.log(‘WARNING: could not get profile from identity service: ‘ + err);
      req.isAuthenticated = function() { return false; };
      }
      next();
      });
      } else {
      console.log(‘no shared session cookie’);
      req.isAuthenticated = function() { return false; };
      next();
      }
      });

Leave a reply to Dejan Glozic Cancel reply

Blog at WordPress.com.

Up ↑