Build a Serverless MBaaS with Azure Functions

I’ve been writing for a while about mobile backends as a service, or MBaaS. An MBaaS is just a service endpoint in the cloud that provides mobile-optimized secure access to cloud resources. The intent is that you do not distribute secrets with your mobile app. Once a secret is distributed with your mobile app, it can be re-used by anyone for anything. However, MBaaS doesn’t have to actually provide the resource and in general they don’t. For example, Azure Mobile Apps is an MBaaS solution that provides access to SQL Azure. It isn’t SQL Azure. It translates the SQL Azure tables to something mobile friendly (in this case, a RESTful interface) and provides security in the form of identity authentication and authorization.

The secret to a good MBaaS, then, is to get in the way of the data source as little as possible so that you can really take advantage of the data store capabilities. Some resources already have rich mobile capabilities – Azure Storage, DocumentDb and Azure Search all fall into this category. All the MBaaS has to do is create a suitable security token based on the inbound authentication token and return it to the mobile client. The mobile client will use the service SDK – Storage, DocumentDb or Search – to access the data, renewing the token when needed.

In this article, I’m going to walk through how to create a NoSQL mobile backend by using Azure Functions and DocumentDb. Using Azure Functions as the “security token generator” is a great idea. Most of the transactions will be done directly against the DocumentDb service. The security token generator is only required to gain access to the DocumentDb service. It is thus light-weight and doesn’t need to be always available. You don’t have to worry about scaling the secure token generator. It will naturally scale as your userbase scales, so you pay only when your userbase starts growing. Similarly, you can grow the size and performance of DocumentDb independently of your Functions, ensuring you only pay for the resources you are consuming. You don’t pay for a “man in the middle” type resource whose only job is to translate queries.

The application I am going to produce is my usual TaskList app. I got asked the other day “why is it always a TaskList app?” I liken it to skiing. When you are learning a new technique in skiing, the instructor will always take you back to simpler slopes so that you can learn the technique. This allows you to learn the technique without worrying about other things at the same time. You can then take that technique back to the steeper, more complex slopes later on. The TaskList app is the easy slope. Take the techniques you learn to the more complex apps with confidence later.

Create the Azure Resources

For this app, I’m going to need two resources.

  • A DocumentDb resource provides the data storage for my account.
  • An Azure Function app provides the secure access token.
    • The Azure Function app requires an App Service Plan and a Storage account as well. These will be created for you.

Place these resources in the same resource group. This allows you to manage the resources as a unit, and it’s a great idea whenever you have a set of associated resources. When all the resources have been created, your resource group should look something like this:

Each of these resources is attached to a common base URL, so it has to be unique within the world. The only URL that my mobile client will need is the Azure Function base URL, which is https://azure-function-mbaas.azurewebsites.net. Everything else will be provided via that URL.

There is a certain amount of configuration needed before I can write my Azure Function. I need to link the DocumentDb service to the Azure Function App so that Azure Functions can access it. To do that:

  • Open the blade for the DocumentDb service, then select Keys.
  • Click on the Read-write Keys tab.
  • Copy the PRIMARY CONNECTION STRING.

With the connection string in hand, go back to the resource group and open the blade for the Azure Function App to link the DocumentDb to the Azure Function:

  • Click the Platform features tab, then select Application settings.
  • Scroll down to the connection strings.
  • Enter DocumentDbConnectionString for the Name, and Custom for the type. The value should be set to the connection string value you copied in the DocumentDb blade.
  • Click Save at the top of the blade.
  • Close the Application settings blade.

The connection string will appear as an environment variable called CUSTOMCONNSTR_DocumentDbConnectionString to your code.

I also want to use authentication. In my app, I’m going to use Facebook authentication. Follow the Azure App Service instructions to implement Facebook authentication. It’s fairly straight forward. You can get to the Azure App Service Authentication and Authorization page from the Platform features section where you found the Application settings for the Function App.

Write a Secure Access Token Generator

