Implement certificate authentication in ASP.NET Core for an Azure B2C API connector

This article shows how an ASP.NET Core API can be setup to require certificates for authentication. The API is used to implement an Azure B2C API connector service. The API connector client uses a certificate to request profile data from the Azure App Service API implementation, which is validated using the certificate thumbprint.

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

Blogs in this series

Setup Azure App Service

An Azure App Service was created which uses .NET and 64 bit configurations. The Azure App Service is configured to require incoming client certificates and will forward this to the application. By configuring this, any valid certificate will work. The certificate still needs to be validated inside the application. You need to check that the correct client certificate is being used.

Implement the API with certificate authentication for deployment

The AddAuthentication sets the default scheme to CertificateAuthentication. The AddCertificate method adds the required configuration to validate the client certificates used with each request. We use a self signed certificate for the authentication. If a valid certificate is used, the MyCertificateValidationService is used to validate that it is also the correct certificate.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddSingleton<MyCertificateValidationService>();

builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
    .AddCertificate(options =>
    {
        // https://docs.microsoft.com/en-us/aspnet/core/security/authentication/certauth
        options.AllowedCertificateTypes = CertificateTypes.SelfSigned;

        options.Events = new CertificateAuthenticationEvents
        {
            OnCertificateValidated = context =>
            {
                var validationService = context.HttpContext.RequestServices.GetService<MyCertificateValidationService>();

                if (validationService != null && validationService.ValidateCertificate(context.ClientCertificate))
                {
                    var 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(claims, context.Scheme.Name));
                    context.Success();
                }
                else
                {
                    context.Fail("invalid cert");
                }

                return Task.CompletedTask;
            }
        };
    });

builder.Host.UseSerilog((hostingContext, loggerConfiguration) => loggerConfiguration
    .ReadFrom.Configuration(hostingContext.Configuration)
    .Enrich.FromLogContext()
    .MinimumLevel.Debug()
    .WriteTo.Console()
    .WriteTo.File(
        //$@"../certauth.txt",
        $@"D:\home\LogFiles\Application\{Environment.UserDomainName}.txt",
        fileSizeLimitBytes: 1_000_000,
        rollOnFileSizeLimit: true,
        shared: true,
        flushToDiskInterval: TimeSpan.FromSeconds(1)));

The middleware services are setup so that in development no authentication is used and the requests are validated using basic authentication. If the environment in not development, certificate authentication is used and all API calls require authorization.

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

if (!app.Environment.IsDevelopment())
{
    app.UseAuthentication();
    app.UseAuthorization();
    app.MapControllers().RequireAuthorization();
}
else
{
    app.UseAuthorization();
    app.MapControllers();
}

app.Run();

The MyCertificateValidationService validates the certificate. This checks if the certificate used has the correct thumbprint and is the same as the certificate used in the client application, in these case the Azure B2C API connector.

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

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

	public bool ValidateCertificate(X509Certificate2 clientCertificate)
	{
		return CheckIfThumbprintIsValid(clientCertificate);
	}

	private bool CheckIfThumbprintIsValid(X509Certificate2 clientCertificate)
	{
		var listOfValidThumbprints = new List<string>
		{
			// add thumbprints of your allowed clients
			"15D118271F9AE7855778A2E6A00A575341D3D904"
		};

		if (listOfValidThumbprints.Contains(clientCertificate.Thumbprint))
		{
			_logger.LogInformation($"Custom auth-success for certificate  {clientCertificate.FriendlyName} {clientCertificate.Thumbprint}");

			return true;
		}

		_logger.LogWarning($"auth failed for certificate  {clientCertificate.FriendlyName} {clientCertificate.Thumbprint}");

		return false;
	}
}

Setup Azure B2C API connector with certification authentication

The Azure B2C API connector is setup to use a certificate. You can create the certificate anyway you want. I used the CertificateManager Nuget package to create a RSA 512 certificate with a 3072 key size. The thumbprint from this certificate needs to be validated in the ASP.NET Core API application.

