30 Days of Zumo.v2 (Azure Mobile Apps): Day 10 – Middleware

I’ve covered the basics of table controllers in the last two posts, covering both per-table configuration and per-operation configuration. Today I’m going to talk about extensibility and middleware. Middleware is a fairly basic concept – it provides a capability of inserting custom code into the HTTP request/response pipeline. Express has a whole section on middleware and pretty much all Express modules are written as middleware. Here is a template:

function myMiddleware(request, response, next) {
    // Do something with request

    // Return something with response or next

    next();  // Call the next middleware
}

app.use(myMiddleware);

Azure Mobile Apps is written as a set of middleware, but it’s been constructed to be interchangable. Let’s look at a sample service. Here is the app.js:

var app = require('express')(),
    mobile = require('azure-mobile-apps')();

app.use(function (req, res, next) {
  console.log('express.use - azureMobile = ', typeof req.azureMobile !== 'undefined');
  return next();
});

zumo.use(function (req, res, next) {
  console.log('zumo.use - azureMobile = ', typeof req.azureMobile !== 'undefined');
  return next();
});

zumo.tables.import('./tables');
zumo.tables.initialize().then(() => {
  app.use(zumo);
  app.listen(3000);
});

And my sample table:

var table = require('azure-mobile-apps').table();

table.use(function (req, res, next) {
  console.log('table.use: azureMobile = ', typeof req.azureMobile !== 'undefined');
  return next();
}, table.operation);

table.read(function (context) {
  console.log('tableop: azureMobile = ', typeof context.req.azureMobile !== 'undefined');
  return context.execute();
});

module.exports = table;

If you run this locally and do a GET of the /tables/todoitem endpoint, you will see the following console output:

express.use - azureMobile = false
zumo.use - azureMobile = true
table.use: azureMobile = true
tableop: azureMobile = true

This gives you a very clear indication of what is available and the most appropriate place to add code. As an example, let’s say the majority of your code depends on the email address being available. For example, you are doing the personal table. You may want to add the email address to the user object. You can easily do this with a zumo.use middleware. Add a file authMiddleware.js with the following contents:

var authCache = {};

function authMiddleware(request, response, next) {
  if (typeof request.azureMobile.user.id !== 'undefined') {
    if (typeof authCache[request.azureMobile.user.id] !== 'undefined') {
      request.azureMobile.user.emailaddress = authCache[request.azureMobile.user.id];
      next();
    }
    request.azureMobile.user.getIdentity().then(function (userInfo) {
      if (typeof userInfo.aad.claims.emailaddress !== 'undefined') {
        authCache[request.azureMobile.user.id] = userInfo.aad.claims.emailaddress;
        request.azureMobile.user.emailaddress = authCache[request.azureMobile.user.id];
      }
      next();
    });
  } else {
    next();
  }
}

module.exports = authMiddleware;

This code does some primitive caching of the result from getIdentity(), but otherwise pulls out the email address just like I did in the Personal Table article. Instead of just using it, though, it puts it in the context for use later.

What’s this request.azureMobile? It’s the context and has all the same properties that I discussed in the table operations article.

Now that I have my auth middleware written, I can use this to adjust my original personal table. Firstly, I need to link it into the app.js:

var express = require('express'),
    serveStatic = require('serve-static'),
    azureMobileApps = require('azure-mobile-apps'),
    authMiddleware = require('./authMiddleware');

// Set up a standard Express app
var app = express();
var mobileApp = azureMobileApps({
    homePage: true,
    swagger: true
});

mobileApp.use(authMiddleware);
mobileApp.tables.import('./tables');
mobileApp.api.import('./api');

I can now adjust my table controller to make it much more simple:

var azureMobileApps = require('azure-mobile-apps');

// Create a new table definition
var table = azureMobileApps.table();

// Require authentication
table.access = 'authenticated';

// CREATE operation
table.insert(function (context) {
  context.item.userId = context.user.emailaddress;
  return context.execute();
});

// READ operation
table.read(function (context) {
  context.query.where({ userId: context.user.emailaddress });
  return context.execute();
});

// UPDATE operation
table.update(function (context) {
  context.query.where({ userId: context.user.emailaddress });
  context.item.userId = context.user.emailaddress;
  return context.execute();
});

// DELETE operation
table.delete(function (context) {
  context.query.where({ userId: context.user.emailaddress });
  return context.execute();
});

module.exports = table;

Note that I’m not using getIdentity() any more. For reference, here is what my read method used to look like:

table.read(function (context) {
    return context.user.getIdentity().then(function (userInfo) {
        context.query.where({ userId: userInfo.aad.claims.emailaddress });
        return context.execute();
    });
});

I’ve isolated my getIdentity() call into the middleware so that the email address is available everywhere.

Doing Some Authorization

You can also do some basic authorization using the same technique. For example (assuming the list is small), you can configure Azure AD to provide the list of groups in a claim. Here is how to do it. To configure AAD to produce the groups as claims:

  1. Log onto the Azure Portal and go to your Azure AD Configuration.
  2. Click on Manage Manifest (at the bottom of the page), then Download Manifest – this will download a JSON file.
  3. Edit the downloaded file with a JSON editor (I use Atom)
  4. Set the “groupMembershipClaims” value to “SecurityGroup”

    day-10-p1

  5. Save the file and upload it by using Manage Manifest -> Upload Manifest

  6. Click on the APPLICATIONS tab
  7. Click on your Web Application (not the Native Client)
  8. Scroll to the bottom under “permissions to other applications”
  9. Click on the Delegated Permissions and check the box next to “Read directory data”

    day-10-p2

  10. Click on Save

Now that you have configured the application to return groups, you need a group to return.

  1. Click on the back arrow to return to the top level of your directory
  2. Click on the GROUPS tab
  3. Click on ADD GROUP in the bottom banner
  4. Enter a name for the group, set the Group Type to Security, then click on the Tick.
  5. Add a member using the ADD MEMBER button in the bottom banner
  6. Click on the user you wish to add, then click on the + in a circle. Repeat for multiple users.
  7. Finally, click on the tick.
  8. Click on PROPERTIES and make a note of the Object ID

The Object ID is the GUID of the group and is what will show up in the groups list. After doing all this and re-running your client, the getIdentity() method will return the group information in the returned value, but it’s not in the most accessible manner:

day-10-p3

Note the item highlighted in user_claims. This is the following, when properly laid out:

"user_claims": [
    // Some other stuff
    { "typ": "groups", "val": "0d270bc3-6431-4d9c-8a05-ba7fa0b5463c" },
    { "typ": "groups", "val": "92d92697-1242-4d38-9c1d-00f3ea0d0640" },
    // More stuff
],

We can convert this into something usable using the following (this is the authMiddleware.js file):

var authCache = {};

/**
 * Reducer method for converting groups into an array
 * @param {string[]} target the accumulator for the array
 * @param {object} claim the current claim being processed
 * @returns {string[]} the accumulator
 */
function groupReducer(target, claim) {
    if (claim.typ === 'groups')
        target.push(claim.val);
    return target;
}
/**
 * Middleware for adding the email address and security groups to the
 * request.azureMobile.user object.
 * @param {express.Request} request the Express request
 * @param {express.Response} response the Express response
 * @param {function} next the next piece of middleware
 * @returns {any} the result of the next middleware
 */
function authMiddleware(request, response, next) {
    if (typeof request.azureMobile.user === 'undefined')
        return next();
    if (typeof request.azureMobile.user.id === 'undefined')
        return next();

    if (typeof authCache[request.azureMobile.user.id] === 'undefined') {
        request.azureMobile.user.getIdentity().then(function (userInfo) {
            var groups = userInfo.aad.user_claims.reduce(groupReducer, []);
            console.log('userInfo.aad.claims = ', userInfo.aad.claims);
            var email = userInfo.aad.claims.emailaddress || userInfo.aad.claims.upn;
            authCache[request.azureMobile.user.id] = {
                emailaddress: email,
                groups: groups
            };

            request.azureMobile.user.emailaddress = authCache[request.azureMobile.user.id].emailaddress;
            request.azureMobile.user.groups = authCache[request.azureMobile.user.id].groups;
            next();
        });
    } else {
        request.azureMobile.user.emailaddress = authCache[request.azureMobile.user.id].emailaddress;
        request.azureMobile.user.groups = authCache[request.azureMobile.user.id].groups;
        next();
    }
}

module.exports = authMiddleware;

Lines 9-12 are a reducer function for converting the object-version of the claims into an array. This is used in line 32 to convert the user_claims into an array that I can actually use. I’ve done the same thing as I did in the case of the email address. Now I can use that in my table controller to implement group-based authorization. For that, I need the object ID of the group I am interested in. For the purposes of this test, I’ve got a group called Administrators with an Object ID of 92d92697-1242-4d38-9c1d-00f3ea0d0640 – this was obtained from the Azure AD configuration screens in the Azure Portal. Authorization is easily done within the table controller with some code:

// DELETE operation
table.delete(function (context) {
    console.log('DELETE: context.user = ', context.user);
    // Authorization - if Administrators is not in the group list, don't allow deletion
    if (context.user.groups.indexOf('92d92697-1242-4d38-9c1d-00f3ea0d0640') < 0) {
        console.log('user is not a member of Administrators');
        context.res.status(401).send('Only Administrators can do this');
        return;
    }

    console.log('user is a member of Administrators');
    context.query.where({ userId: context.user.emailaddress });
    return context.execute();
});

If you log in as a member of the Administrators group, then you will be able to delete. You will see an error message for others.

Of course, this isn’t really convenient. I mean – who wants to code up the authorization for each group? Instead, let’s show off a little middleware for the operation. First the middleware, which I’ve put at the top of the file, but could just as easily be in its own file:

function isAdministrator(request, response, next) {
    if (request.azureMobile.user.groups.indexOf('92d92697-1242-4d38-9c1d-00f3ea0d0640') < 0) {
        response.status(401).send('Only Administrators can do this');
        return;
    }
    next();
}

I do the same check as before in this middleware. If the user is not a member of the required group, then the same response (401 Unauthorized) with a custom message is done. You may remember that I can integrate middleware at the table level using this:

table.use(isAdministrator, table.operation);

I can also do this on a per-operation basis. The table.use is just a shortcut for the individual operations. So:

table.delete.use(isAdministrator, table.operation);

Putting it together, I can code my delete script as follows:

// DELETE operation
function isAdministrator(request, response, next) {
    if (request.azureMobile.user.groups.indexOf('92d92697-1242-4d38-9c1d-00f3ea0d0640') < 0) {
        response.status(401).send('Only Administrators can do this');
        return;
    }
    next();
}

table.delete.use(isAdministrator, table.operation);
table.delete(function (context) {
    context.query.where({ userId: context.user.emailaddress });
    return context.execute();
});

Next Steps

Authorization is a major milestone for most real applications. However, we are still operating within a single table without any interactivity between tables. I’d like to adjust that with my next article – going both multi-table and providing linkage between the tables.

Until then, I’ve uploaded my code to my GitHub Repository.