I want to write an Azure Function that will be called by my mobile client before any work is done by the mobile client on the cloud data. It should:

  • Ensure the DocumentDb database exists.
  • If authenticated, pull the username from the token. If not, the username is ‘anonymous’.
  • Create a new user within DocumentDb if one does not exist.
  • Return a permissions token to the mobile client allowing the user to access the Tasks collection.

I’m writing this in Node, but I could just as easily write this in any language that is supported by both Azure Functions and DocumentDb. That includes Node and C# right now, but will likely be expanded in the future. Since I’m writing this is node, I’m going to need a few NPM packages. For this project, I need:

To install these:

  • Open the Azure Function blade.
  • Click Console in the Platform features tab.
  • Run npm init -y, followed by npm install --save bluebird documentdb jsonwebtoken.

The install will take a short period of time. Once done, close the console. To create the function:

  • Click on + next to the Functions menu in the left hand side.
  • Select Custom function under Get started on your own, then select the HttTrigger-JavaScript template.
  • Name the function documentdb.
  • Select Anonymous authorization level. (You can also select Function to require an API key – something that is not covered here).
  • Once done, click Create.

This will drop me into the Function editor. It’s a highly capable editor with all the things you would expect out of a code editor. Now I am ready to start editing. Here is the code:

const Promise = require('bluebird');
const DocumentDb = require('documentdb');
const jwt = require('jsonwebtoken');

// The settings that we will use for the endpoint
let settings = {
    connectionString: process.env['CUSTOMCONNSTR_DocumentDbConnectionString'],
    database: 'TaskListApp',
    collection: 'Items',
    connectionPolicy: undefined,
    consistencyLevel: 'Session',
    pricingTier: 'S1'
};

// Split the connection string into a host and accountKey.
const re = new RegExp(/^AccountEndpoint=([^;]+);AccountKey=([^;]+);/);
let results = re.exec(settings.connectionString);
if (results !== null) {
    settings.host = results[1];
    settings.accountKey = results[2];
} else {
    throw new Error("Invalid Connection String");
}

// Build a cache of DocumentDb objects - the first object is the client connection
let cache = {
    client: new DocumentDb.DocumentClient(
        settings.host, 
        { masterKey: settings.accountKey },
        settings.connectionPolicy, 
        settings.consistencyLevel),
    databases: {},
    collections: {},
    users: {}
};

const createCollection = Promise.promisify((dbRef, collectionName, callback) => {
    cache.client.createCollection(dbRef._self, { 
        id: collectionName,
        partitionKey: {
            paths: [ "/userid" ]
        }
    }, { 
        offerType: settings.pricingTier 
    }, callback);
});

const createDatabase = Promise.promisify((databaseName, callback) => {
    cache.client.createDatabase({ id: databaseName }, callback);
});

const upsertPermission = Promise.promisify((userLink, body, callback) => {
    cache.client.upsertPermission(userLink, body, callback);
});

const upsertUser = Promise.promisify((dbRef, userObject, callback) => {
    cache.client.upsertUser(dbRef._self, userObject, callback);
});

const findCollectionById = Promise.promisify((dbRef, collectionName, callback) => {
    const querySpec = {
        query: 'SELECT * FROM r WHERE r.id = @id',
        parameters: [ { name: '@id', value: collectionName } ]
    };
    cache.client.queryCollections(dbRef._self, querySpec).toArray((err, results) => {
        if (err) {
            callback(err, null);
        } else {
            callback(null, (results.length === 0) ? null : results[0]);
        }
    });
});

const findDatabaseById = Promise.promisify((databaseName, callback) => {
    const querySpec = {
        query: 'SELECT * FROM root r WHERE r.id = @id',
        parameters: [ { name: '@id', value: databaseName } ]
    };
    cache.client.queryDatabases(querySpec).toArray((err, results) => {
        if (err) {
            callback(err, null);
        } else {
            callback(null, (results.length === 0) ? null : results[0]);
        }
    });
});

