Comment faire en sorte que HttpClient transmette les informations d'identification avec la demande?

164

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ée) et peut donc être communiqué via http à l'aide de JSON. L'application Web est configurée pour effectuer une usurpation d'identité, l'idée étant que l'utilisateur qui fait la demande à l'application Web doit ê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 d'un HttpClient:

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

Cela fait la demande au service Windows, mais ne transmet pas correctement les informations d'identification (le service signale l'utilisateur comme IIS APPPOOL\ASP.NET 4.0). Ce n'est pas ce que je souhaite .

Si je change le code ci-dessus pour utiliser à la WebClientplace, les informations d'identification de l'utilisateur sont transmises correctement:

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 fait la demande à l'application Web.

Qu'est-ce que je fais mal avec l' HttpClientimplémentation qui fait qu'elle ne transmet pas correctement les informations d'identification (ou est-ce un bogue avec le HttpClient)?

La raison pour laquelle je veux utiliser l ' HttpClientest qu'il a une API asynchrone qui fonctionne bien avec Tasks, alors que l WebClient' API asyc doit être gérée avec des événements.

Adrianbanks
la source
Copie
Il semble que HttpClient et WebClient considèrent différentes choses comme étant DefaultCredentials. Avez-vous essayé HttpClient.setCredentials (...)?
Germann Arlington
BTW, WebClient a DownloadStringTaskAsyncdans .Net 4.5, qui peut également être utilisé avec async / await
LB
1
@GermannArlington: HttpClientn'a pas de SetCredentials()méthode. Pouvez-vous m'indiquer ce que vous voulez dire?
adrianbanks
4
Il semblerait que cela ait été corrigé (.net 4.5.1)? J'ai essayé de créer new HttpClient(new HttpClientHandler() { AllowAutoRedirect = true, UseDefaultCredentials = true }sur un serveur Web accédé par un utilisateur authentifié par Windows, et le site Web s'est authentifié pour une autre ressource distante après cela (ne s'authentifierait pas sans le jeu d'indicateurs).
GSerg

Réponses:

67

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

