Protect Azure Functions with API Keys using Azure API Management - Part 1
Peter Davis
Posted on September 6, 2023
Secure your Azure Functions with API Keys
While building REST APIs using .NET I've generally handled all the authentication tasks within the code I'm writing, however as I've transitioned across to using Isolated Azure Functions I've started to take advantage of the built in authentication provided by Azure, this allows me to remove authentication code from my projects and have that handled externally, providing me with the flexibility to change how I authenticate without the need to update my code.
Because my projects have previously used Azure B2C authentication I'd setup my Azure Functions to use that as an identity provider, which meant I needed to provide a valid token to authenticate.
I wanted to change this to allow for authentication with an API Key instead and by combining Azure Functions with Azure API Management I've been able to setup a simple way to achieve this programmatically.
In this article I'll cover:
- Creating Azure Functions and publishing them to Azure
- Importing Azure Functions into Azure API Management
- Setting up Products and Subscriptions in Azure API Management
- Generating API Keys and creating subscriptions Programmatically
Costs
One thing to note before we dive in is that the use of Azure Functions and API Management does come with a cost. In this example we'll use the consumption based tier for our functions so that cost will be minimal but for API management, there are limitations on the number of subscriptions you can create in the consumption based plan and more importantly we need access to the Management API which isn't provided at that level so we need to use the developer tier.
Create our Azure Functions
We're going to create two very simple Azure functions for this example. A System Function, that will handle the creation of new API Keys and a User Function that will be protected by those created API keys.
We will protect our System Function with an API Key that only we know and in a real world scenario this would be used by our application to create new API keys for users, likely at the point of user sign-up, without us needing to do it manually. Those generated keys can then be used to call our other function, and if we store those keys somewhere that will also allow us to identify the user making the call.
Within Azure choose Create a resource and then select Function App:
Create our first System function using a new resource group and name, choosing to deploy code using the .NET stack, 7 Isolated for the version and a region applicable to you. The operating system will be Linux and we'll use the Consumption hosting option.
Choose Review + create as all other settings can be left as their defaults.
Once this is done create a second User function with the same options but a different name
After this you should have two functions created in Azure, mine are called devto-apiman-system and devto-apiman-user.
Create Visual Studio Projects
You can use whatever development tool you want to create your code, in this example I'm going to use Visual Studio, the community edition is free to use, but you can also use Visual Studio Code or something like Rider from Jet Brains.
In Visual Studio choose to create a new Azure Function and choose a name, I'm going to use devto.apiman.system with a solution called DevTo APIManagement
On the next screen make sure you've choose .NET 7.0 Isolated for the 'Functions worker' and Http trigger for the 'Function'. Make sure that the 'Authorization level' is set to Function, this means that a key will be required to call the function once it is deployed to Azure. If we set this to 'Anonymous' we could still wrap it with an API Key when calling the function from Azure API Management, but if the user found the direct URI of the function they would be able to call it directly without a key.
Once created, add another function project to the solution with the same settings but for the user function. Once done you should have a solution that looks a little like this.
We're going to keep this simple for the moment, so change the Function1.cs in your system function so that the function name is something better ("GenerateUserAPIKey" in my case), set the Trigger to be a "post" request only, and tweak the message returned.
using System.Net;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
namespace devto.apiman.system
{
public class Function1
{
private readonly ILogger _logger;
public Function1(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<Function1>();
}
[Function("GenerateUserAPIKey")]
public HttpResponseData Run([HttpTrigger(AuthorizationLevel.Function, "post")] 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("Generated a user API Key!");
return response;
}
}
}
Edit Function1.cs in the user function in a similar way but make it a "get" request.
using System.Net;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
namespace devto.apiman.user
{
public class Function1
{
private readonly ILogger _logger;
public Function1(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<Function1>();
}
[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("We got some user data!");
return response;
}
}
}
Update you solution properties so that both projects get started and then just run the solution to test it out. You should get the two functions starting, with their URIs.
If we use something like Postman to call these APIs you should get an 'OK' and the appropriate message back.
Notice that although we've specified AuthorizationLevel.Function for these APIs, we don't need to provide a key to call them. this is because when run locally the function authorization is ignored, it will only come into effect once we upload to Azure. We could still get the passed in function key from the request header for testing purposes, but it won't be validated during the call.
Lets push these functions up to Azure and test them there.
Deploy to Azure
Right-Click on the system function project and choose publish. Work through the wizard, logging into your Azure account, choosing 'Azure Function App (Linux)' and then your system function.
This will generate you a publishing profile, once this has completed choose Publish and after a minute or so you should receive a 'Publish succeeded' message.
Go ahead and publish our user function in the same way.
If you happen to receive errors during this publish stage quite often if you head over to Azure and stop the function and then retry the publish that will clear the issue.
Once published head over to our User function in Azure and you should see our GetUserData function listed at the bottom of the page (Sometimes you might need to restart and refresh the app a few times to see this) and the URI in the top right.
Lets go ahead and call the GetUserData function like we did locally.
This time notice that we get a 401 Unauthorized response, that's because the AuthorizationLevel.Function has now activated and we now need a function key to call the API.
Head over to the App keys tab in azure and notice that we have some host keys
If we copy the default key and then add a x-functions-key header, which includes that default key value, to our request in Postman then our request should now complete successfully.
Next Steps
Mission complete right, we have now published our functions to Azure and protected them with a key....
....the trouble is, we want to provide our users with their own API Keys, rather than a global key that everyone has, so that we can add and withdraw access as required.
While we could manually create new host keys for the functions, we really want a way to manage that easily, programmatically generate our keys, and potentially provide access to specific calls for different users.
For that, we'll need a better way to manage access and we'll use Azure API Management.
Head on over to Part 2 to continue this.
Pete
Posted on September 6, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.