When a third party tries to call my API endpoint with the certificate in .cer format, which I exported from the .pfx file and sent to them. They will get 403 - Forbidden: Access is denied. You do not have permission to view this directory or page using the credentials that you supplied.
I investigate what could be caused this problem.
- When i Install/import Certificate in .cer format in certificate store under Personal and than i try to call my endpoint i can see my certificate is NOT on list of certificates and than i hit ok button i will get 403 - Forbidden: Access is denied.
BUT
- When i Install/import Certificate in .pfx format with passepharse in certificate store under Personal and then i try to call my endpoint on browser and also with postman THIS time i can see my certificate in the list of certificates on browser and than i choose the certificate and hit button i will successfully coming into directory and i also get 200 ok response in postman with ofcourse add certificate in .pfx format in postman.
And im confused now 3rd party only accept Client Certificate in .cer format and as i understand .pfx is for inside organization and not for outside organization.
** I should note my Client Certificate contains no Private Key its only contains Public key.
** I'm sure All configuration on server and IIS it is correct.
** I'm not sure how to add private key into my Client certificate with .cer format! or should i ?!
Did i missed something here ! its been 4 days working on this but still no luck :(
Can anyone please help me or point me into right direction! thanks :)
This is how i get Client certificate in ASP.NET Core 3.1:
MyCertificateValidationService.cs
I compared the client certificate I have with the client certificate I get from the request:
public class MyCertificateValidationService
{
public bool ValidateCertificate(X509Certificate2 clientCertificate)
{
try
{
var _path = @"c:\temp\ClientCertification.cer";
var cert2 = new X509Certificate2(File.ReadAllBytes(_path));
if (clientCertificate.Thumbprint == cert2.Thumbprint)
{
return true;
}
}
catch (System.Exception)
{
throw;
}
return false;
}
My API Endpoint:
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
[Consumes("application/xml")]
[Produces("application/xml")]
[ProducesResponseType(typeof(DespatchAdvice), (int)HttpStatusCode.OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesDefaultResponseType]
[HttpPost("SendDespatch")]
public IActionResult SendDespatch([FromBody] DespatchAdvice despatches)
{
//do something
}
}
Startup.cs
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<MyCertificateValidationService>();
services.AddSingleton<MailHandler>();
services.AddScoped<IDespatch, DespatchRepo>();
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options => // code from ASP.NET Core sample
{
// https://docs.microsoft.com/en-us/aspnet/core/security/authentication/certauth
options.AllowedCertificateTypes = CertificateTypes.All;
//options.RevocationMode = X509RevocationMode.NoCheck;
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
var validationService =
context.HttpContext.RequestServices.GetService<MyCertificateValidationService>();
if (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;
}
};
});
services.AddAuthorization();
services.AddCertificateForwarding(options =>
{
options.CertificateHeader = "X-ARR-ClientCert";
options.HeaderConverter = (headerValue) =>
{
X509Certificate2 clientCertificate = null;
if (!string.IsNullOrWhiteSpace(headerValue))
{
byte[] bytes = StringToByteArray(headerValue);
clientCertificate = new X509Certificate2(bytes);
}
return clientCertificate;
};
});
services.AddControllers().AddXmlSerializerFormatters();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseCertificateForwarding();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
private static byte[] StringToByteArray(string hex)
{
int NumberChars = hex.Length;
byte[] bytes = new byte[NumberChars / 2];
for (int i = 0; i < NumberChars; i += 2)
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
return bytes;
}
Program.cs
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args)
=> WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureKestrel(options =>
{
var cert = new X509Certificate2(Path.Combine("cert.pfx"), "password");
options.ConfigureHttpsDefaults(o =>
{
o.ServerCertificate = cert;
o.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
});
})
.Build();
}