DEV Community

Cover image for Protect Azure Functions with API Keys using Azure API Management - Part 3
Peter Davis
Peter Davis

Posted on

Protect Azure Functions with API Keys using Azure API Management - Part 3

In Part 1 and Part 2 of this series we created two Azure Functions protected by function keys, published those to Azure and then wrapped those using Azure API Management so we could control access using products and subscription keys.

In this final part we're going to update our System function to let it create new subscriptions directly in API Management which can then be used to call our user functions.

Enable Management API

Open the Management API tab from the left hand menu of the API Management instance in Azure and then set Enable Management REST API to Yes. In addition click on the Generate button next to Access Token, this will allow us to test our calls to make sure everything is working.

Management API

The API Management service exposes a management API that we can use to create subscriptions, along with many other things, and you can get more information here.

As you can see from the screenshot above ours is exposed at https://devtoapiman.management.azure-api.net and using the Access Token we generated above we can make calls to this API. Let's give it a go and get the details for the DevToUserFunctions product we created earlier.

We will need to make a call to:

https://devtoapiman.management.azure-api.net/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ApiManagement/service/{serviceName}/products/{productId}?api-version=2022-08-01

  • {subscriptionId} can be found on the Overview tab.
  • {resourceGroupName} can be found on the Overview tab.
  • {serviceName} is the name you gave to your API Management service (devtoapiman in this example)
  • {productId} is the product we want to get the details of (DevToUserFunctions in this example)

We will also need to add a header to the call. This will be called Authorization and the value should be set to the Access Token you generated above.

If we provide these details we should get information regarding our product returned, confirming we have access to the management API.

Get Product details

We will need to use the Subscription end point of this API to programmatically create new subscriptions. Details for this end point can be reviewed here.

Update our System API

Head back to Visual Studio and our devto.apiman.system project and the first thing we want to do is add RestSharp as a dependency, for this example add the following nuget packages:

  • RestSharp
  • RestSharp.Serializers.SystemTextJson

Now add a new class called APIManagementSubscription to our project.

namespace devto.apiman.system
{
    public class APIManagementSubscription
    {
        public APIManagementSubscription()
        {
            id = string.Empty;
            type = string.Empty;
            name = string.Empty;
            properties = new Properties();
        }

        public string id { get; set; }
        public string type { get; set; }
        public string name { get; set; }
        public Properties properties { get; set; }
    }

    public class Properties
    {
        public Properties()
        {
            scope = string.Empty;
            displayName = string.Empty;
            primaryKey = string.Empty;
            secondaryKey = string.Empty;
        }

        public string scope { get; set; }
        public string displayName { get; set; }
        public bool allowTracing { get; set; }
        public string primaryKey { get; set; }
        public string secondaryKey { get; set; }
    }

    public class APIManagementSubscriptionCreate
    {
        public APIManagementSubscriptionCreate()
        {
            properties = new Properties();
        }

        public Properties properties { get; set; }
    }
}
Enter fullscreen mode Exit fullscreen mode

This will be used to generate the JSON to create our new subscriptions.

We can let the management API auto generate our subscription primary and secondary key, but we're going to provide our own. In this case we'll just generate a random string, but in reality we may use guids generated in a database or another value that comes from somewhere else. Our random string generator is:

private string GenerateAPIKey(int randomLength = 50)
{
    char[] chars = "_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-=".ToCharArray();

    var randomString = new StringBuilder();

    for (var i = 0; i < randomLength; i++)
    {
        randomString.Append(chars[Random.Shared.Next(chars.Length)]);
    }

    return randomString.ToString();
}
Enter fullscreen mode Exit fullscreen mode

Next we're going to generate our payload, based on the documentation here.

private APIManagementSubscriptionCreate GenerateSubscriptionCreation(string userId)
{
    var apiManagementSubscriptionCreate = new APIManagementSubscriptionCreate();

    var subscriptionId = "XXXXXXXXXXXXXX";
    var resourceGroup = "DevTo-APIManagement";
    var serviceName = "devtoapiman";
    var productName = "devtouserfunctions";

    apiManagementSubscriptionCreate.properties.displayName = userId;
    apiManagementSubscriptionCreate.properties.scope = $"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.ApiManagement/service/{serviceName}/products/{productName}";
    apiManagementSubscriptionCreate.properties.primaryKey = GenerateAPIKey();
    apiManagementSubscriptionCreate.properties.secondaryKey = GenerateAPIKey();
    apiManagementSubscriptionCreate.properties.allowTracing = true;

    return apiManagementSubscriptionCreate;
}
Enter fullscreen mode Exit fullscreen mode

