Azure Cloud Resume Challenge Update: Adding Azure API Management

Azure Cloud Resume Challenge Update: Adding Azure API Management
Azure API Management is a great way to protect resources on Azure from unauthorised access and more fine tuned control over how requests arrive to resources over APIs - Photo by Leon Seibert / Unsplash

The Azure Cloud Resume Challenge in most cases seeks to update a visitor counter when a visitor lands on the resume holder's page.

In my first iteration of the Azure Cloud Resume Challenge I wrote about here, I initially created an architecture that looks like this:

First iteration in Azure Cloud Resume Challenge - This works but invoking the Azure Function directly is not ideal

As you can see, my implementation allowed all users to effectively invoke the Azure Function responsible for updating the visit counter in Cosmos DB, directly with Anonymous invocation. There were minor CORS protections added in to the Function App to make sure that responses would only be sent back to browser clients that were running from the desired domain in the form of the CDN endpoint. Regardless, there is a need to acknowledge the sub-optimal practice in the first implementation where the users on their browsers are rather 'too close to the metal' being able to call in directly to the Azure Function.

The Azure Function's duties and visibility can be further abstracted away from the user's browser, and we can do this with an API Gateway inside of an Azure API Management Instance.

From the Azure Portal, I created a new API Management instance on the Developer Tier. Then I imported my existing CosmosReadWriter Azure Function as an API from under the APIM Instance>APIs>Add API>Create from Azure Resource>Function App:

Adding Function as an API to APIM

Another key point is that we want to have the browser/client code be free of keys of any kind to keep things clean. We won't be using the Ocp-Apim-Subscription-Key header in an API call from the browser's javascript. Instead, we will be allowing all browser clients to freely call the API Endpoint, just without any knowledge of any keys. To stop the API Gateway automatically checking for the default Ocp-Apim-Subscription-Key in each request, we can Untick the tick box for Subscription Required in the Settings for our API and Save:

Remove Ocp-Apim-Subscription-Key Requirement from API

Then each client IP address will be rate limited to prevent too many requests from each client using the Limit Call Rate policy in Inbound Processing:

Add restrictions to how many calls can be made per IP Address

The API Gateway will enforce a CORS Policy to make sure that browser based requests came from an allowed origin. In my case, I added both the CDN Endpoint where I know users should be coming from and the Blob Storage Origin Endpoint as part of a CORS policy in Inbound Processing:

Restrict which endpoints can receive responses in a browser environment

The API Gateway will add a known custom header key using the Set headers Policy in Inbound Processing to the request before it gets to the backend (the Azure Function), so that the user's browser never gets to know what this key is and is completely blind to any keys or secrets. This key is completely custom and is SEPERATE from the Ocp-Apim-Subscription-Key and can be thought of being a 'stamp of approval' made by the API Gateway itself:

Add a custom header Key for the Azure Function to check for on each request 

Then the Javascript call used by the HTML Page is updated to now call to the endpoint of the API now ready in the APIM instance, rather than the old Function URL used before. This new file is uploaded back (and overwrites the existing file) into the $web storage container where the Static Web App runs from:

$( document ).ready(function() {
	
 $.ajax({
  url: 'https://challengeapim.azure-api.net/CosmosReadWriter/CosmosReadWriter',
  type: 'GET',
  success: function(data) {

        document.getElementById("VisitCounterText").innerHTML = "Visitor Count:" + data;
  },
  error: function(xhr, status, error) {
	alert(error);
  }
});
	
});
The updated azurefunctions.js file

Here, it is also a good idea to purge the CDN under CDN instance Overview>Purge so that the CDN can be re-hydrated with the latest updates from the origin storage container. Otherwise the CDN will use the default content refresh policy, which may not be immediate to take effect for our need.

Finally, the Azure Function code will be updated to become responsible for adding a check for the existence of a specific request header key that is expected to be added by the API Gateway. This key will also be stored in Key Vault. If this expected key is present on the request coming into the Azure Function verified against the same value stored in Key Vault, then the Azure Function can be assured that the request went through the API Gateway and went through the appropriate checks to be granted permission to reach the Azure Function:

The Azure Function code can be updated to look like this instead (other classes):

[FunctionName("CosmosReadWriter")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req,
ILogger log)
{
    //This is the new check done by Function
    //to ensure the header Key on the request coming from
    //the API Gateway is the one that we expect
    if (req.Headers["CustomGateWayKey"] != await GetKeyVaultSecret("CustomGateWayKey"))
        return new BadRequestObjectResult("Request Invalid");

    string tableName = "VisitCounter";
    string accountName = await GetKeyVaultSecret("cosmosAccountName");
    string accountKey = await GetKeyVaultSecret("cosmosDBKey");
    string tableUri = await GetKeyVaultSecret("cosmosTableUri");

    var tableClient = new TableClient(
    new Uri(tableUri),
    tableName,
    new TableSharedKeyCredential(accountName, accountKey));

    var queryResultsFilter = 
    	tableClient.Query<TableEntity>().OrderByDescending(x => x.Timestamp)?.Take(1);
    if (!queryResultsFilter.Any())
    {
        var tableEntity = 
        	new TableEntity(DateTime.UtcNow.ToString("yyyy-MMM-dd HH:mm:ss"),
            Guid.NewGuid().ToString())
        {
        	{ "Counter", 1 }
        };

        tableClient.AddEntity(tableEntity);
        return new OkObjectResult(1);

    }
    else
    {
        var result = queryResultsFilter?.FirstOrDefault();
        var current = result.GetInt32("Counter");
        var newvalue = current + 1;
        var tableEntity = 
        	new TableEntity(DateTime.UtcNow.ToString("yyyy-MMM-dd HH:mm:ss"),
            Guid.NewGuid().ToString())
        {
        	{ "Counter",newvalue }
        };

        tableClient.AddEntity(tableEntity);
        return new OkObjectResult(newvalue);
    }

}
Azure Function App's CosmosReadWriter Function with new simplified check for a known custom header that can only come from APIM instance, verified from Key Vault
public static async Task<string> GetKeyVaultSecret(string kvSecret)
{
    var credential = new DefaultAzureCredential();

    var secretClient = new SecretClient(new Uri("{KEY_VAULT_URI}"),
    				   credential);

    KeyVaultSecret secret = await secretClient.GetSecretAsync(kvSecret);
    return secret.Value;
}
Key Vault Access code from Function (The Azure Function MUST have a System-Assigned Managed Identity with GET Permissions on the Key Vault Access Policy)

The entire static web app can now be sending requests to the API Gateway in Azure APIM rather than sending requests directly to the Azure Function.

The new architecture would look like this now:

New Architecture with Azure APIM that applies policies to incoming requests