¿Cómo hacer que HttpClient pase credenciales junto con la solicitud?

164

Tengo una aplicación web (alojada en IIS) que habla con un servicio de Windows. El servicio de Windows está utilizando la API web ASP.Net MVC (autohospedado), por lo que se puede comunicar a través de http utilizando JSON. La aplicación web está configurada para suplantar, la idea es que el usuario que realiza la solicitud a la aplicación web debe ser el usuario que la aplicación web utiliza para realizar la solicitud al servicio. La estructura se ve así:

(El usuario resaltado en rojo es el usuario al que se hace referencia en los ejemplos a continuación).


La aplicación web realiza solicitudes al servicio de Windows utilizando un HttpClient:

var httpClient = new HttpClient(new HttpClientHandler() 
                      {
                          UseDefaultCredentials = true
                      });
httpClient.GetStringAsync("http://localhost/some/endpoint/");

Esto realiza la solicitud al servicio de Windows, pero no pasa las credenciales correctamente (el servicio informa al usuario como IIS APPPOOL\ASP.NET 4.0). Esto no es lo que quiero que suceda .

Si cambio el código anterior para usar un WebClienten su lugar, las credenciales del usuario se pasan correctamente:

WebClient c = new WebClient
                   {
                       UseDefaultCredentials = true
                   };
c.DownloadStringAsync(new Uri("http://localhost/some/endpoint/"));

Con el código anterior, el servicio informa al usuario como el usuario que realizó la solicitud a la aplicación web.

¿Qué estoy haciendo mal con la HttpClientimplementación que hace que no pase las credenciales correctamente (o es un error con el HttpClient)?

La razón por la que quiero usar HttpClientes que tiene una API asíncrona que funciona bien con Tasks, mientras que la WebClientAPI asíncrona debe manejarse con eventos.

adrianbanks
fuente
Posible duplicado de stackoverflow.com/q/10308938/1045728
Tommy Grovnes
Parece que HttpClient y WebClient consideran diferentes cosas como DefaultCredentials. ¿Has probado HttpClient.setCredentials (...)?
Germann Arlington
Por cierto, WebClient tiene DownloadStringTaskAsyncen .Net 4.5, que también se puede usar con async / await
LB
1
@GermannArlington: HttpClientno tiene un SetCredentials()método. ¿Puedes señalarme a qué te refieres?
adrianbanks
44
Parece que esto se ha solucionado (.net 4.5.1)? Intenté crear new HttpClient(new HttpClientHandler() { AllowAutoRedirect = true, UseDefaultCredentials = true }en un servidor web al que accedía un usuario autenticado de Windows, y el sitio web se autenticó para otro recurso remoto después de eso (no se autenticaría sin el indicador establecido).
GSerg

Respuestas:

67

También estaba teniendo este mismo problema. Desarrollé una solución síncrona gracias a la investigación realizada por @tpeczek en el siguiente artículo de SO: No se puede autenticar al servicio ASP.NET Web Api con HttpClient

Mi solución utiliza un WebClient, que como notó correctamente, pasa las credenciales sin problemas. La razón por la HttpClientque no funciona es porque la seguridad de Windows deshabilita la capacidad de crear nuevos subprocesos bajo una cuenta suplantada (consulte el artículo SO anterior). HttpClientCrea nuevos subprocesos a través de la Fábrica de tareas, lo que causa el error. WebClientpor otro lado, se ejecuta sincrónicamente en el mismo hilo, evitando así la regla y reenviando sus credenciales.

Aunque el código funciona, la desventaja es que no funcionará de forma asíncrona.

var wi = (System.Security.Principal.WindowsIdentity)HttpContext.Current.User.Identity;

var wic = wi.Impersonate();
try
{
    var data = JsonConvert.SerializeObject(new
    {
        Property1 = 1,
        Property2 = "blah"
    });

    using (var client = new WebClient { UseDefaultCredentials = true })
    {
        client.Headers.Add(HttpRequestHeader.ContentType, "application/json; charset=utf-8");
        client.UploadData("http://url/api/controller", "POST", Encoding.UTF8.GetBytes(data));
    }
}
catch (Exception exc)
{
    // handle exception
}
finally
{
    wic.Undo();
}

Nota: Requiere el paquete NuGet: Newtonsoft.Json, que es el mismo serializador JSON que utiliza WebAPI.

Joshua
fuente
1
Al final hice algo similar y funciona muy bien. El problema asincrónico no es un problema, ya que quiero bloquear las llamadas.
adrianbanks
136

Puede configurar HttpClientpara pasar automáticamente credenciales como esta:

var myClient = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true });
Sean
fuente
11
Yo se como hacer eso. El comportamiento no es lo que quiero (como se indica en la pregunta): "Esto realiza la solicitud al servicio de Windows, pero no pasa las credenciales correctamente (el servicio informa al usuario como IIS APPPOOL \ ASP.NET 4.0). Esto no es lo que quiero que suceda ".
adrianbanks
44
Esto parece solucionar mi problema donde iis solo tiene habilitada la autenticación de Windows. si solo necesita pasar algunas credenciales legítimas, esto debería hacerlo.
Timmerz
No estoy seguro de que funcione igual que WebClient en escenarios de suplantación / delegación. Obtengo "El nombre principal de destino es incorrecto" cuando uso HttpClient con la solución anterior, pero usar WebClient con una configuración similar pasa las credenciales del usuario.
Peder Rice
Esto funcionó para mí y los registros muestran el usuario correcto. Aunque, con doble salto en la imagen, no esperaba que funcionara con NTLM como el esquema de autenticación subyacente, pero funciona.
Nitin Rastogi
¿Cómo hacer lo mismo con la última versión de aspnet core? (2.2) Si alguien sabe ...
Nico
26