Note that we're hardcoding our values like subscription ID and resource group in this example (and you should change them to be appropriate for your setup), but in a proper application we'd read those in from something like environment variables or an Azure Key Vault.

We need to generate an Access Token to be able to call the Management API and for that we can use the following code.

private string GenerateToken()
{
    var id = "integration";
    var key = "XXXXXXXXXXXXXXXXXXXXXX";
    var expiry = DateTime.UtcNow.AddDays(10);
    using (var encoder = new HMACSHA512(Encoding.UTF8.GetBytes(key)))
    {
        var dataToSign = id + "\n" + expiry.ToString("O", CultureInfo.InvariantCulture);
        var hash = encoder.ComputeHash(Encoding.UTF8.GetBytes(dataToSign));
        var signature = Convert.ToBase64String(hash);
        var encodedToken = string.Format("SharedAccessSignature uid={0}&ex={1:o}&sn={2}", id, expiry, signature);
        return encodedToken;
    }
}
Enter fullscreen mode Exit fullscreen mode

The 'id' above is the Identifier field from the Management API tab we looked at in Azure earlier. The 'key' is either the Primary key or Secondary key from the same screen.

Our final step before we can make our call is generating the API endpoint to use.

private string GetManagerEndPoint(string userId)
{
    var subscriptionId = "XXXXXXXXXXXXXX";
    var resourceGroup = "DevTo-APIManagement";
    var serviceName = "devtoapiman";

    return $"subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.ApiManagement/service/{serviceName}/subscriptions/{userId}?api-version=2022-08-01";
}
Enter fullscreen mode Exit fullscreen mode

Now we can update our function to call the management API to create a subscription and return the result.

[Function("GenerateUserAPIKey")]
public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req)
{
    var restOptions = new RestClientOptions("https://devtoapiman.management.azure-api.net");

    var restClient = new RestClient(restOptions);

    var managerToken = GenerateToken();

    restClient.AddDefaultHeader("Authorization", managerToken);

    var apiManagementSubscriptionCreate = GenerateSubscriptionCreation("generatedUserSubscription");

    var managerEndpoint = GetManagerEndPoint("generatedUserSubscription");

    var subscriptionResult = await restClient.PutJsonAsync<APIManagementSubscriptionCreate, APIManagementSubscription>(managerEndpoint, apiManagementSubscriptionCreate);

    if(subscriptionResult == null)
    {
        var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError);
        errorResponse.Headers.Add("Content-Type", "text/plain; charset=utf-8");
        errorResponse.WriteString("Error Generating user subscription");
    }

    var validResponse = req.CreateResponse(HttpStatusCode.OK);
    await validResponse.WriteAsJsonAsync(subscriptionResult);
    return validResponse;
}
Enter fullscreen mode Exit fullscreen mode

The only other change we're going to make is to our user function, to display the API Key that was passed in so we can demonstrate retrieving that from the request so we could then use it to look up user data.

[Function("GetUserData")]
public HttpResponseData Run([HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequestData req)
{
    _logger.LogInformation("C# HTTP trigger function processed a request.");

    var response = req.CreateResponse(HttpStatusCode.OK);
    response.Headers.Add("Content-Type", "text/plain; charset=utf-8");

    response.WriteString($"Found the API Key: {GetAPIKey(req)}");

    return response;
}

public string GetAPIKey(HttpRequestData req)
{
    var foundAPIKey = req.Headers.Where(req => req.Key == "devto-key").FirstOrDefault();

    if (foundAPIKey.Value == null)
        return string.Empty;

    return foundAPIKey.Value.FirstOrDefault(string.Empty);
}
Enter fullscreen mode Exit fullscreen mode

You can now re-publish these functions from Visual Studio, but before you test the System call to generate a new subscription you'll need to manually create a subscription to call that API.

Create system subscription

Call the system 'GenerateUserAPIKey' function and check you get a valid response.

Generate User Subscription

Assuming it completed successfully you can check that in the subscriptions to see the new value

created subscription

And if we call the user function with the created API key we should now be able to retrieve that key from the request.

Get user data

Final Thoughts

Hopefully this guide has provided you with all the basic building blocks you need to create new Azure Functions and protect them with API Keys provided via Azure API Management. You have also seen how you can use the management API to generate those keys so that this can be an automated process in your application on user sign up.

It's important to note that although we have been using Azure Functions in this example, you could easily use the same functionality in API Management to protect REST APIs created and hosted via different means.

I hope you found this useful.

Pete

Buy Me A Coffee

Top comments (0)