const ensureCollectionExists = (dbRef, collectionName) => {
    if (collectionName in cache.collections) {
        return new Promise((resolve) => { resolve(cache.collections[collectionName]); });
    }

    return findCollectionById(dbRef, collectionName).then((collection) => {
        if (collection === null) {
            return createCollection(dbRef, collectionName);
        } else {
            return new Promise((resolve) => { resolve(collection); });
        }
    }).then((cref) => {
        cache.collections[collectionName] = cref;
        return cref;
    });
};

const ensureDatabaseExists = (databaseName) => {
    if (databaseName in cache.databases) {
        return new Promise((resolve) => { resolve(cache.databases[databaseName]); });
    }

    return findDatabaseById(databaseName).then((dbRef) => {
        if (dbRef === null) {
            return createDatabase(databaseName);
        } else {
            return new Promise((resolve) => { resolve(dbRef); });
        }
    }).then((createdRef) => {
        cache.databases[databaseName] = createdRef;
        return createdRef;
    });
};

const ensureUserExists = (dbRef, userId) => {
    if (userId in cache.users) {
        return new Promise((resolve) => { resolve(cache.users[userId]); });
    }

    return upsertUser(dbRef, { id: userId }).then((userRef) => {
        cache.users[userId] = userRef;
        return userRef;
    });
};

module.exports = function (context, req) {
    // Figure out the authenticated user first
    let authenticatedUser = 'anonymous';
    if ('x-zumo-auth' in req.headers) {
        context.log(`authenticated token received: ${req.headers['x-zumo-auth']}`);
        let decodedToken = jwt.decode(req.headers['x-zumo-auth']);
        if ('stable_sid' in decodedToken) {
            authenticatedUser = decodedToken['stable_sid'].substr(4);
        }
    }
    context.log(`userId = ${authenticatedUser}`);
    ensureDatabaseExists(settings.database).then((dbRef) => {
        context.log(`dbLink = ${dbRef._self}`);
        // Ensure the collection exists
        return ensureCollectionExists(dbRef, settings.collection);
    }).then((collRef) => {
        context.log(`collLink = ${collRef._self}`);
        // Ensure the user exists on the database - if not, then create it
        return ensureUserExists(cache.databases[settings.database], authenticatedUser);
    }).then((userRef) => {
        context.log(`userLink = ${userRef._self}`);
        // Create an updated permission to the collection
        let permissionBody = {
            id: `pk-${userRef.id}`,
            permissionMode: 'all',
            resource: cache.collections[settings.collection]._self,
            resourcePartitionKey: [ userRef.id ]
        };
        return upsertPermission(userRef._self, permissionBody);
    }).then((permission) => {
        context.log('permission = ', permission);
        context.res = {
            status: 200,
            body: {
                "token": permission._token,
                "userId": authenticatedUser,
                "host": settings.host,
                "collection": settings.collection,
                "database": settings.database
            }
        };
        context.done();
    }).catch((err) => {
        context.log('error = ', err);
        context.res = {
            status: 500,
            body: err
        };
        context.done();
    });
};

Ok – that’s a lot of code. Fortunately, most of it is self-explanatory. I have a bunch of functions at the top that are Promisified versions of the same functions in the DocumentClient. The ones I wrote are the ensureXXXexists() functions. These all search for the entity, and create it if it doesn’t exist. All the ensureXXXexists() functions using caching to minimize the number of times the service goes out to grab data from DocumentDb, which should reduce the latency for the service as a whole.

The important one to look at is createCollection(), recreated here:

const createCollection = Promise.promisify((dbRef, collectionName, callback) => {
    cache.client.createCollection(dbRef._self, { 
        id: collectionName,
        partitionKey: {
            paths: [ "/userid" ]
        }
    }, { 
        offerType: settings.pricingTier 
    }, callback);
});

The partitionKey object identifies the userid field as the partition key for this collection. I need to ensure that my model in the client fills this field in.