Lo que está tratando de hacer es hacer que NTLM reenvíe la identidad al siguiente servidor, lo que no puede hacer: solo puede suplantar lo que solo le da acceso a los recursos locales. No te permitirá cruzar el límite de una máquina. La autenticación Kerberos admite la delegación (lo que necesita) mediante el uso de tickets, y el ticket se puede reenviar cuando todos los servidores y aplicaciones de la cadena están configurados correctamente y Kerberos está configurado correctamente en el dominio. Entonces, en resumen, debe cambiar el uso de NTLM a Kerberos.

Para obtener más información sobre las opciones de autenticación de Windows disponibles y cómo funcionan, comience en: http://msdn.microsoft.com/en-us/library/ff647076.aspx

BlackSpy
fuente
3
" NTLM para reenviar la identidad al siguiente servidor, lo que no puede hacer ". ¿Cómo es que hace esto cuando se usa WebClient? Esto es lo que no entiendo: si no es posible, ¿cómo es que lo está haciendo?
adrianbanks
2
Cuando se usa el cliente web, sigue siendo solo una conexión, entre el cliente y el servidor. Puede suplantar al usuario en ese servidor (1 salto), pero no puede reenviar esas credenciales a otra máquina (2 saltos - cliente a servidor al segundo servidor). Para eso necesitas delegación.
BlackSpy
1
La única forma de lograr lo que intenta hacer de la manera en que lo intenta hacer es que el usuario escriba su nombre de usuario y contraseña en un cuadro de diálogo personalizado en su aplicación ASP.NET, almacénelos como cadenas y luego use ellos para establecer su identidad cuando se conecta a su proyecto de API web. De lo contrario, debe soltar NTLM y pasar a Kerberos, de modo que pueda pasar el ticket de Kerboros al proyecto de API web. Recomiendo leer el enlace que adjunto en mi respuesta original. Lo que está tratando de hacer requiere una sólida comprensión de la autenticación de Windows antes de comenzar.
BlackSpy
2
@BlackSpy: Tengo mucha experiencia con la autenticación de Windows. Lo que estoy tratando de entender es por qué WebClientpueden transmitir las credenciales de NTLM, pero HttpClientno pueden. Puedo lograr esto usando la suplantación de ASP.Net solo, y sin tener que usar Kerberos o almacenar nombres de usuario / contraseñas. Sin embargo, esto solo funciona con WebClient.
adrianbanks
1
Debería ser imposible hacerse pasar por más de 1 salto sin pasar el nombre de usuario y la contraseña como texto. rompe las reglas de suplantación y NTLM no lo permitirá. WebClient le permite saltar 1 salto porque pasa las credenciales y se ejecuta como ese usuario en el cuadro. Si observa los registros de seguridad, verá el inicio de sesión: el usuario inicia sesión en el sistema. No puede ejecutarse como ese usuario desde esa máquina a menos que haya pasado las credenciales como texto y use otra instancia de cliente web para iniciar sesión en el siguiente cuadro.
BlackSpy
17

OK, así que gracias a todos los contribuyentes anteriores. Estoy usando .NET 4.6 y también tuvimos el mismo problema. Pasé tiempo depurando System.Net.Http, específicamente el HttpClientHandler, y encontré lo siguiente:

    if (ExecutionContext.IsFlowSuppressed())
    {
      IWebProxy webProxy = (IWebProxy) null;
      if (this.useProxy)
        webProxy = this.proxy ?? WebRequest.DefaultWebProxy;
      if (this.UseDefaultCredentials || this.Credentials != null || webProxy != null && webProxy.Credentials != null)
        this.SafeCaptureIdenity(state);
    }

