I implemented a dotnet core Api where endpoints are defined based on the Controller attribute Route. I have for example 2 endpoints api/controller1 and api/controller2
I want to configure IIS so a client certificate is ignored for controller1 and required for controller2. In my Api, I implemented the host this way
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureKestrel(o =>
{
o.ConfigureHttpsDefaults(o=>o.ClientCertificateMode=ClientCertificateMode.AllowCertificate);
})
.UseIISIntegration()
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.SetMinimumLevel(LogLevel.Debug);
})
.UseNLog();
and configured services
services.AddSingleton<CertificateValidationService>();
services.Configure<IISOptions>(options =>
{
options.ForwardClientCertificate = true;
});
services.AddAuthentication()
.AddCertificate(x =>
{
x.AllowedCertificateTypes = CertificateTypes.All;
x.ValidateValidityPeriod = true;
x.RevocationMode = X509RevocationMode.NoCheck;
x.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
_logger.Trace("Enters OnCertificateValidated");
var validationService =
context.HttpContext.RequestServices.GetService<CertificateValidationService>();
if (validationService.ValidateCertificate(context.ClientCertificate))
{
_logger.Trace("OnCertificateValidated success");
context.Success();
}
else
{
_logger.Trace("OnCertificateValidated fail");
context.Fail("invalid certificate");
}
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
_logger.Trace("Enters OnAuthenticationFailed");
context.Fail("invalid certificate");
return Task.CompletedTask;
}
};
});
Here is the middleware pipeline configuration in Configure method of Startup.cs
if (env.IsLocal())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler(appBuilder =>
{
appBuilder.Use(async (context, next) =>
{
var error = context.Features[typeof(IExceptionHandlerFeature)] as IExceptionHandlerFeature;
if (error != null && error.Error is SecurityTokenExpiredException)
{
_logger.Warn($"No valid token provided. {error.Error.Message}");
context.Response.StatusCode = 401;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(JsonConvert.SerializeObject(new
{
IpUrl = _globalSettings.IdP.Url,
SpName = _globalSettings.IdP.Name,
Authenticate = context.Request.GetEncodedUrl(),
//State = 401,
Msg = "Token expired"
}));
}
else if (error?.Error != null)
{
_logger.Error($"Unexpected error - {error.Error.Message}");
context.Response.StatusCode = 500;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(JsonConvert.SerializeObject(new
{
State = 500,
Msg = error.Error.Message
}));
}
else
{
await next();
}
});
});
// 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.UseCors("AllowOrigin");
app.UseAuthentication();
app.UseAuthorization();
app.UseSwagger(SwaggerHelper.ConfigureSwagger);
app.UseSwaggerUI(SwaggerHelper.ConfigureSwaggerUi);
app.UseEndpoints(endpoints => endpoints.MapControllers());
I tried to use web.config location but the "path" api/controller2 doesn't not actually exists (it's routed) so it has no effect
I created in the app folder faked api/controller2 folders to setup the SSL requirement on it. Unfortunately, I get a 405 because I lose then the routing and there's nothing behind those folders.
The only way I have yet is to "accept" a certificate at the api application level. But then, my front end, as soon as it queries for the first time my API asks for a certificate when it uses only api/controller1
Is there a way or do I have to build and deploy a specific API to have it protected and the other one for not using client certificate ?
Unfortunatelly this is not possible. Certificate validation happens on TLS level, i.e. before the actual request gets to ASP.NET core, so you cannot distinguish by route. It fails even before you could implement such logic.
We had a similar problem and we had to set up two applications, one with certificate validation and one without. The one with certificate validation than called the other app with "normal" (JWT machine-to-machine in our case) authentication and passed certificate parameters along.
This is official docu that states this:
Can I configure my app to require a certificate only on certain paths? This isn't possible. Remember the certificate exchange is done at the start of the HTTPS conversation, it's done by the server before the first request is received on that connection so it's not possible to scope based on any request fields.