Securing Azure Functions using certificate authentication

This article shows how to secure Azure Functions using X509 certificates. The client is required to send a specific certificate to access the Azure Function.

Code: https://github.com/damienbod/AzureFunctionsSecurity

History

  • 2024-07-05 Updated to .NET 8, V4 isolated Azure functions

Blogs in the series

Setup the Azure Function to require certificates

A Dedicated (App Service) plan is used, so that certificates can be set to required for all incoming requests. The Azure Functions are hosted using an dedicated Azure App Service. Now the Azure App Service can be forced to require certificates.

If any certificates are sent, the certificate sent with the HTTP request will get forwarded to the Azure Functions hosted in the Azure App Service.

Using and validating the certificate in an Azure Function

The incoming certificate needs to be validated. The Azure App service forwards the certificate to the X-ARR-ClientCert header. A X509Certificate2 can be created from the header value which is a base64 string containing the certificate byte array. Now the certificate can be validated. In the example, the Thumbprint is checked and the NotBefore, NotAfter values. Sadly only self signed certificates can be used together with Azure (Not chained). The X509Chain only loads the certificate and not the chain in Azure. This might work with a trusted chain, but I don’t have to money to try this and buy a root certificate for client/server certificate auth. This is a pity as using chained certificates would be awesome for this type of security. Chained certificates created from a non-trusted root certificate works outside Azure and other hosts.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text.Encodings.Web;

namespace FunctionCertificate;

public class RandomStringFunction
{
    private readonly ILogger<RandomStringFunction> _logger;

    public RandomStringFunction(ILogger<RandomStringFunction> logger)
    {
        _logger = logger;
    }

    /// <summary>
    /// Only the Thumbprint, NotBefore and NotAfter are checked, further validation of the client can/should be added
    /// Chained certificate do not work with Azure App services, X509Chain only loads the certificate, not the chain on Azure
    /// Maybe due to the chain being not trusted. (Works outside Azure)
    /// Certificate validation docs
    /// https://github.com/dotnet/aspnetcore/blob/master/src/Security/Authentication/Certificate/src/CertificateAuthenticationHandler.cs
    /// https://docs.microsoft.com/en-us/azure/app-service/app-service-web-configure-tls-mutual-auth#aspnet-sample
    /// </summary>
    [Function("RandomStringCertAuth")]
    public IActionResult RandomStringCertAuth([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)]
        HttpRequest request)
    {
        _logger.LogInformation("C# HTTP trigger RandomString processed a request.");

        StringValues cert;
        if (request.Headers.TryGetValue("X-ARR-ClientCert", out cert))
        {
            if (cert[0] == null) throw new ArgumentNullException(nameof(cert));

            byte[] clientCertBytes = Convert.FromBase64String(cert[0]);
            var clientCert = new X509Certificate2(clientCertBytes);

            // Validate Thumbprint
            if (clientCert.Thumbprint != "5726F1DDBC5BA5986A21BDFCBA1D88C74C8EDE90")
            {
                return new BadRequestObjectResult($"A valid client certificate is not used: {clientCert.Thumbprint}");
            }

            // Validate NotBefore and NotAfter
            if (DateTime.Compare(DateTime.UtcNow, clientCert.NotBefore) < 0
                        || DateTime.Compare(DateTime.UtcNow, clientCert.NotAfter) > 0)
            {
                return new BadRequestObjectResult("client certificate not in alllowed time interval");
            }

            // Add further validation of certificate as required.

            return new OkObjectResult(GetEncodedRandomString());
        }

        return new BadRequestObjectResult("A valid client certificate is not found");
    }

    private string GetEncodedRandomString()
    {
        var base64 = Convert.ToBase64String(GenerateRandomBytes(100));
        return HtmlEncoder.Default.Encode(base64);
    }

    private byte[] GenerateRandomBytes(int length)
    {
        return RandomNumberGenerator.GetBytes(length);
    }
}

Sending a request using a HttpClient

A client can now use the Azure Function API. We will use a .NET Core console application which uses the HttpClient. The HTTP X-ARR-ClientCert header is used to send the certificate when testing locally. We can add this directly in the HttpClient.

using System;
using System.Net.Http;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

namespace AzureCertAuthClientConsole;

class Program
{
    async static Task Main(string[] args)
    {
        Console.WriteLine("Let's try to get a random string from the Azure Function using a certificate!");
        Console.WriteLine("----");
        var result = await CallApi();
        Console.WriteLine($"{result}");
        Console.WriteLine("----");
        Console.WriteLine($"Success!");
    }

