'How to configure IIS to define Client Certificate required on a specific endpoint when routing make endpoint path different from physical path

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 ?



Solution 1:[1]

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.

Solution 2:[2]

I have a similar issue. And I found a solution for iis express. I think for iis it is solved similarly, I will write about it later (if it's will work).

But about solution (starting terms):

  1. I'am on stage testing from visual studio, and i run net core app under iis express integrated in VS.
  2. For my solution i need to request user certificate when user go to url '/certificate/apply/' (only on this page).
  3. Project name is 'TestCore'

Steps:

  1. In visual studio project folder you need to finde hidden folder .vs and in this folder you need to find folder 'config' and file 'applicationhost.config'

  2. In this file you need to find similar as below section with you project configuration:

       <location path="TestCore" inheritInChildApplications="false">
     <system.webServer>
       <modules>
         <remove name="WebMatrixSupportModule" />
       </modules>
       <handlers>
         <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
       </handlers>
       <aspNetCore processPath="%LAUNCHER_PATH%" stdoutLogEnabled="false" hostingModel="InProcess" startupTimeLimit="3600" requestTimeout="23:00:00" />
       <httpCompression>
         <dynamicTypes>
           <add mimeType="text/event-stream" enabled="false" />
         </dynamicTypes>
       </httpCompression>
     </system.webServer>
    
  3. clone (copy - paste) this section in file and modify copy (change path and add sequryti section):

     <location path="TestCore/certificate/apply" inheritInChildApplications="false">
     <system.webServer>
         <modules>
             <remove name="WebMatrixSupportModule" />
         </modules>
         <handlers>
             <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
         </handlers>
         <aspNetCore processPath="%LAUNCHER_PATH%" stdoutLogEnabled="false" hostingModel="InProcess" startupTimeLimit="3600" requestTimeout="23:00:00" />
         <httpCompression>
             <dynamicTypes>
                 <add mimeType="text/event-stream" enabled="false" />
             </dynamicTypes>
         </httpCompression>
         <security>
             <access sslFlags="SslNegotiateCert" />
         </security>
     </system.webServer>
    
  4. try to start project (for mee it works fine).

I hope i (or some body else) will find same way for IIS.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Maxim Zabolotskikh
Solution 2 Igor Dyubin