30 Days of Zumo.v2 (Azure Mobile Apps): Day 20 – Custom API

Thus far, I’ve covered authentication and table controllers in both the ASP.NET world and the Node.js world. I’ve got two clients – an Apache Cordova one and a Universal Windows one – and I’ve got two servers – a Node.js one and an ASP.NET one. I’ve looked at what it takes to bring in existing SQL tables. It’s time to move on.

Not every thing that you want to do can fit into a nice table controller. Sometimes, you need to do something different. Let’s take, for example, the application key. When we had Mobile Services, the API had an application key. It was meant to secure “the API” – in other words, only your applications could access the API. Others would need to know the application key to get into the API. This is insanely insecure and easily defeated. Anyone downloading your app and installing a MITM sniffer will be able to figure out application key. It’s in a header, after all. Then, all the attacked needed to do is use the REST endpoint with your application key and your API is as open as before. It’s trivial – which is why pretty much no-one who understands security at all will produce an API with an application key any more. It doesn’t buy you anything.

How about a secure approach? When you have a mobile app out there, you have to register it with the various app stores – the Google App Store, Apple iTunes or the Microsoft App Store. The only apps that can use the push notification systems (GCM for Google, APNS for Apple and WNS for Microsoft)re registered apps. So, use a Custom API to request a token. The token is sent via the push notification scheme for the device and is unique to the session. Add that token to the headers and then your API looks for that. This technique is really secure. But it relies on your application being able to receive push notifications and needs your application registered with the stores. In addition, push notifications sometimes take time. Would you want the first experience of your app to be a five minute delay for “registration”?

There is a middle ground. Use a Custom API to create a per-device token. The token can be used for only a certain amount of time before it expires, thus limiting the exposure. Each time the token expires, it must be re-acquired from the server. It isn’t secure – your API can still get hijacked. However, it makes the process much more costly and that, at the end, is probably enough.

Version 1: The Node.js Easy API

You can use the Easy API if you meet all the following criteria:

  • You have created the server with the Node.js Quickstart
  • You have not modified the main application code

If you followed Day 1, then this doesn’t apply to you. Easy Tables and Easy API are only available with a specially configured server that is deployed when you use the Quickstart deployment. Any other deployment pretty much doesn’t work.

Here is how to use Easy API after creating the server. Firstly, go to the Settings menu for your App Service and click on the Easy APIs option. (If you do not have access to Easy APIs, then this will also tell you – in which case, use Version 2 instead). Click on the + Add button and fill in the form:

day-20-p1

I’m only going to access this API via GET, so I’ve disabled the others. For the GET API, I’m enabling anonymous access. I can also select authenticated access. Easy APIs integrates with your regular mobile authentication – the same authentication token used for table access.

Once the API is created, click on the API and then click on Edit script. This will open Visual Studio Online. This will allow you to edit the script online. A blueprint has been implemented for me:

module.exports = {
    //"get": function (req, res, next) {
    //}
}

Not much there – next is my code. The version I’m going to use is this:

var md5 = require('md5');
var jwt = require('jsonwebtoken');

module.exports = {
    "get": function (req, res, next) {
        var d = new Date();
        var now = d.getUTCFullYear() + '-' + (d.getUTCMonth() + 1) + '-' + d.getUTCDate();
        console.info('NOW = ', now);
        var installID = req.get('X-INSTALLATION-ID');
        console.info('INSTALLID = ', installID);
        
        if (typeof installID === 'undefined') {
            console.info('NO INSTALLID FOUND');
            res.status(400).send({ error: "Invalid Installation ID" });
            return;
        }
        
        var subject = now + installID;
        var token = md5(subject);
        console.info('TOKEN = ', token);
        
        var payload = {
            token: token
        };
        
        var options = {
            expiresIn: '4h',
            audience: installID,
            issuer: process.env.WEBSITE_SITE_NAME || 'unk',
            subject: subject
        };
        
        var signedJwt = jwt.sign(payload, installID, options);
        res.status(200).send({ jwt: signedJwt });
    }
};

This won’t work yet – that’s because the md5 and jsonwebtoken modules are not yet available. I can install these through Kudu. Go back to the Azure Portal, select your App Service, then Tools, followed by Kudu. Click on the PowerShell version of the Debug console. change directory into site/wwwroot, then type the following into the console:

npm install --save md5 jsonwebtoken

Did you know You can download your site for backup at any time from here. Just click on the Download icon next to the wwwroot folder.

Version 2: The Node.js Custom API

If you aren’t a candidate for the Easy API, then you can still use Custom APIs and the same code. However, you need to add Custom API’s into your code. Place the code below into the api/createKey.js file. Add the npm packages to the package.json file.

In the Easy API version, there is also a createKey.json file. In the Custom API version, the authentication information is placed in the Javascript file, like this:

var md5 = require('md5');
var jwt = require('jsonwebtoken');