    private static async Task<string> CallApi()
    {
        var cert = new X509Certificate2("functionsCertAuth.pfx", "1234");
        var azureRandomStringBasicUrl = "https://functioncertificate20240704202458.azurewebsites.net/api/RandomStringCertAuth";
        return await CallApiClientCertHeader(azureRandomStringBasicUrl, cert, false);

        //var cert = new X509Certificate2("client401.pfx", "1234");
        //var localRandomStringBasicUrl = "http://localhost:7108/api/RandomStringCertAuth";
        //return await CallApiClientCertHeader(localRandomStringBasicUrl, cert, true);
    }

    private static async Task<string> CallApiClientCertHeader(string url, X509Certificate2 clientCertificate, bool isLocalTesting)
    {
        try
        {
            var handler = new HttpClientHandler();
            handler.ClientCertificates.Add(clientCertificate);
            var client = new HttpClient(handler);

            var request = new HttpRequestMessage()
            {
                RequestUri = new Uri(url),
                Method = HttpMethod.Get,
            };

            // Only needed for testing on local host
            if (isLocalTesting)
            {
                request.Headers.Add("X-ARR-ClientCert", Convert.ToBase64String(clientCertificate.RawData));
            }
            var response = await client.SendAsync(request);

            var responseContent = await response.Content.ReadAsStringAsync();
            if (response.IsSuccessStatusCode)
            {

                return responseContent;
            }

            throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
        }
        catch (Exception e)
        {
            throw new ApplicationException($"Exception {e}");
        }
    }
}

Testing the Function

If we start the application using the correct client certificate, the random string will be returned from the Azure Function.

If we start the application using the incorrect client certificate or no certificate, an exception will be thrown in the HttpClient.

 private static async Task<string> CallApi()
 {
     var cert = new X509Certificate2("functionsCertAuth.pfx", "1234");
     var azureRandomStringBasicUrl = "https://functioncertificate20240704202458.azurewebsites.net/api/RandomStringCertAuth";
     return await CallApiXARRClientCertHeader(azureRandomStringBasicUrl, cert);

     //var cert = new X509Certificate2("client401.pfx", "1234");
     //var localRandomStringBasicUrl = "http://localhost:7108/api/RandomStringCertAuth";
     //return await CallApiXARRClientCertHeader(localRandomStringBasicUrl, cert);
 }

Links

https://docs.microsoft.com/en-us/azure/azure-functions/security-concepts

https://docs.microsoft.com/en-us/aspnet/core/security/authentication/certauth

https://github.com/dotnet/aspnetcore/blob/master/src/Security/Authentication/Certificate/src/CertificateAuthenticationHandler.cs

https://stackoverflow.com/questions/27307322/verify-server-certificate-against-self-signed-certificate-authority

https://stackoverflow.com/questions/24107374/ssl-certificate-not-in-x509store-when-uploaded-to-azure-website#34719216

https://docs.microsoft.com/en-us/azure/app-service/app-service-web-configure-tls-mutual-auth#access-client-certificate

10 comments

  1. cocowalla's avatar

    As well as the `X-ARR-ClientCert` header, you can also access the certificate from the HTTP request object:
    `request.HttpContext.Connection.ClientCertificate`

    I seem to recall it also works like:
    `request.HttpContext.Features.Get().ClientCertificate`

    1. damienbod's avatar

      Hi Cocowalla thanks, I’ll try this again, this didn’t work when I deployed it to Azure using functions, works with ASP.ENT Core, but I will try it and again and validate.

      Greetings Damien

  2. […] Securing Azure Functions using certificate authentication (Damien Bowden) […]

  3. […] Securing Azure Functions using certificate authentication – Damien Bowden […]

  4. Tom's avatar

    Great article.

    So how do I call the Function App? I keep getting 403 and I am sending a .cer certificate in code using c#.

    Will incoming certs work for HTTPS calls to the app? or does it only work with HTTP?

  5. Unknown's avatar

    […] Securing Azure Functions using certificate authentication […]

  6. […] Securing Azure Functions using certificate authentication Posted in News […]

  7. […] Securing Azure Functions using certificate authentication […]

Leave a reply to The Morning Brew - Chris Alcock » The Morning Brew #3063 Cancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.