[HOWTO] Implement Azure Functions middleware for authentication purposes

Important
Currently middleware feature is only supported by isolated worker model .NET Azure Functions.
For more details see here.

In a project I’m currently working on, we consume webhook calls from Enode with an isolated worker model .NET Azure Function. Enode is a platform to connect, control and report on energy devices. Enode provides a very well documented REST API and also offers webhooks that deliver notifications of events to a defined target.

To ensure that the webhook consumer (in our case the Azure Function) only accepts calls from the Enode webhook, the payload signature sent in X-Enode-Signature header has to be verified. In a first approach I implemented the validation directly in the Run method of the Azure Function. However, I felt that there should be a more appropriate place for the signature validation. I checked, if Azure Functions also supports middleware in a similar way as known from ASP.NET. It’s supported – however only by .NET isolated worker model .NET Azure Functions as pointed out in the beginning of this article.

Lucky me to have an Azure Function of this type 😉 I did a little more research and finally ended up with the following implementation.

Middleware

First, I implemented the middleware that validates the Enode signature according to the docs.

internal sealed class EnodeSignatureValidationMiddleware : IFunctionsWorkerMiddleware
{
    public const string IsEnodeSignatureValid = "IsSignatureValid";

    private const string _enodeSignatureHeaderName = "X-Enode-Signature";
    private readonly EnodeOptions _enodeOptions;
    private readonly ILogger<EnodeSignatureValidationMiddleware> _logger;

    public EnodeSignatureValidationMiddleware(
        IOptions<EnodeOptions> enodeOptions,
        ILoggerFactory loggerFactory)
    {
        _enodeOptions = enodeOptions.Value;
        _logger = loggerFactory.CreateLogger<EnodeSignatureValidationMiddleware>();
    }

    /// <summary>
    /// Verify Enode payload signature
    /// See https://developers.enode.com/docs/webhooks#verifying-a-payload-signature
    /// </summary>
    /// <param name="context">The <see cref="FunctionContext"/> for the current invocation.</param>
    /// <param name="next">The next middleware in the pipeline.</param>
    /// <returns>A <see cref="Task"/> that represents the asynchronous invocation.</returns>
    public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
    {
        var requestData = await context.GetHttpRequestDataAsync();

        var isEnodeSignatureHeaderPresent = requestData!.Headers.TryGetValues(_enodeSignatureHeaderName, out var enodeSignatureHeader);
        if (!isEnodeSignatureHeaderPresent)
        {
            _logger.LogWarning("No {EnodeSignatureHeaderName} header found", _enodeSignatureHeaderName);
            context.Items.Add(IsEnodeSignatureValid, false);
            await next(context);
            return;
        }

        var keyBytes = Encoding.UTF8.GetBytes(_enodeOptions.WebhookSecret);

        var body = await BinaryData.FromStreamAsync(requestData.Body);
        var payloadBytes = Encoding.UTF8.GetBytes(body.ToString());

        using var hmac = new HMACSHA1(keyBytes);
        var hashBytes = hmac.ComputeHash(payloadBytes);
        var hashString = "sha1=" + BitConverter.ToString(hashBytes).Replace("-", "").ToLower();

        var hashBytesUtf8 = Encoding.UTF8.GetBytes(hashString);
        var enodeSignatureHeaderBytesUtf8 = Encoding.UTF8.GetBytes(enodeSignatureHeaderValue);

        if (!TimingSafeEqual(hashBytesUtf8, enodeSignatureHeaderBytesUtf8))
        {
            _logger.LogWarning("Value of Enode signature header does not match computed signature");
            context.Items.Add(IsEnodeSignatureValid, false);
            await next(context);
            return;
        }

        context.Items.Add(IsEnodeSignatureValid, true);
        // Resets position of HTTP request body stream to the beginning
        requestData.Body.Position = 0;
        await next(context);
    }

    private static bool TimingSafeEqual(byte[]? a, byte[]? b)
    {
        ...
    }
}

There are two important things to mention here. The validation result gets added to the Items dictionary of the function context, so that it can be evaluated in the Run method of the function. If you read the request body in the middleware and in the function logic, make sure to reset the position of the HTTP request body stream before calling next(..). Otherwise the body stream will be empty when reading next time.

Azure Function

The Azure Function EnodeWebhookConsumer reads the IsSignatureValid item from the function context and creates a HTTP response with status code 401, in case the signature is invalid.

public class EnodeWebhookConsumer
{
    private readonly ILogger _logger;

    public EnodeWebhookConsumer(ILoggerFactory loggerFactory)
    {
        _logger = loggerFactory.CreateLogger<EnodeDataIngestor>();
    }

    [Function("EnodeEndpoint")]
    public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData request, FunctionContext executionContext)
    {
        var isEnodeSignatureValid = executionContext.Items.TryGetValue(EnodeSignatureValidationMiddleware.IsEnodeSignatureValid, out var isSignatureValid) && (bool)isSignatureValid;
        if (!isEnodeSignatureValid)
        {
            return request.CreateResponse(HttpStatusCode.Unauthorized);
        }

        var requestBodyData = await BinaryData.FromStreamAsync(request.Body);

        // implement/call logic here

        return request.CreateResponse(HttpStatusCode.OK);
    }
}

Program.cs

Last but not least, let’s have a look at the registration and configuration of the middleware.

public class Program
{
    public static async Task Main(string[] args)
    {
        await CreateHostBuilder().Build().RunAsync();
    }

    public static IHostBuilder CreateHostBuilder()
    {
        return new HostBuilder()
            .ConfigureFunctionsWorkerDefaults((_, functionsWorkerApplicationBuilder) =>
            {
                functionsWorkerApplicationBuilder.UseWhen<EnodeSignatureValidationMiddleware>(ctx =>
                {
                    // use this middleware only for http trigger invocations
                    return ctx.FunctionDefinition.InputBindings.Values
                        .First(a => a.Type.EndsWith("Trigger")).Type == "httpTrigger";
                });
            })
            .ConfigureServices((context, services) =>
            {
                services.AddOptions<EnodeOptions>()
                    .Configure<IConfiguration>((settings, configuration) =>
                    {
                        configuration.GetSection("Enode").Bind(settings);
                    });
            });
    }
}

For more details concerning middleware in isolated .NET Azure Functions, see here.

That’s it. In a next step I tried to implement some integration tests for the function but unfortunately failed. Seems like I was not the only one based on the resources I found in the internet. .NET integration testing seems to not yet be fully supported for isolated worker model .NET Azure Functions as also mentioned in the comments of the following article: Azure Functions – Part 2 – Unit and Integration Testing – Microsoft Community Hub.

One thought on “[HOWTO] Implement Azure Functions middleware for authentication purposes

Add yours

Leave a comment

Website Powered by WordPress.com.

Up ↑