Ma solution utilise un WebClient, qui, comme vous l'avez correctement noté, transmet les informations d'identification sans problème. La raison HttpClientne fonctionne pas parce que la sécurité Windows désactive la possibilité de créer de nouveaux threads sous un compte usurpé (voir l'article SO ci-dessus.) HttpClientCrée de nouveaux threads via Task Factory, provoquant ainsi l'erreur. WebClientd'autre part, s'exécute de manière synchrone sur le même thread, contournant ainsi la règle et 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, qui est le même que le sérialiseur JSON que WebAPI utilise.

Joshua
la source
1
J'ai fait quelque chose de similaire à la fin, et cela fonctionne vraiment bien. Le problème asynchrone n'est pas un problème, car je souhaite que les appels soient bloqués.
adrianbanks
136

Vous pouvez configurer HttpClientpour transmettre automatiquement les informations d'identification comme ceci:

var myClient = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true });
Sean
la source
11
Je sais comment faire ca. Le comportement n'est pas ce que je veux (comme indiqué dans la question) - "Cela fait la demande au service Windows, mais ne transmet pas correctement les informations d'identification (le service signale l'utilisateur comme IIS APPPOOL \ ASP.NET 4.0). ce n'est pas ce que je veux. "
adrianbanks
4
cela semble résoudre mon problème où l'authentification Windows est uniquement activée. si vous avez juste besoin de quelques informations d'identification légitimes, cela devrait le faire.
Timmerz
Je ne suis pas sûr que cela fonctionne de la même manière que WebClient dans les scénarios d'emprunt d'identité / délégation. J'obtiens "Le nom du principal cible est incorrect" lors de l'utilisation de HttpClient avec la solution ci-dessus, mais l'utilisation de WebClient avec une configuration similaire transmet les informations d'identification de l'utilisateur.
Peder Rice
Cela a fonctionné pour moi et les journaux montrent l'utilisateur correct. Bien que, avec le double saut dans l'image, je ne m'attendais pas à ce qu'il fonctionne avec NTLM comme schéma d'authentification sous-jacent, mais cela fonctionne.
Nitin Rastogi
Comment faire de même avec la dernière version d'aspnet core? (2,2). Si quelqu'un sait ...
Nico
26

Ce que vous essayez de faire, c'est demander à NTLM de transmettre l'identité au serveur suivant, ce qu'il ne peut pas faire - il ne peut faire que l'emprunt d'identité qui vous donne uniquement accès aux ressources locales. Cela ne vous permettra pas de franchir les limites d'une machine. L'authentification Kerberos prend en charge la délégation (dont vous avez besoin) à l'aide de tickets, et le ticket peut être transféré lorsque tous les serveurs et applications de la chaîne sont correctement configurés et que Kerberos est correctement configuré sur le domaine. Donc, en bref, vous devez passer de NTLM à Kerberos.

Pour en savoir plus sur les options d'authentification Windows disponibles et leur fonctionnement, accédez à: http://msdn.microsoft.com/en-us/library/ff647076.aspx

BlackSpy
la source
3
" NTLM pour transmettre l'identité au serveur suivant, ce qu'il ne peut pas faire " - comment se fait-il qu'il fasse cela lors de l'utilisation WebClient? C'est ce que je ne comprends pas - si ce n'est pas possible, comment se fait-il qu'il le fasse?
adrianbanks
2
Lors de l'utilisation du client Web, il s'agit toujours d'une seule connexion, entre le client et le serveur. Il peut emprunter l'identité de l'utilisateur sur ce serveur (1 saut), mais ne peut pas transmettre ces informations d'identification à une autre machine (2 sauts - client au serveur vers le deuxième serveur). Pour cela, vous avez besoin d'une délégation.
BlackSpy
1
La seule façon d'accomplir ce que vous essayez de faire de la manière dont vous essayez de le faire est d'amener l'utilisateur à taper son nom d'utilisateur et son mot de passe dans une boîte de dialogue personnalisée sur votre application ASP.NET, de les stocker sous forme de chaînes, puis d'utiliser les pour définir votre identité lorsque vous vous connectez à votre projet API Web. Sinon, vous devez supprimer NTLM et passer à Kerberos, afin de pouvoir transmettre le ticket Kerboros au projet API Web. Je recommande vivement de lire le lien que j'ai joint dans ma réponse originale. Ce que vous essayez de faire nécessite une solide compréhension de l'authentification Windows avant de commencer.
BlackSpy
2
@BlackSpy: J'ai beaucoup d'expérience avec l'authentification Windows. Ce que j'essaie de comprendre, c'est pourquoi le WebClientpeut transmettre les informations d'identification NTLM, mais le HttpClientne peut pas. Je peux y parvenir en utilisant uniquement l'emprunt d'identité ASP.Net, sans avoir à utiliser Kerberos ou à stocker les noms d'utilisateur / mots de passe. Cela ne fonctionne cependant qu'avec WebClient.
adrianbanks
1
Il devrait être impossible de se faire passer pour plus d'un saut sans transmettre le nom d'utilisateur et le mot de passe sous forme de texte. il enfreint les règles d'emprunt d'identité et NTLM ne l'autorisera pas. WebClient vous permet de sauter 1 saut parce que vous passez les informations d'identification et exécutez en tant qu'utilisateur sur la boîte. Si vous regardez les journaux de sécurité, vous verrez la connexion - l'utilisateur se connecte au système. Vous ne pouvez pas ensuite exécuter en tant qu'utilisateur à partir de cette machine à moins d'avoir transmis les informations d'identification sous forme de texte et d'utiliser une autre instance de client Web pour vous connecter à la boîte suivante.
BlackSpy
17

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

Donc, après avoir évalué que cela ExecutionContext.IsFlowSuppressed()aurait pu être le coupable, j'ai enveloppé notre code d'usurpation 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é usurpée. Cela est repris parce que nous supprimons maintenant le flux. En raison de l'utilisation / de l'élimination, ceci est réinitialisé après l'appel.

Cela semble maintenant fonctionner pour nous, ouf!

Chullybun
la source
2
Merci beaucoup d'avoir fait cette analyse. Cela a aussi réglé ma situation. Maintenant, mon identité est correctement transmise à l'autre application Web! Vous m'avez économisé des heures de travail! Je suis surpris que ce ne soit pas plus élevé sur le nombre de ticks.
justdan23
J'avais seulement besoin using (System.Threading.ExecutionContext.SuppressFlow())et le problème a été résolu pour moi!
ZX9
10

Dans .NET Core, j'ai réussi à obtenir un System.Net.Http.HttpClientwith UseDefaultCredentials = truepour transmettre les informations d'identification Windows de l'utilisateur authentifié à un service principal en utilisant 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);
    });
}
fléau192
la source
4

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)
.... 
Paulo Augusto Batista
la source
3

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

la mise en oeuvre

// Je suppose que vous avez votre propre type concret. Dans mon cas, j'utilise d'abord le code avec une classe appelée FileCategory

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 classes d'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.

caché
la source