The actual HTTP request is a function that is exported. It decodes the JWT so that I have a userId. It then creates everything if needed (including the user record), and then creates a read/write permission to the collection. Finally, it packages up what the mobile client needs into a JSON blob and returns it with HTTP status 200. If anything goes wrong, a HTTP status 400 or 500 is returned (depending on where the error was). The permission is limited to a specific partition of the collection, identified by the user ID that was found in the authentication token.

Test the secure token generator

There are two tests I need to do – an anonymous test and an authenticated test. Both can be done via Postman or the Test button in the right sidebar of your Function App. I’m going to use the Test button. First, open up the Logs tab at the bottom of the screen. The function outputs a bunch of log messages to show where it is and will display any exceptions that are produced so you can deal with them.

To test the anonymous version, click Test, set the HTTP method to GET and click the Run button at the bottom. After a few moments, the logs for the running function appear in the Logs window:

Once complete, the results will be shown in the Output window:

Each of the four fields in the JSON result should have a value, and the userId field should have the value anonymous.

To test the authenticated version, I need to add the X-ZUMO-AUTH header and have a token available. To get a token, point a new tab in your browser to https://your-function-app.azurewebsites.net/.auth/login/facebook. Login and/or approve the request to Facebook. Eventually, a nice authentication successful screen will appear:

The authentication token is encoded into the URL. Copy and paste the URL into a URL Encoder/Decoder and decode it. There will be an authenticationToken within the JSON blob that is encoded. Copy that. Now go back to the Azure Function Test area and add a header called X-ZUMO-AUTH. The value should be the value of the authenticationToken:

Click Run at the bottom of the panel to run the test. This time, the userId should be a hex string. The token will also be different.

A Mobile Client

I’ve produced task lists a number of times. Get the starting point from my GitHub repository. I’ve stripped the project down to the bare bones. It logs in with Facebook Authentication (using client-flow on iOS and server-flow on Android).

The first step after logging in is to fetch a new DocumentDb service token from the Azure Function. To do this, I need a small model:

namespace TaskList.Models
{
    public class DocumentDbServiceAccess
    {
        public string Token { get; set; }
        public string UserId { get; set; }
        public string Host { get; set; }
        public string Collection { get; set; }
        public string Database { get; set; }
    }
}

These are the same field names are I am returning in the Azure Function. I can now capture this use the Azure Mobile Apps InvokeApiAsync() method (in the Services/ServerlessCloudService.cs class):

        DocumentDbServiceAccess serviceAccess;

        public async Task<MobileServiceUser> LoginAsync()
        {
            var loginProvider = DependencyService.Get<ILoginProvider>();
            var currentUser = await loginProvider.LoginAsync(client);

            serviceAccess = await client.InvokeApiAsync<DocumentDbServiceAccess>(
                "documentdb", HttpMethod.Get, new Dictionary<string, string>());

            return currentUser;
        }

I don’t need to use the MobileServiceClient here. I could “just as easily” construct a HttpClient, add the X-ZUMO-AUTH header, do the HTTP GET, retrieve the results, and deserialize the JSON into the object. InvokeApiAsync() does all that for me, and I need the MobileServiceClient for logging in anyhow. It seems silly to not use the convenience.

Now that I have the token, I can generate my DocumentClient object:

        public async Task<MobileServiceUser> LoginAsync()
        {
            var loginProvider = DependencyService.Get<ILoginProvider>();
            var currentUser = await loginProvider.LoginAsync(client);

            serviceAccess = await client.InvokeApiAsync<DocumentDbServiceAccess>(
                "documentdb", HttpMethod.Get, new Dictionary<string, string>());

            DocumentClient = new DocumentClient(new System.Uri(serviceAccess.Host), serviceAccess.Token);
            CollectionLink = UriFactory.CreateDocumentCollectionUri(serviceAccess.Database, serviceAccess.Collection);

            return currentUser;
        }

        private DocumentClient DocumentClient { get; set; }

        private Uri CollectionLink { get; set; }

I will need three methods for accessing and updating records in the DocumentDb collection, and I’ve updated the ICloudService interface accordingly:

