Secure Azure Functions locally using a custom provider

Developing Azure Functions could be cumbersome if you want to use App Service Authentication feature. While it works flawlessly when a function is deployed, it brings many unfair challenges when working locally(mostly because of a need to create an artificial mock of identity provider and injecting it somehow). I decided to give it a try and modify Azure Functions CLI a little bit, to it has that feature implemented already. Surprisingly it was easier that I thought.

How does Azure Functions CLI work?

When you're working with Functions locally, when you hit F5, you'll see a local runtime starting and ready to handle a request(or trigger a function). In fact VS starts it using Azure Functions CLI and invoking following command:

> func start

Local instance of runtime with a boilerplate function enabled

When this local host is started, it handles multiple features like:

  • loading settings from local.settings.json
  • starting a runtime
  • providing an endpoint to handle HTTP requests
  • ...and many more

In general you're able to pass many different parameters so you can start a host listening on a specific port, with HTTPS enabled and CORS configured. You can do it like so:

func start --useHttps=true --cors=*

I got an idea to extend parameters so you can do following:

func start --security=true

So each function invocation has to be challenged against a custom security provider. How did I achieve this?

Handling a parameter

The very first thing I had to do was to handle a security parameter in CLI. To do so I modified StartHostAction class which parses inline arguments when CLI is started. Added line looks like this:

	.WithDescription("Enable securing HTTP functions using available providers")
	.Callback(s => Security = s);

This was super easy. Let's do something more difficult - use this parameter so some logic is performed.

Securing each request

Because CLI is built against new ASP.NET Core pipeline, you have to provide a custom middleware so each request has to pass through it. There's a Startup class, which is the foundation of the whole host. There you can inject your functionality as I did:

app.Use(async (context, next) =>
	if (_security)
		var provider = new SecurityProvider();
		var authenticationResult = provider.IsAuthenticated(context.Request);

		if (authenticationResult == false)
			context.Response.StatusCode = 401;

	await next.Invoke();

Now if security is enabled, each request will be validated using some SecurityProvider,which is a custom class implementing following interface:

internal interface ISecurityProvider
	bool IsAuthenticated(HttpRequest req);

How does it work?

Now when I start CLI using following command:

func start --security=true

I'm getting the following result:

When I disable security:

Of course the error in the second response comes from the boilerplate function because I didn't pass name parameter.

Now since IsAuthenticated() has a full request passed, you can implement whichever flow you want, starting from a very basic one like me:

public class SecurityProvider : ISecurityProvider
	public bool IsAuthenticated(HttpRequest req)
		if (req.Headers.ContainsKey("Authorization"))
			return true;

		return false;

What's next?

In the next episode I'll try to enhance this solution a little bit, so ISecurityProvider will be loaded from a Function App you'll be developing locally(so it gains much flexibility). For now you can following this issue on GitHub, where I proposed solution I described above.

Beware of TimerTrigger in Azure Functions

TimerTrigger is one of the easiest triggers in Azure Functions to handle. It has however an interesting "issue", which is described in the documentation - you should not use the same id property for different Function App. Why is that?

Just as planned

Let's assume you have two identical functions triggered using TimerTrigger:

public static void Run([TimerTrigger("0 */1 * * * *")]TimerInfo myTimer, TraceWriter log)
	log.Info($"TIMER trigger function executed at: {DateTime.Now}");

Assuming our host.json file is empty, after we deploy two Function Apps, we'll get following results:

As you can see both are working correctly(I changed displayed text a little bit so you can see the difference). Now let's abuse things a little - I forced two Function Apps to work under the same id. To do so I changed my host.json file:

  "id": "9f4ea53c5136457d883d685e57164f08"

With this change something broke:

As we can read in documentation:

If you share a Storage account across multiple function apps, make sure that each function app has a different id in host.json. You can omit the id property or manually set each function app's id to a different value. The timer trigger uses a storage lock to ensure that there will be only one timer instance when a function app scales out to multiple instances. If two function apps share the same id and each uses a timer trigger, only one timer will run.

What is the cause of such behaviour?

Underlying functionality

If you check the underlying storage powering Azure Function, you'll find azure-webjobs-host container, which stores e.g. locks from TimerTrigger. Mine looks like this:

As you can see I have 3 different locks - timertriggertest come from the previous version of my functionality, where I didn't have id explicitely set. The very first folder is named using id property in host.json file.

We can find also in the source code. There's a class PrimaryHostCoordinator, which is used to determine which host instance is the primary one. It is initialized at the very beginning after functions has been loaded and tries to aquire a lock on a blob every 5 seconds. If it fails, a function won't be triggered. Remember not to set id if you don't have to and if you do, try to avoid using functions tiggered by TimerTrigger in different Function Apps.