Cómo usar un certificado de cliente para autenticar y autorizar en una API web

Estoy tratando de usar un certificado de cliente para autenticar y autorizar dispositivos usando una API web y desarrollé una prueba de concepto simple para resolver problemas con la posible solución. Me encuentro con un problema en el que la aplicación web no recibe el certificado del cliente. Varias personas informaron este problema,incluido en estas preguntas y respuestas, pero ninguno de ellos tiene una respuesta. Espero proporcionar más detalles para revivir este problema y espero obtener una respuesta para mi problema. Estoy abierto a otras soluciones. El requisito principal es que un proceso independiente escrito en C # pueda llamar a una API web y autenticarse mediante un certificado de cliente.

La API web en este POC es muy simple y solo devuelve un valor único. Utiliza un atributo para validar que se usa HTTPS y que hay un certificado de cliente.

public class SecureController : ApiController
{
    [RequireHttps]
    public string Get(int id)
    {
        return "value";
    }

}

Aquí está el código para RequireHttpsAttribute:

public class RequireHttpsAttribute : AuthorizationFilterAttribute 
{ 
    public override void OnAuthorization(HttpActionContext actionContext) 
    { 
        if (actionContext.Request.RequestUri.Scheme != Uri.UriSchemeHttps) 
        { 
            actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden) 
            { 
                ReasonPhrase = "HTTPS Required" 
            }; 
        } 
        else 
        {
            var cert = actionContext.Request.GetClientCertificate();
            if (cert == null)
            {
                actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
                {
                    ReasonPhrase = "Client Certificate Required"
                }; 

            }
            base.OnAuthorization(actionContext); 
        } 
    } 
}

En este POC solo estoy verificando la disponibilidad del certificado del cliente. Una vez que esto funciona, puedo agregar comprobaciones de información en el certificado para validar con una lista de certificados.

Aquí está la configuración en IIS para SSL para esta aplicación web.

Aquí está el código para el cliente que envía la solicitud con un certificado de cliente. Esta es una aplicación de consola.

    private static async Task SendRequestUsingHttpClient()
    {
        WebRequestHandler handler = new WebRequestHandler();
        X509Certificate certificate = GetCert("ClientCertificate.cer");
        handler.ClientCertificates.Add(certificate);
        handler.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(ValidateServerCertificate);
        handler.ClientCertificateOptions = ClientCertificateOption.Manual;
        using (var client = new HttpClient(handler))
        {
            client.BaseAddress = new Uri("https://localhost:44398/");
            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            HttpResponseMessage response = await client.GetAsync("api/Secure/1");
            if (response.IsSuccessStatusCode)
            {
                string content = await response.Content.ReadAsStringAsync();
                Console.WriteLine("Received response: {0}",content);
            }
            else
            {
                Console.WriteLine("Error, received status code {0}: {1}", response.StatusCode, response.ReasonPhrase);
            }
        }
    }

    public static bool ValidateServerCertificate(
      object sender,
      X509Certificate certificate,
      X509Chain chain,
      SslPolicyErrors sslPolicyErrors)
    {
        Console.WriteLine("Validating certificate {0}", certificate.Issuer);
        if (sslPolicyErrors == SslPolicyErrors.None)
            return true;

        Console.WriteLine("Certificate error: {0}", sslPolicyErrors);

        // Do not allow this client to communicate with unauthenticated servers.
        return false;
    }

Cuando ejecuto esta aplicación de prueba, obtengo un código de estado de 403 Prohibido con una frase de razón de "Certificado de Cliente Requerido" que indica que está entrando en mi RequireHttpsAttribute y no está encontrando ningún certificado de cliente. Al ejecutar esto a través de un depurador, verifiqué que el certificado se está cargando y agregando a WebRequestHandler. El certificado se exporta a un archivo CER que se está cargando. El certificado completo con la clave privada se encuentra en los almacenes personales y de confianza de la máquina local para el servidor de aplicaciones web. Para esta prueba, el cliente y la aplicación web se ejecutan en la misma máquina.

Puedo llamar a este método de API web usando Fiddler, adjuntando el mismo certificado de cliente, y funciona bien. Al usar Fiddler, pasa las pruebas en RequireHttpsAttribute y devuelve un código de estado exitoso de 200 y devuelve el valor esperado.

¿Alguien se ha encontrado con el mismo problema donde HttpClient no envía un certificado de cliente en la solicitud y encuentra una solución?

Actualización 1:

También intenté obtener el certificado del almacén de certificados que incluye la clave privada. Así es como lo recuperé:

    private static X509Certificate2 GetCert2(string hostname)
    {
        X509Store myX509Store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
        myX509Store.Open(OpenFlags.ReadWrite);
        X509Certificate2 myCertificate = myX509Store.Certificates.OfType<X509Certificate2>().FirstOrDefault(cert => cert.GetNameInfo(X509NameType.SimpleName, false) == hostname);
        return myCertificate;
    }

Verifiqué que este certificado se estaba recuperando correctamente y se estaba agregando a la colección de certificados del cliente. Pero obtuve los mismos resultados donde el código del servidor no recupera ningún certificado de cliente.

Para completar, aquí está el código utilizado para recuperar el certificado de un archivo:

    private static X509Certificate GetCert(string filename)
    {
        X509Certificate Cert = X509Certificate.CreateFromCertFile(filename);
        return Cert;

    }

Notará que cuando obtiene el certificado de un archivo, devuelve un objeto del tipo X509Certificate y cuando lo recupera del almacén de certificados es del tipo X509Certificate2. El método X509CertificateCollection.Add espera un tipo de X509Certificate.

Actualización 2: Todavía estoy tratando de resolver esto y he probado muchas opciones diferentes, pero fue en vano.

Cambié la aplicación web para que se ejecute con un nombre de host en lugar de un host local.Configuré la aplicación web para que requiera SSLVerifiqué que el certificado se configuró para la autenticación del cliente y que está en la raíz de confianzaAdemás de probar el certificado del cliente en Fiddler, también lo validé en Chrome.

En un momento durante la prueba de estas opciones, comenzó a funcionar. Luego comencé a retroceder los cambios para ver qué causaba que funcionara. Siguió funcionando. Luego intenté eliminar el certificado de la raíz de confianza para validar que se requería y dejó de funcionar y ahora no puedo volver a hacerlo, aunque volví a colocar el certificado en la raíz de confianza. Ahora Chrome ni siquiera me pedirá un certificado como el que usó también y falla en Chrome, pero aún funciona en Fiddler. Debe haber alguna configuración mágica que me falta.

También intenté habilitar "Negociar certificado de cliente" en el enlace, pero Chrome todavía no me pedirá un certificado de cliente. Aquí está la configuración usando "netsh http show sslcert"

 IP:port                 : 0.0.0.0:44398
 Certificate Hash        : 429e090db21e14344aa5d75d25074712f120f65f
 Application ID          : {4dc3e181-e14b-4a21-b022-59fc669b0914}
 Certificate Store Name  : MY
 Verify Client Certificate Revocation    : Disabled
 Verify Revocation Using Cached Client Certificate Only    : Disabled
 Usage Check    : Enabled
 Revocation Freshness Time : 0
 URL Retrieval Timeout   : 0
 Ctl Identifier          : (null)
 Ctl Store Name          : (null)
 DS Mapper Usage    : Disabled
 Negotiate Client Certificate    : Enabled

Aquí está el certificado del cliente que estoy usando:

Estoy desconcertado sobre cuál es el problema. Estoy agregando una recompensa por cualquiera que pueda ayudarme a resolver esto.

Respuestas a la pregunta(5)

Su respuesta a la pregunta