using Microsoft.WindowsAzure.MobileServices;
using System.Collections.Generic;
using System.Threading.Tasks;
using TaskList.Models;

namespace TaskList.Abstractions
{
    public interface ICloudService
    {
        Task<MobileServiceUser> LoginAsync();
        Task<List<TaskItem>> GetAllItemsAsync();
        Task<TaskItem> InsertItemAsync(TaskItem item);
        Task<TaskItem> UpdateItemAsync(TaskItem item);
    }
}

These methods will use a TaskItem model:

using Newtonsoft.Json;

namespace TaskList.Models
{
    public class TaskItem
    {
        [JsonIgnore]
        public string _self { get; set; }

        [JsonProperty(PropertyName = "id")]
        public string Id { get; set; }

        [JsonProperty(PropertyName = "userid")]
        public string UserId { get; set; }

        [JsonProperty(PropertyName = "text")]
        public string Text { get; set; }

        [JsonProperty(PropertyName = "complete")]
        public bool Complete { get; set; }
    }
}

Note that the JSON encoding for the UserId field must match the partition key that is used when creating the collection. Without this, there will be errors because the user will not be able to write to the partition within DocumentDb.

The code for the three new methods are taken from the DocumentDb samples:

        public async Task<List<TaskItem>> GetAllItemsAsync()
        {
            var query = DocumentClient.CreateDocumentQuery<TaskItem>(
                CollectionLink,
                new FeedOptions
                {
                    MaxItemCount = -1,
                    PartitionKey = new PartitionKey(serviceAccess.UserId)
                })
                .Where(item => item.Complete == false)
                .AsDocumentQuery();

            var tlist = new List<TaskItem>();
            while (query.HasMoreResults)
            {
                tlist.AddRange(await query.ExecuteNextAsync<TaskItem>());
            }
            return tlist;
        }

        public async Task<TaskItem> InsertItemAsync(TaskItem item)
        {
            item.UserId = serviceAccess.UserId;
            var result = await DocumentClient.CreateDocumentAsync(CollectionLink, item);
            item.Id = result.Resource.Id;
            return item;
        }

        public async Task<TaskItem> UpdateItemAsync(TaskItem item)
        {
            var uri = UriFactory.CreateDocumentUri(serviceAccess.Database, serviceAccess.Collection, item.Id);
            var result = await DocumentClient.ReplaceDocumentAsync(uri, item);
            return item;
        }

In the TaskListViewModel, I can now update the ExecuteRefreshCommand() method:

        async Task ExecuteRefreshCommand()
        {
            if (IsBusy)
                return;
            IsBusy = true;

            try
            {
                var items = await CloudService.GetAllItemsAsync();
                Items.ReplaceRange(items);
            }
            catch (Exception ex)
            {
                await Application.Current.MainPage.DisplayAlert("Refresh Failed", ex.Message, "OK");
            }
            finally
            {
                IsBusy = false;
            }
        }

I’ve also added a Task Detail page (which includes a TaskDetail.xaml and a TaskDetailViewModel.cs class). This includes code to update and insert the records via the ICloudService. If the user clicks on Save, the page notifies the TaskList page that the items have potentially changed. The TaskList page then uses that notification to trigger a refresh.

Inspect the DocumentDb

If you open up Cloud Explorer or Server Explorer in Visual Studio, note that there is no DocumentDb node. I cannot inspect DocumentDb from within Visual Studio. There are also no handy extensions for me to use from within Visual Studio. Instead, download the DocumentDb Studio to inspect the contents. Once it is started and configured, it looks like this:

Right-click on a node to do something with that node. For example, if you want to delete a user, right-click the user in question and select Delete User.

Wrap Up

Currently, the Microsoft.Azure.DocumentDb.Core package (which is the DocumentDb SDK) does not support UWP. It does support iOS and Android. This won’t be a big issue for most mobile phone applications. You will notice that the final project does not include a UWP package for this reason.

You can find the code for this project on my GitHub repository.