Centralized authentication with a microservices gateway

Implement authentication and authorization using FusionAuth for a gateway API application that routes to two different microservices.

Authors

Published: September 15, 2020


In this article, we’re going to implement authentication and authorization for a gateway API application that routes to two different microservices. FusionAuth will be the auth server for the gateway.

An API gateway with microservices is a common pattern for enterprise architectures. In this post, we’ll pretend we’re setting this up for an eCommerce enterprise. Our gateway application is a central API that will control access to a product catalog service and a product inventory service. We’ll allow customers to access public endpoints but require authentication for some of the product inventory endpoints.

For this article, we’re going to need a running FusionAuth instance and three simple Node/Express applications. You can download the example project for this article and customize your FusionAuth configuration accordingly, or you can follow along conceptually.

If you want to follow along, it will be very helpful to go through the 5-Minute Setup Guide first, as that will set up the needed users and roles in FusionAuth.

We’re going to have four applications running, on the following ports:

  • FusionAuth: 9011
  • Gateway Application: 3000
  • Product Catalog Service: 3001
  • Product Inventory Service: 3002

We’re also going to be dealing with authentication and authorization quite a bit, so let’s briefly clarify what we mean by these terms.

Authentication and authorization

Authentication is the verification of a particular user. When a user is logged in, they’re saying to the application, “Hey, it’s the real John Doe, let me in.” The application validates their credentials, and they have access.

In our API gateway, we’re going to use FusionAuth, based on the 5-Minute Setup Guide as mentioned above. We’ll talk about specific details when we set up our API gateway application later.

Authorization is the process whereby we verify that a particular user (e.g. John Doe) has access to certain parts of our system (e.g. product inventory). In our eCommerce ecosystem, we’re going to require authorization for the product inventory API, but not for the basic product APIs, since we want everyone to access the latter. For the product inventory route, we’ll allow users with the “admin” role access.

Product Catalog service

We’ll return to authentication and authorization soon, but let’s start building our applications! We’re going to start with the services and work our way towards the gateway application.

Setting up the nodejs Product Catalog

Before you get going, you’ll need node installed (code tested with version 14). If you don’t have it installed, grab it from the node website.

Clone the project onto your local computer and cd into the directory.

You’ll notice three folders corresponding to our applications: gateway, product-catalog, and product-inventory. Go ahead and cd into the product-catalog application and do the following:

  • Run npm install to install the dependencies.
  • Start the application by running npm start. It should be running on port 3001, which is defined in bin/www.

Now that your application is up and running, you should be able to send a request to it and get a response. Run this curl command in a separate terminal window and you should get a successful response with an empty list of products (products: []):

curl http://localhost:3001/products

Let’s open up the hood on this and check out the routes/index.js file that gave us our /products route.

const express = require('express');
const router = express.Router();

router.get('/products', function(req, res, next) {
  res.json('products: []')
});

router.get('/products/:id', function(req, res, next) {
  res.json(`product: ${req.params.id}`)
});

module.exports = router;

We’ve created two basic routes, /products and /products/:id, so we can get a list of products and a single product. Obviously, for a real microservice, these routes would request the product information from a datastore. For now, the former is returning an empty array [] and the latter returns the product id requested.

Try modifying your curl request to add a product ID, and notice that the response will indicate the specific ID you requested.

The Product Catalog service is ready to go!

Product Inventory service

Open up another terminal window and enter the product-inventory folder. Run npm install to install needed dependencies.

Here’s what our Product Inventory service looks like (in routes/index.js):

const express = require('express');
const router = express.Router();

router.get('/branches/:id/products', function(req, res, next) {
  const roles = req.headers.roles;
  if (roles && roles.includes('admin')) {
    res.json(`Products for branch #${req.params.id}`);
  } else {
    res.redirect(403, 'http://localhost:3000');
    return;
  }
});

module.exports = router;