var api = {
    "get": function (req, res, next) {
        var d = new Date();
        var now = d.getUTCFullYear() + '-' + (d.getUTCMonth() + 1) + '-' + d.getUTCDate();
        console.info('NOW = ', now);
        var installID = req.get('X-INSTALLATION-ID');
        console.info('INSTALLID = ', installID);
        
        if (typeof installID === 'undefined') {
            console.info('NO INSTALLID FOUND');
            res.status(400).send({ error: "Invalid Installation ID" });
            return;
        }
        
        var subject = now + installID;
        var token = md5(subject);
        console.info('TOKEN = ', token);
        
        var payload = {
            token: token
        };
        
        var options = {
            expiresIn: '4h',
            audience: installID,
            issuer: process.env.WEBSITE_SITE_NAME || 'unk',
            subject: subject
        };
        
        var signedJwt = jwt.sign(payload, installID, options);
        res.status(200).send({ jwt: signedJwt });
    }
};

api.get.access = 'anonymous';

module.exports = api;

In addition, the custom API system must be loaded in the main server.js file:

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

// Set up a standard Express app
var webApp = express();

// Set up the Azure Mobile Apps SDK
var mobileApp = azureMobileApps();
mobileApp.use(authMiddleware);
mobileApp.tables.import('./tables');
mobileApp.api.import('./api');

// Create the public app area
webApp.use(serveStatic('public'));

// Initialize the Azure Mobile Apps, then start listening
mobileApp.tables.initialize().then(function () {
    webApp.use(mobileApp);
    webApp.listen(process.env.PORT || 3000);
});

Once published (or, if you are doing continuous deployment, just checking the code into the relevant branch of your source-code control system), this will operate exactly the same as the Easy API version.

Version 3: The Node.js Custom Middleware

Both the Easy API and Custom API use the same underlying code to do the implementation. You have access to the whole Azure Mobile Apps environment (more on that in a later blog post). However, you are limited in the routes that you can use. You have four verbs (so no HEAD, for example) and very little in the way of variable routes. Sometimes, you want to take control of the routes and verbs. You maybe want to produce a composed API that has a two level Id structure or you are really into doing REST “properly” (which isn’t much, but there are some accepted norms). There are many constraints to the Easy API / Custom API route in Node.js – most notably that the routes are relatively simple. Fortunately, the Node.js SDK uses ExpressJS underneath, so you can just spin up a Router and do the same thing. I’ve placed the following code in the server.js file:

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

// Set up a standard Express app
var webApp = express();

// Set up the Azure Mobile Apps SDK
var mobileApp = azureMobileApps();
mobileApp.use(authMiddleware);
mobileApp.tables.import('./tables');
mobileApp.api.import('./api');

// Create the public app area
webApp.use(serveStatic('public'));

// Initialize the Azure Mobile Apps, then start listening
mobileApp.tables.initialize().then(function () {
    webApp.use(mobileApp);
    webApp.use('/custom', customRouter);
    webApp.listen(process.env.PORT || 3000);
});

Note that I’m putting the custom middleware after I’ve added the Azure Mobile App to the ExpressJS app. Ordering is important here – if I place it before, then authentication and table controllers will not be available – I might need those later on. The customRouter object must export an express.Router:

var express = require('express');
var jwt = require('jsonwebtoken');
var md5 = require('md5');

var router = express.Router();

router.get('/createKey', function (req, res, next) {
    var d = new Date();
    var now = d.getUTCFullYear() + '-' + (d.getUTCMonth() + 1) + '-' + d.getUTCDate();
    console.info('NOW = ', now);
    var installID = req.get('X-INSTALLATION-ID');
    console.info('INSTALLID = ', installID);

    if (typeof installID === 'undefined') {
        console.info('NO INSTALLID FOUND');
        res.status(400).send({ error: "Invalid Installation ID" });
        return;
    }

    var subject = now + installID;
    var token = md5(subject);
    console.info('TOKEN = ', token);

    var payload = {
        token: token
    };

    var options = {
        expiresIn: '4h',
        audience: installID,
        issuer: process.env.WEBSITE_SITE_NAME || 'unk',
        subject: subject
    };

    var signedJwt = jwt.sign(payload, installID, options);
    res.status(200).send({ jwt: signedJwt });
});

module.exports = router;

The actual code here is identical once you get past the change to an ExpressJS Router – in fact, I can put the algorithm in its own library to make it easier to include. The advantage of this technique is flexibility, but at the expense of complexity. I can easily add any routing scheme and use any verb since I’m just using the ExpressJS SDK. It really depends on your situation as to whether the complexity is worth it. This technique is really good for producing composed APIs where you have really thought out the mechanics of the API (as opposed to Easy API which is really good for a one-off piece of functionality). My advice is to either use Custom Middleware or Custom APIs though – don’t mix and match.

Note that this technique does not put APIs under /api – the Azure Mobile Apps SDK takes this over (which is part of the reason why you shouldn’t mix and match).

Version 4: The ASP.NET Custom API

