web-dev-qa-db-fra.com

Comment obtenir que HttpClient transmette les informations d'identification avec la demande?

J'ai une application Web (hébergée dans IIS) qui communique avec un service Windows. Le service Windows utilise l'API Web ASP.Net MVC (auto-hébergé) et peut donc être communiqué via http à l'aide de JSON. L'application Web est configurée pour effectuer l'emprunt d'identité, l'idée étant que l'utilisateur qui fait la demande à l'application Web devrait être l'utilisateur que l'application Web utilise pour faire la demande au service. La structure ressemble à ceci:

(L'utilisateur surligné en rouge est l'utilisateur auquel il est fait référence dans les exemples ci-dessous.)


L'application Web envoie des demandes au service Windows à l'aide de HttpClient :

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

La demande est envoyée au service Windows, mais les informations d'identification ne sont pas transmises correctement (le service signale l'utilisateur sous la forme IIS APPPOOL\ASP.NET 4.0). Ce n'est pas ce que je veux arriver .

Si je modifie le code ci-dessus pour utiliser un WebClient , les informations d'identification de l'utilisateur sont correctement transmises:

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

Avec le code ci-dessus, le service signale l'utilisateur comme l'utilisateur qui a effectué la demande auprès de l'application Web.

Qu'est-ce que je fais de mal avec l'implémentation HttpClient qui l'empêche de transmettre correctement les informations d'identification (ou s'agit-il d'un bogue avec HttpClient)?

La raison pour laquelle je souhaite utiliser HttpClient est que son API asynchrone fonctionne bien avec Tasks, alors que l'API asyc de WebClient doit être gérée avec des événements.

144
adrianbanks

J'avais aussi ce même problème. J'ai développé une solution synchrone grâce aux recherches effectuées par @tpeczek dans l'article SO suivant: Impossible de s'authentifier auprès du service ASP.NET Web Api avec HttpClient

Ma solution utilise un WebClient, qui, comme vous l'avez indiqué correctement, transmet les informations d'identification sans problème. La raison pour laquelle HttpClient ne fonctionne pas, c'est que la sécurité de Windows a désactivé la possibilité de créer de nouveaux threads sous un compte emprunté (voir l'article SO ci-dessus.) HttpClient crée de nouveaux threads via le Task Factory provoquant ainsi l'erreur. WebClient en revanche, s'exécute de manière synchrone sur le même thread en contournant ainsi la règle et en transmettant ses informations d'identification.

Bien que le code fonctionne, l'inconvénient est qu'il ne fonctionnera pas de manière asynchrone.

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();
}

Remarque: Nécessite le package NuGet: Newtonsoft.Json, identique au sérialiseur JSON utilisé par WebAPI.

58
Joshua

Vous pouvez configurer HttpClient pour qu'il passe automatiquement les informations d'identification comme ceci:

myClient = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true })
109
Sean

Ce que vous essayez de faire est de faire en sorte que NTLM transmette l’identité au prochain serveur, ce qu’il ne peut pas faire. Il ne peut effectuer qu’une usurpation d’identité qui ne vous donne accès qu’aux ressources locales. Cela ne vous laissera pas dépasser une machine. L'authentification Kerberos prend en charge la délégation (ce dont vous avez besoin) à l'aide de tickets. Le ticket peut être transféré lorsque tous les serveurs et applications de la chaîne sont correctement configurés et que Kerberos est configuré correctement sur le domaine. En bref, vous devez passer de NTLM à Kerberos.

Pour plus d'informations sur les options d'authentification Windows disponibles et sur leur fonctionnement, commencez à l'adresse suivante: http://msdn.Microsoft.com/en-us/library/ff647076.aspx

24
BlackSpy

OK, merci à tous les contributeurs ci-dessus. J'utilise .NET 4.6 et nous avons également eu le même problème. J'ai passé du temps à déboguer System.Net.Http, en particulier le HttpClientHandler, et j'ai trouvé ce qui suit:

    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);
    }

Ainsi, après avoir évalué que la ExecutionContext.IsFlowSuppressed() était peut-être le coupable, j'ai enveloppé notre code d'emprunt d'identité comme suit:

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

Le code à l'intérieur de SafeCaptureIdenity (pas ma faute d'orthographe), saisit WindowsIdentity.Current() qui est notre identité imitée. Ceci est pris en compte parce que nous supprimons maintenant le flux. En raison de l'utilisation/dispose, il est réinitialisé après l'appel.

Cela semble maintenant fonctionner pour nous, ouf!

11
Chullybun

Dans .NET Core, j'ai réussi à obtenir un System.Net.Http.HttpClient avec UseDefaultCredentials = true à transmettre aux informations d'identification Windows de l'utilisateur authentifié à un service dorsal à l'aide de 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);
    });
}
6
scourge192

Cela a fonctionné pour moi après avoir configuré un utilisateur avec un accès Internet dans le service Windows.

Dans mon code:

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

Ok, j'ai pris le code Joshoun et l'ai rendu générique. Je ne suis pas sûr de devoir implémenter un modèle singleton sur la classe SynchronousPost. Peut-être que quelqu'un de plus intelligent peut aider.

La mise en oeuvre

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

Classe générique ici. Vous pouvez passer n'importe quel type

 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; }
    }

Mes cours Api ressemblent à ceci, si vous êtes curieux

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();
    }
}

J'utilise ninject, et repo pattern avec unité de travail. Quoi qu'il en soit, la classe générique ci-dessus aide vraiment.

3
hidden