In this service, we’ve just got one route, to get products for a specific store or branch. Notice, however, that we’re allowing access (or denying it) based on the inclusion of an admin role in the roles header. The API gateway application will be responsible for passing this data to our Product Inventory service.

If you were to start the service (go ahead and do so with npm start) and send a request to http://localhost:3002/branches/1/products, you should receive a 403. You can simulate a successful response by adding a roles header with a value of admin:

curl -i -H "Accept: application/json" -H "Content-Type: application/json" -H "roles: admin" http://localhost:3002/branches/1/products

We’ve got our Product Inventory service up and running, with authorization to ensure that only admins can access a list of products for a branch. Doing an authorization check at the service level allows us to granularly implement authorization.

API-level authentication for your microservices

A quick note on API-level authentication. We’re implementing centralized authentication through the API gateway. Only that gateway should have access to these microservices. You could do this at the network level, or with some form of API-level authentication, like an API Key. It’s a little much for us to cover in this article, but it’s definitely something you’ll want to implement before launching your microservices into production.

The gateway application

Now that we’ve got our Product Catalog and Product Inventory services running on ports 3001 and 3002, we’re ready to tackle the API gateway application.

Before we dive into the code, let’s briefly discuss why we’re creating our own gateway as opposed to using something like Apigee or Amazon’s API Gateway. We certainly could go that route, but in creating our own gateway, we have ultimate flexibility. There’s an additional benefit of gaining an understanding of exactly what a gateway application is doing.

Our gateway application is straightforward and lightweight. It primarily functions as a router, directing requests to the appropriate service. But, because it’s a gateway to our distributed services, it’s the perfect spot for centralized user-level auth checks.

Centralized authentication

Centralizing authentication is a common pattern because authentication is primarily just a check to ensure the following:

  • The user is logged in
  • The user is who they say they are

In the context of separate services in an eCommerce domain, we want to have this centralized authentication so one check in the gateway gives a user access to the services, assuming their credentials check out. We’ll use FusionAuth for authenticating each of our routes before we forward them over to the right service.

FusionAuth Setup

Open up yet another terminal window, enter the gateway director, and run npm install.

Head over to the 5-Minute Setup Guide for FusionAuth. During setup, the application that you configure will be linked to our gateway application, so you can name it “Gateway”. Note that while FusionAuth supports multi-tenant configurations, here you’re setting everything up in the default tenant.

Update the routes/index.js in the gateway directory with your FusionAuth application’s client ID and secret and views/index.pug with your client ID. Then start the application by running npm start.

This application will be our gateway (hence the name) to our services, so at this point, we should access our services only through the gateway application. Run the curl command for /products, but do so on port 3000, which will hit our gateway application.

curl http://localhost:3000/products

Now we’re funneling traffic through our gateway application and forwarding it over to the Product Catalog service. You can verify this by opening the terminal window for the running Product Catalog service and checking the logs. You should see the request we just sent hitting that server:

GET /products 200

Routes

Let’s go through the gateway application’s routes/index.js file step by step. We start by requiring necessary files and setting up a FusionAuthClient. We also include a handy authentication middleware, which we’ll use on our routes.

// ...
const request = require('request');
const express = require('express');
const router = express.Router();
const {FusionAuthClient} = require('@fusionauth/typescript-client');
const clientId = [YOUR_CLIENT_ID];
const clientSecret = [YOUR_CLIENT_SECRET];
const client = new FusionAuthClient('noapikeyneeded', 'http://localhost:9011');
const checkAuthentication = require('../middleware');
// ...

Our gateway application, along with our services, are based on the fusionauth-example-node project, which gives us a basic UI (at the root) for interacting with FusionAuth. We also have a route for our OAuth redirect:

// ...
/* GET home page. */
router.get('/', function (req, res, next) {
  const stateValue = Math.random().toString(36).substring(2,15) + Math.random().toString(36).substring(2,15) + Math.random().toString(36).substring(2,15) + Math.random().toString(36).substring(2,15) + Math.random().toString(36).substring(2,15) + Math.random().toString(36).substring(2,15);
  req.session.stateValue = stateValue
  res.render('index', {user: req.session.user, stateValue: stateValue, title: 'FusionAuth Example'});
});

/* OAuth return from FusionAuth */
router.get('/oauth-redirect', function (req, res, next) {
  // This code stores the user in a server-side session
  const stateFromServer = req.query.state;
  if (stateFromServer !== req.session.stateValue) {
    console.log("State doesn't match. uh-oh.");
    console.log("Saw: "+stateFromServer+ ", but expected: "+req.session.stateValue);
    res.redirect(302, '/');
    return;
  }
  client.exchangeOAuthCodeForAccessToken(req.query.code,
                                         clientId,
                                         clientSecret,
                                         'http://localhost:3000/oauth-redirect')
      .then((response) => {
        console.log(response.response.access_token);
        return client.retrieveUserUsingJWT(response.response.access_token);
      })
      .then((response) => {
        req.session.user = response.response.user;
      })
      .then((response) => {
        res.redirect(302, '/');
      }).catch((err) => {console.log("in error"); console.error(JSON.stringify(err));});
});
// ...

At this point, with your gateway server running, you should be able to go to http://localhost:3000 in your browser and see this:

The login screen for the gateway server.

If we’ve got our setup working right, you should be able to click “Login”. Login to FusionAuth with the user you set up previously. You’ll see a “Hello [your name]” message.

This UI wouldn’t be necessary in a typical usage of an API gateway application, but it’s an easy, visual way for us to demonstrate a successful OAuth login.

Looking at the remainder of our routes/index.js file, we’ve got routes that will forward to our Product Catalog and Product Inventory services.

// ...
/* PRODUCT CATALOG ROUTES */
const productUrl = 'http://localhost:3001';

router.get('/products', function(req, res, next) {
  request(`${productUrl}/products`).pipe(res);
});

router.get('/products/:id', function(req, res, next) {
  request(`${productUrl}/products/${req.params.id}`).pipe(res);
});
// ...

These routes are completely public and don’t require endpoint-level authentication, as we want customers and employees alike to be able to view products. We want them to buy something, right? The routes forward over to the product catalog service on port 3001. However, if there was ever part of the catalog that required protection, we’ve built the infrastructure.

Now for the product inventory route, to retrieve products at a branch:

// ...
/* PRODUCT INVENTORY ROUTES */
router.get('/branches/:id/products', checkAuthentication, function(req, res, next) {
  const user = req.session.user;
  const options = {
    url: `http://localhost:3002/branches/${req.params.id}/products`,
    headers: { roles: user.registrations[0].roles }
  };
  request(options).pipe(res);
});
// ...

This route forwards over to our Product Inventory service. The checkAuthentication middleware can be used in any route where we want to check that the user is authenticated; it sends the user back to the root URL if they aren’t logged in.

Using your browser (to leverage our OAuth credentials present because we signed in), go to http://localhost:3000/branches/1/products. This works because the user you set up in the 5-Minute Setup Guide has an admin role in FusionAuth, and we’re passing that role as a header to the Product Inventory service.

Conclusion

We’ve covered a lot in this article. Our goal was to create a basic eCommerce ecosystem with an API gateway application and two microservices, a Product Catalog service and a Product Inventory service.

For our API gateway, we leverage FusionAuth for centralized authentication and authorization. We then created forwarding routes to our two services, with the ability to pass roles to the services. And in the services themselves, we implemented the ability to allow or deny requests at the endpoint level based on the user’s role.

If you wanted to explore this further, you could:

  • Add more users and roles; for example, a branch-manager role which can view the products for a certain branch.
  • Build out products and branches tables and have the microservices return dynamic data.
  • Build an application to display available products.

Happy coding!

More on javascript

Subscribe to The FusionAuth Newsletter

A newsletter for developers covering techniques, technical guides, and the latest product innovations coming from FusionAuth.

Just dev stuff. No junk.