Monday, November 6, 2023

Azure API Management: Self-Signed Certificate Authentication with ASP.NET Core Web API Running in an Azure Web Apps for Containers

I recently spent quite an interesting time trying to get self-signed Certificate Authentication working with ASP.NET Core Web API Running in Azure Web Apps for Containers. The caller was an Azure API Management, that was making a send-request as part of its policy, something like the following.

<send-request mode="new" response-variable-name="httpresponse" timeout="10" ignore-error="true">
    <set-url>@("https://app-something-001.azurewebsites.net/api/values)</set-url>
    <set-method>GET</set-method>
    <authentication-certificate certificate-id="self-signed-certificate" />
</send-request>

There were a couple of important things that had to be in place in order for it to finally work. So in this post, let's have a look at those, so hopefully someone might find it helpful.

The first piece is to get the Certificate Authentication working locally.

1: Configure Kestrel configuration options to AllowCertificates and if required introduce additional  client certificate validation

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureHttpsDefaults(options =>
    {
        options.ClientCertificateMode = ClientCertificateMode.AllowCertificate;
        options.ClientCertificateValidation = (certificatechainerrors) =>
        {
            // TODO: Additional client certificate validation logic here
            return true;
        };
    });
});

2: Customize Certificate Authentication Options to allow self-signed certificates

builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
    {
     // Default is Chained only, All is required Chained | SelfSigned
        options.AllowedCertificateTypes = CertificateTypes.All;
        options.RevocationMode = X509RevocationMode.NoCheck;
        options.ValidateCertificateUse = false;

        options.Events = new CertificateAuthenticationEvents
        {
          OnCertificateValidated = context =>
             { // TODO: Further validation if required
              Claim[] claims = new[]
                 {
                  new Claim(ClaimTypes.NameIdentifier, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer),
                     new Claim(ClaimTypes.Name, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer)
                 };

                 context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claimscontext.Scheme.Name));

                 context.Success();

                 return Task.CompletedTask;
             },
             OnAuthenticationFailed = context =>
             {
              context.Fail("Invalid certificate");

                 return Task.CompletedTask;
             }
        };
});
builder.Services.AddAuthorization();

3: Check whether everything is working locally. 

Send a request to an endpoint that requires authorization using a Tool like Postman and configure the certificate to be sent along.

Postman Certificate Authentication
So when a request matches the HOST, Postman will automatically associate the certificate to the request.

Once the Certificate Authentication is working locally, the next step is getting it to work when running on Azure Web Apps for Containers.

Here there are some very important things. 

4: Configure Incoming client certificates in Azure App Service Configuration -> General Settings.

Incoming Client Certificates

5: The most important thing: in Azure App Services, TLS termination of the request happens at the frontend load balancer. When forwarding the request to your app code with client certificates enabled, App Service injects an X-ARR-ClientCert request header with the client certificate. App Service does not do anything with this client certificate other than forwarding it to your app. Your app code is responsible for validating the client certificate.

So we need to do some code changes as follows. 

First, configure Services as below.

builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
    // Only loopback proxies are allowed by default. Clear that restriction to enable this explicit configuration.
    options.KnownNetworks.Clear();
    options.KnownProxies.Clear();
});

// Configure the application to client certificate forwarded the frontend load balancer
builder.Services.AddCertificateForwarding(options => 

    options.CertificateHeader = "X-ARR-ClientCert"
}); // builder.Services.AddAuthentication...

Then we need to update the middleware order adding UseForwardedHeaders and UseCertificateForwarding to the beginning of the execution order.

WebApplication app = builder.Build();

app.UseForwardedHeaders();
app.UseCertificateForwarding();

// Note: HttpLogging should be after UseForwardedHeaders and UseCertificateForwarding. // otherwise those won't get logged
app.UseHttpLogging();

app.UseHttpsRedirection();

app.UseAuthentication();
app.UseAuthorization(); // ...

And hopefully, that should get the things to work.

The certificate was successfully authenticated
If things aren't working as expected, enable Debug logging in the application and start troubleshooting from there.

Another important note: If you try to route a request with certificate authentication to your local machine using Tunneling in Visual Studio, it will always fail because it's still not supported (as of today). Follow the feature request here: Support forwarding HTTPS client certificates.

Hope this helps.

Happy Coding.

Regards,
Jaliya

No comments:

Post a Comment