Finally, let’s talk about ASP.NET implementation. There is already a well-known implementation for APIs in ASP.NET, so just do the same thing! The only difference is some syntactic sugar to wire up the API into the right place and to handle responses in such a way that our application can handle them. To add a custom controller, right-click on the Controllers node and use Add -> Controller… to add a new controller. The Azure Mobile Apps Custom Controller should be right at the top:

day-20-p3

Here is the default scaffolding:

using System.Web.Http;
using Microsoft.Azure.Mobile.Server.Config;

namespace backend.dotnet.Controllers
{
    [MobileAppController]
    public class CreateKeyController : ApiController
    {
        // GET api/CreateKey
        public string Get()
        {
            return "Hello from custom controller!";
        }
    }
}

The important piece here is the [MobileAppController] – this will wire the API controller into the right place and register some handlers so the objects are returned properly. I expanded on this in a similar way to my Node.js example:

using System.Web.Http;
using Microsoft.Azure.Mobile.Server.Config;
using System.Web;
using System.Net;
using System;
using System.Security.Cryptography;
using System.Text;
using System.Diagnostics;
using System.IdentityModel.Tokens;
using System.Collections.Generic;
using Jose;

namespace backend.dotnet.Controllers
{
    [MobileAppController]
    public class CreateKeyController : ApiController
    {
        // GET api/CreateKey
        public Dictionary<string, string> Get()
        {
            var now = DateTime.UtcNow.ToString("yyyy-M-d");
            Debug.WriteLine($"NOW = {now}");
            var installID = HttpContext.Current.Request.Headers["X-INSTALLATION-ID"];
            if (installID == null)
            {
                throw new HttpResponseException(HttpStatusCode.BadRequest);
            }
            Debug.WriteLine($"INSTALLID = {installID}");

            var subject = $"{now}-{installID}";
            var token = createMD5(subject);
            var issuer = Environment.GetEnvironmentVariable("WEBSITE_SITE_NAME");
            if (issuer == null)
            {
                issuer = "unk";
            }
            Debug.WriteLine($"SUBJECT = {subject}");
            Debug.WriteLine($"TOKEN = {token}");

            var expires = ((TimeSpan)(DateTime.UtcNow.AddHours(4) - new DateTime(1970, 1, 1))).TotalMilliseconds;
            var payload = new Dictionary<string, object>()
            {
                { "aud", installID },
                { "iss", issuer },
                { "sub", subject },
                { "exp", expires },
                { "token", token }
            };

            byte[] secretKey = Encoding.ASCII.GetBytes(installID);
            var result = new Dictionary<string, string>()
            {
                { "jwt", JWT.Encode(payload, secretKey, JwsAlgorithm.HS256) }
            };

            return result;
        }

        /// <summary>
        /// Compute an MD5 hash of a string
        /// </summary>
        /// <param name="input">The input string</param>
        /// <returns>The MD5 hash as a string of hex</returns>
        private string createMD5(string input)
        {
            using (MD5 md5 = MD5.Create())
            {
                byte[] ib = Encoding.ASCII.GetBytes(input);
                byte[] ob = md5.ComputeHash(ib);
                StringBuilder sb = new StringBuilder();
                for (int i = 0; i < ob.Length; i++)
                {
                    sb.Append(ob[i].ToString("X2"));
                }
                return sb.ToString();
            }
        }
    }
}

Most of this code is dealing with the C#.NET equivalent of the Node code I posted earlier in the article. I’m using jose-jwt to implement the JWT signing. The algorithm is identical, so you should be able to use the same client code with either a Node or ASP.NET backend. Want it authenticated? Just add an [Authorize] annotation to the method.

Testing the API

In all cases, you should be able to do a Postman request to GET /api/createKey (or /custom/createKey if you are using the Node custom middleware technique) with a header for X-INSTALLATION-ID that in a unique ID (specifically, a GUID):

day-20-p2

If you don’t submit an X-INSTALLATION-ID, then you should get a 400 Bad Request error.

What are Custom APIs good for?

I use this type of custom API commonly to provide additional settings to my clients or to kick off a process. Some examples of simple Custom APIs:

  • Push to a Tag from a client device
  • Get enabled features for a client
  • Get an Azure Storage API Key for uploading files

The possibilities are really open to what you can dream up.

What are Custom APIs not good for?

Custom APIs are not good candidates for offline usage. There are ways you can queue up changes for synchronization when you are back online. In general, these end up being a hacked up version of a table controller – the client inserts a record into the offline table; when it syncs the backend processes the custom API during the insert operation. However, I cringe when writing that. A better idea would be to implement an offline queue mechanism. In any case, custom APIs are not good for an offline sync scenario.

Next Steps

I only covered the various server APIs this time. In the next article, I’ll take a look at calling the custom API from the clients and adjusting the request properties so that special headers can be inserted. After that, I’m going to cover accessing the Azure Mobile Apps data and authentication objects from within your custom API so that you can do some interesting things with data.

Until then, you can check all four implementations at my GitHub Repository.

One thought

  1. Pingback: Azure Weekly: May 23, 2016 | Build Azure

Comments are closed.