Click here to Skip to main content
16,022,234 members
Please Sign up or sign in to vote.
0.00/5 (No votes)
See more: , +
I have tried to implement Certificate Authentication against my RESTful web services exactly as prescribed by Microsoft in the article at Configure certificate authentication in ASP.NET Core | Microsoft Learn. Unfortunately, when I perform my own custom validation of the certificate and call
C#
context.Fail("Fail reason");
then nothing happens. The API authenticates the request because the selected certificate is a vaild chained certificate. It just does not meet my custom validation requirements. How can I get authentication to fail when I perform my own custom vaildation?

What I have tried:

I have tried the following:

C#
builder.Services.AddAuthentication(
        CertificateAuthenticationDefaults.AuthenticationScheme)
    .AddCertificate(options =>
    {
        options.Events = new CertificateAuthenticationEvents
        {
            OnCertificateValidated = context =>
            {
                var validationService = context.HttpContext.RequestServices
                    .GetRequiredService<ICertificateValidationService>();

                if (!validationService.ValidateCertificate(context.ClientCertificate))
                {
                    context.Principal = null;

                    context.Fail("Invalid certificate");
                }
                else
                {
                    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();
                }

                return Task.CompletedTask;
            }
        };
    });


And here is the ValidateCertificate method implementation:

C#
public bool ValidateCertificate(X509Certificate2 clientCertificate)
		{
			if (clientCertificate == null!)
			{
				var error = new ConfigurationErrorsException("Missing certificate or certificate not sent by client.");

				Logger.Log.Error(error, "Certificate validation failed. Missing certificate or certificate not sent by client.");

				throw error;
			}

			if (_ClientCertificateSettings == null! || _ClientCertificateSettings.AllowedCertificates == null! || !_ClientCertificateSettings.AllowedCertificates.ToList().Any())
			{
				var error = new ConfigurationErrorsException("Certificate configuration missing. Check AppSettings.");

				Logger.Log.Error(error, "Certificate validation failed. Certificate configuration missing. Check AppSettings.");

				throw error;
			}

			if (_ClientCertificateSettings.AllowedCertificates.Any(cert => string.IsNullOrEmpty(cert.Subject)))
			{
				var error = new ConfigurationErrorsException("Certificate configuration missing Subject. Check AppSettings.");

				Logger.Log.Error(error, "Certificate validation failed. Certificate configuration missing Subject. Check AppSettings.");

				throw error;
			}

			if (_ClientCertificateSettings.AllowedCertificates.Any(cert => string.IsNullOrEmpty(cert.Issuer)))
			{
				var error = new ConfigurationErrorsException("Certificate configuration missing Subject. Check AppSettings.");

				Logger.Log.Error(error, "Certificate validation failed. Certificate configuration missing Issuer. Check AppSettings.");

				throw error;
			}

			// 1. Check time validity of certificate.
			if (DateTime.Compare(DateTime.UtcNow, clientCertificate.NotBefore) < 0 || DateTime.Compare(DateTime.UtcNow, clientCertificate.NotAfter) > 0)
			{
				Logger.Log.Warning($"Certificate with thumbprint {clientCertificate.Thumbprint} is expired.");

				return false;
			}

			// 2. Check subject name of certificate.
			var foundSubject = false;

			var certSubjectData = clientCertificate.Subject.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);

			if (certSubjectData.Any(certSubject => _ClientCertificateSettings.AllowedCertificates.Any(cert => cert.Subject.Equals(certSubject.Trim(), StringComparison.InvariantCultureIgnoreCase))))
			{
				foundSubject = true;
			}

			if (!foundSubject)
			{
				Logger.Log.Warning($"Certificate with thumbprint {clientCertificate.Thumbprint} does not have a matching Subject.");

				return false;
			}

			// 3. Check issuer name of certificate.
			var certIssuerData = clientCertificate.Issuer.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);

			var foundIssuer = certIssuerData.Any(issuerData => _ClientCertificateSettings.AllowedCertificates.Any(cert => cert.Issuer.Equals(issuerData.Trim(), StringComparison.InvariantCultureIgnoreCase)));

			if (!foundIssuer)
			{
				Logger.Log.Warning($"Certificate with thumbprint {clientCertificate.Thumbprint} does not have a matching Issuer.");

				return false;
			}

			// Check if the Certificate exists in the personal cert store.
			if (_ClientCertificateSettings.CheckThumbprintInCertStore)
			{
				var store = new X509Store(StoreName.My, StoreLocation.LocalMachine);

				try
				{
					store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);

					var certs = store.Certificates.Find(X509FindType.FindByThumbprint, clientCertificate.Thumbprint, true);

					if (certs.Count == 0)
					{
						Logger.Log.Warning("Invalid client certificate. The thumbprint does not match with a certificate in the certificate store. {@clientCertificate}", clientCertificate);

						return false;
					}
				}
				catch (Exception ex)
				{
					Logger.Log.Error(ex, "An exception occurred searching for the client certificate in the certificate store. {@clientCertificate}", clientCertificate);

					throw;
				}
				finally
				{
					store.Close();

					store.Dispose();
				}
			}

			return true;
		}
Posted
Updated 2-Oct-24 9:51am
v2
Comments
Pete O'Hanlon 2-Oct-24 11:23am    
What does your ValidateCertificate implementation look like? What is it doing that will trigger it to return false?
Mat Hamilton 2-Oct-24 15:49pm    
ValidateCertificate looks at the issuer and subject of the client certificate to match them against a list of valid issuers and subject combinations. IT does some other validations as well. I will add the actual implementation to the question above.
Pete O'Hanlon 3-Oct-24 3:08am    
If you are seeing failure conditions, your code would be writing it to the logs. What do you see in your logs?
Mat Hamilton 3-Oct-24 11:24am    
The logs contain the failure conditions that I expect to see. The problem is context.Fail("Fail reason"); is not doing anything. I even tried to set the principal to null before calling fail. That did not change the outcome. I just need to know how to get the authentication to actually fail when I call context.Fail...
Mat Hamilton 3-Oct-24 11:27am    
More specifically, the logs contain the error message, "Certificate with thumbprint xyz does not have a matching Subject."

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900