Entonces, después de evaluar que el ExecutionContext.IsFlowSuppressed()culpable podría haber sido el culpable, envolví nuestro código de suplantación de la siguiente manera:

using (((WindowsIdentity)ExecutionContext.Current.Identity).Impersonate())
using (System.Threading.ExecutionContext.SuppressFlow())
{
    // HttpClient code goes here!
}

El código dentro de SafeCaptureIdenity(no es mi error de ortografía), agarra WindowsIdentity.Current()cuál es nuestra identidad suplantada. Esto se está recogiendo porque ahora estamos suprimiendo el flujo. Debido al uso / disposición, esto se restablece después de la invocación.

Ahora parece funcionar para nosotros, ¡uf!

Chullybun
fuente
2
Muchas gracias por hacer este análisis. Esto solucionó mi situación también. ¡Ahora mi identidad se transfiere correctamente a la otra aplicación web! Me salvaste horas de trabajo! Me sorprende que no sea mayor en el recuento de garrapatas.
justdan23
¡Solo necesitaba using (System.Threading.ExecutionContext.SuppressFlow())y el problema se resolvió por mí!
ZX9
10

En .NET Core, logré obtener una System.Net.Http.HttpClientcon UseDefaultCredentials = truepara pasar las credenciales de Windows del usuario autenticado a un servicio de fondo mediante el uso WindowsIdentity.RunImpersonated.

HttpClient client = new HttpClient(new HttpClientHandler { UseDefaultCredentials = true } );
HttpResponseMessage response = null;

if (identity is WindowsIdentity windowsIdentity)
{
    await WindowsIdentity.RunImpersonated(windowsIdentity.AccessToken, async () =>
    {
        var request = new HttpRequestMessage(HttpMethod.Get, url)
        response = await client.SendAsync(request);
    });
}
flagelo192
fuente
4

Me funcionó después de configurar un usuario con acceso a Internet en el servicio de Windows.

En mi código:

HttpClientHandler handler = new HttpClientHandler();
handler.Proxy = System.Net.WebRequest.DefaultWebProxy;
handler.Proxy.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
.....
HttpClient httpClient = new HttpClient(handler)
.... 
Paulo Augusto Batista
fuente
3

Ok, tomé el código de Joshoun y lo hice genérico. No estoy seguro de si debería implementar el patrón singleton en la clase SynchronousPost. Quizás alguien más conocedor pueda ayudar.

Implementación

// Supongo que tienes tu propio tipo concreto. En mi caso, estoy usando el código primero con una clase llamada FileCategory

FileCategory x = new FileCategory { CategoryName = "Some Bs"};
SynchronousPost<FileCategory>test= new SynchronousPost<FileCategory>();
test.PostEntity(x, "/api/ApiFileCategories"); 

Clase genérica aquí. Puedes pasar cualquier tipo

 public class SynchronousPost<T>where T :class
    {
        public SynchronousPost()
        {
            Client = new WebClient { UseDefaultCredentials = true };
        }

        public void PostEntity(T PostThis,string ApiControllerName)//The ApiController name should be "/api/MyName/"
        {
            //this just determines the root url. 
            Client.BaseAddress = string.Format(
         (
            System.Web.HttpContext.Current.Request.Url.Port != 80) ? "{0}://{1}:{2}" : "{0}://{1}",
            System.Web.HttpContext.Current.Request.Url.Scheme,
            System.Web.HttpContext.Current.Request.Url.Host,
            System.Web.HttpContext.Current.Request.Url.Port
           );
            Client.Headers.Add(HttpRequestHeader.ContentType, "application/json;charset=utf-8");
            Client.UploadData(
                                 ApiControllerName, "Post", 
                                 Encoding.UTF8.GetBytes
                                 (
                                    JsonConvert.SerializeObject(PostThis)
                                 )
                             );  
        }
        private WebClient Client  { get; set; }
    }

Mis clases de Api se ven así, si tienes curiosidad

public class ApiFileCategoriesController : ApiBaseController
{
    public ApiFileCategoriesController(IMshIntranetUnitOfWork unitOfWork)
    {
        UnitOfWork = unitOfWork;
    }

    public IEnumerable<FileCategory> GetFiles()
    {
        return UnitOfWork.FileCategories.GetAll().OrderBy(x=>x.CategoryName);
    }
    public FileCategory GetFile(int id)
    {
        return UnitOfWork.FileCategories.GetById(id);
    }
    //Post api/ApileFileCategories

    public HttpResponseMessage Post(FileCategory fileCategory)
    {
        UnitOfWork.FileCategories.Add(fileCategory);
        UnitOfWork.Commit(); 
        return new HttpResponseMessage();
    }
}

Estoy usando ninject y repo pattern con unidad de trabajo. De todos modos, la clase genérica anterior realmente ayuda.

oculto
fuente