The Azure B2C API connector is added to the Azure B2C user flow. The use flow requires all the custom claims to be defined and the values can be set in the API Connector service. See the first post in this blog group for details.

Creating an RSA 512 with a 3072 size key

You can create certificates using .NET Core using the CertificateManager Nuget package which provides some helper methods for creating the X509 certificates as required.

class Program
{
	static CreateCertificates _cc;
	static void Main(string[] args)
	{
		var builder = new ConfigurationBuilder()
			.AddUserSecrets<Program>();
		var configuration = builder.Build();

		var sp = new ServiceCollection()
		   .AddCertificateManager()
		   .BuildServiceProvider();

		_cc = sp.GetService<CreateCertificates>();

		var rsaCert = CreateRsaCertificateSha512KeySize2048("localhost", 10);

		string password = configuration["certificateSecret"];
		var iec = sp.GetService<ImportExportCertificate>();

		var rsaCertPfxBytes = iec.ExportSelfSignedCertificatePfx(password, rsaCert);
		File.WriteAllBytes("cert_rsa512.pfx", rsaCertPfxBytes);

		Console.WriteLine("created");
	}

	public static X509Certificate2 CreateRsaCertificateSha512KeySize2048(string dnsName, int validityPeriodInYears)
	{
		var basicConstraints = new BasicConstraints
		{
			CertificateAuthority = false,
			HasPathLengthConstraint = false,
			PathLengthConstraint = 0,
			Critical = false
		};

		var subjectAlternativeName = new SubjectAlternativeName
		{
			DnsName = new List<string>
			{
				dnsName,
			}
		};

		var x509KeyUsageFlags = X509KeyUsageFlags.DigitalSignature;

		// only if certification authentication is used
		var enhancedKeyUsages = new OidCollection
		{
			OidLookup.ClientAuthentication,
			// OidLookup.ServerAuthentication 
			// OidLookup.CodeSigning,
			// OidLookup.SecureEmail,
			// OidLookup.TimeStamping  
		};

		var certificate = _cc.NewRsaSelfSignedCertificate(
			new DistinguishedName { CommonName = dnsName },
			basicConstraints,
			new ValidityPeriod
			{
				ValidFrom = DateTimeOffset.UtcNow,
				ValidTo = DateTimeOffset.UtcNow.AddYears(validityPeriodInYears)
			},
			subjectAlternativeName,
			enhancedKeyUsages,
			x509KeyUsageFlags,
			new RsaConfiguration
			{ 
				KeySize = 3072,
				HashAlgorithmName = HashAlgorithmName.SHA512
			});

		return certificate;
	}
}

Running the applications

I setup two user flows for running and testing the applications. One is using ngrok and local development with basic authentication. The second is using certification authentication and the deployed Azure App service. I published the API to the App service and run the UI application. When the user signs in, the API connector is used to get the extra custom claims from the deployed API and is returned.

Links:

https://docs.microsoft.com/en-us/azure/active-directory-b2c/api-connectors-overview?pivots=b2c-user-flow

https://docs.microsoft.com/en-us/azure/active-directory-b2c/

https://github.com/Azure-Samples/active-directory-dotnet-external-identities-api-connector-azure-function-validate/

https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-customize-properties?pivots=dotnet-6-0

https://github.com/AzureAD/microsoft-identity-web/wiki

4 comments

  1. […] Implement certificate authentication in ASP.NET Core for an Azure B2C API connector (Damien Bowden) […]

  2. […] Implement certificate authentication in ASP.NET Core for an Azure B2C API connector (Damien Bowden) […]

  3. […] best practices (Anna Monus) Kubernetes Cluster API v1.0, Production Ready (Aditya Kulkarni) Implement certificate authentication in ASP.NET Core for an Azure B2C API connector (Damien Bowden) Use AzCopy to migrate files from AWS S3 to Azure Storage (Dave Brock) Rethink […]

  4. […] Implement certificate authentication in ASP.NET Core for an Azure B2C API connector – Damien Bowden […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

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

%d bloggers like this: