HttpClient calling a Windows-Authenication ApiCont

2019-02-27 11:04发布

问题:

Is there a way for my api controller to get the IIdentity of the account who initiated the call to the api controller when the api-controller is using windows-authentication ?

My "castController.User.Identity" is (of type) WindowsIdentity. But it is "empty". Empty, as is : IsAuthenticated = false, and an empty UserName. It isn't null, it is "empty".

My "WebTier" is an IIS application running with an custom AppPool and the IIdentity which runs the custom AppPool is something like "mydomain\myServiceAccount". I'm trying to get the "castController.User.Identity.Name" value to be this service account.

(I guess it could be any client who is able to connect to my WebApiTier with a valid windows-account, but I'm mentioning this just in case it could be throwing a weird monkey wrench)

My "WebTier" (Mvc Application) has this method:

You'll notice 2 ways I'm using UseDefaultCredentials. (Aka, I've been trying to figure this out for a bit)

    private async Task<HttpResponseMessage> ExecuteProxy(string url)
    {
            HttpClientHandler handler = new HttpClientHandler()
            {
                UseDefaultCredentials = true
            };
            handler.PreAuthenticate = true;

            WebRequestHandler webRequestHandler = new WebRequestHandler();
            webRequestHandler.UseDefaultCredentials = true;
            webRequestHandler.AllowPipelining = true;
            webRequestHandler.AuthenticationLevel = System.Net.Security.AuthenticationLevel.MutualAuthRequired;
            webRequestHandler.ImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Identification;


            using (var client = new HttpClient(handler)) /* i've tried webRequestHandler too */ 
            {
                Uri destinationUri = new Uri("http://localhost/MyVirtualDirectory/api/mycontroller/mymethod");

                this.Request.RequestUri = destinationUri;

                return await client.SendAsync(this.Request);
            }
    }

"WebApiTier" Setup.

web.config

  <system.web>
    <compilation debug="true" targetFramework="4.5" />
    <httpRuntime targetFramework="4.5" />
    <authentication mode="Windows" />

"WebApiTier" Code

public MyController : ApiController
{

    [ActionName("MyMethod")]
    [MyCustomAuthorization]
    public IEnumerable<string> MyMethod()
    {
                return new string[] { "value1", "value2" };
    }

}


public class MyCustomAuthorizationAttribute : System.Web.Http.AuthorizeAttribute
{

    private string CurrentActionName { get; set; }


    public override void OnAuthorization(HttpActionContext actionContext)
    {
        this.CurrentActionName = actionContext.ActionDescriptor.ActionName;
        base.OnAuthorization(actionContext);
    }

    protected override bool IsAuthorized(HttpActionContext actionContext)
    {

        var test1 = System.Threading.Thread.CurrentPrincipal;
        /* the above is "empty" */

        ////string userName = actionContext.RequestContext.Principal;/*  Web API v2  */
        string userName = string.Empty;
        ApiController castController = actionContext.ControllerContext.Controller as ApiController;
        if (null != castController)
        {
            userName = castController.User.Identity.Name; 
            /* the above is "empty" */
        }

        return true;
    }
}

}

Again. I'm not doing a "double hop" (that I've read about in a few places).
Both tiers are on the same domain (and local development, they're on the same machine)....

The funny thing is that I've read this ( How to get HttpClient to pass credentials along with the request? ) and the "problem" reported there is EXACTLY how I want mine to work. (?!?!).

For development, the "WebApiTier" is running under full IIS. For "WebTier", I've tried it under IIS-Express and full-fledge IIS.

I also ran a console app program with this code:

Console App

    IEnumerable<string> returnItems = null;

        HttpClientHandler handler = new HttpClientHandler()
        {
            UseDefaultCredentials = true
        };
        handler.PreAuthenticate = true;


        WebRequestHandler webRequestHandler = new WebRequestHandler();
        webRequestHandler.UseDefaultCredentials = true;
        webRequestHandler.AllowPipelining = true;
        webRequestHandler.AuthenticationLevel = System.Net.Security.AuthenticationLevel.MutualAuthRequired;
        webRequestHandler.ImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Identification;


        HttpClient client = new HttpClient(handler);
    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));


    string serviceUrl = "http://localhost/MyVirtualDirectory/api/mycontroller/mymethod";

    HttpResponseMessage response = client.GetAsync(new Uri(serviceUrl)).Result;

    var temp1 = (response.ToString());
    var temp2 = (response.Content.ReadAsStringAsync().Result);

    if (response.IsSuccessStatusCode)
    {
        Task<IEnumerable<string>> wrap = response.Content.ReadAsAsync<IEnumerable<string>>();
        if (null != wrap)
        {
            returnItems = wrap.Result;
        }
        else
        {
            throw new ArgumentNullException("Task<IEnumerable<string>>.Result was null.  This was not expected.");
        }
    }
    else
    {
        throw new HttpRequestException(response.ReasonPhrase + " " + response.RequestMessage);
    }

Same result as the other code. An "empty" Windows Identity.

I also went through this

http://www.iis.net/configreference/system.webserver/security/authentication/windowsauthentication

just as a sanity check.

回答1:

Ok. I figured out the issue. Thanks to this post.

How to get Windows user name when identity impersonate="true" in asp.net?

//Start Quote//

With <authentication mode="Windows"/> in your application and Anonymous access enabled in IIS, you will see the following results:

System.Environment.UserName: Computer Name
Page.User.Identity.Name: Blank
System.Security.Principal.WindowsIdentity.GetCurrent().Name: Computer Name

//End Quote

So I'll also include a full answer.......to show the issue and some possible settings that need to be tweaked.

Go and download this mini example.

https://code.msdn.microsoft.com/ASP-NET-Web-API-Tutorial-8d2588b1

This will give you a quick "WebApiTier" called ProductsApp (ProductsApp.csproj).

If you want to do it yourself....just create a WebApi Controller...that returns some Products.

public class ProductsController : ApiController
{
    Product[] products = new Product[] 
    { 
        new Product { Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1 }, 
        new Product { Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M }, 
        new Product { Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M } 
    };

    [IdentityWhiteListAuthorization]
    public IEnumerable<Product> GetAllProducts()
    {
        return products;
    }

}

Open the above .sln.

Add a new "class library" csproj called "WebApiIdentityPoc.Domain.csproj".

Create a new class in this library.

namespace WebApiIdentityPoc.Domain
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Category { get; set; }
        public decimal Price { get; set; }
    }
}

Delete (or comment out)

\ProductsApp\Models\Product.cs

Add a (project) reference in ProductsApp to WebApiIdentityPoc.Domain.

Fix the namespace issue in

\ProductsApp\Controllers\ProductsController.cs

//using ProductsApp.Models;
using WebApiIdentityPoc.Domain;

namespace ProductsApp.Controllers
{
    public class ProductsController : ApiController
    {

(You're basically moving the "Product" object to another library so the Server and the Client can share the same object.)

You should be able to compile at this point.

..........

Add a new "Console Application" projec to the solution.

WebApiIdentityPoc.ConsoleOne.csproj

Use Nuget to add "Newtonsoft.Json" reference/library to the WebApiIdentityPoc.ConsoleOne.csproj.

Add the references (Framework or Extensions using right-click/add references on the "/References folder in the csproj)

System.Net.Http
System.Net.Http.Formatting
System.Net.Http.WebRequest (this one is may not be needed)

Add a project reference to WebApiIdentityPoc.Domain.

In "Program.cs" in the Console App, paste this code: .............

namespace WebApiIdentityPoc.ConsoleOne
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    using System.Net.Http;
    using System.Net.Http.Headers;
    using System.Security.Principal;
    using System.Text;
    using System.Threading.Tasks;
    using Newtonsoft.Json;
    using WebApiIdentityPoc.Domain;

    public class Program
    {
        private static readonly string WebApiExampleUrl = "http://localhost:47503/api/Products/GetAllProducts"; /* check ProductsApp.csproj properties, "Web" tab, "IIS Express" settings if there is an issue */

        public static void Main(string[] args)
        {
            try
            {
                System.Security.Principal.WindowsIdentity ident = System.Security.Principal.WindowsIdentity.GetCurrent();
                if (null != ident)
                {
                    Console.WriteLine("Will the Identity '{0}' Show up in IdentityWhiteListAuthorizationAttribute ???", ident.Name);
                }

                RunHttpClientExample();
                RunWebClientExample();
                RunWebClientWicExample();
            }
            catch (Exception ex)
            {
                System.Text.StringBuilder sb = new System.Text.StringBuilder();
                Exception exc = ex;
                while (null != exc)
                {
                    sb.Append(exc.GetType().Name + System.Environment.NewLine);
                    sb.Append(exc.Message + System.Environment.NewLine);
                    exc = exc.InnerException;
                }

                Console.WriteLine(sb.ToString());
            }

            Console.WriteLine("Press ENTER to exit");
            Console.ReadLine();
        }

        private static void RunWebClientExample()
        {
            /* some articles said that HttpClient could not pass over the credentials because of async operations, these were some "experiments" using the older WebClient.  Stick with HttpClient if you can */
            WebClient webClient = new WebClient();
            webClient.UseDefaultCredentials = true;
            string serviceUrl = WebApiExampleUrl;
            string json = webClient.DownloadString(serviceUrl);
            IEnumerable<Product> returnItems = JsonConvert.DeserializeObject<IEnumerable<Product>>(json);
            ShowProducts(returnItems);
        }

        private static void RunWebClientWicExample()
        {
            /* some articles said that HttpClient could not pass over the credentials because of async operations, these were some "experiments" using the older WebClient.  Stick with HttpClient if you can */
            System.Security.Principal.WindowsIdentity ident = System.Security.Principal.WindowsIdentity.GetCurrent();
            WindowsImpersonationContext wic = ident.Impersonate();
            try
            {
                WebClient webClient = new WebClient();
                webClient.UseDefaultCredentials = true;
                string serviceUrl = WebApiExampleUrl;
                string json = webClient.DownloadString(serviceUrl);
                IEnumerable<Product> returnItems = JsonConvert.DeserializeObject<IEnumerable<Product>>(json);
                ShowProducts(returnItems);
            }
            finally
            {
                wic.Undo();
            }
        }

        private static void RunHttpClientExample()
        {
            IEnumerable<Product> returnItems = null;

            HttpClientHandler handler = new HttpClientHandler()
            {
                UseDefaultCredentials = true, PreAuthenticate = true
            };

            ////////WebRequestHandler webRequestHandler = new WebRequestHandler();
            ////////webRequestHandler.UseDefaultCredentials = true;
            ////////webRequestHandler.AllowPipelining = true;
            ////////webRequestHandler.AuthenticationLevel = System.Net.Security.AuthenticationLevel.MutualAuthRequired;
            ////////webRequestHandler.ImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Identification;

            using (HttpClient client = new HttpClient(handler))
            {
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

                string serviceUrl = WebApiExampleUrl;

                HttpResponseMessage response = client.GetAsync(new Uri(serviceUrl)).Result;

                var temp1 = response.ToString();
                var temp2 = response.Content.ReadAsStringAsync().Result;

                if (response.IsSuccessStatusCode)
                {
                    Task<IEnumerable<Product>> wrap = response.Content.ReadAsAsync<IEnumerable<Product>>();
                    if (null != wrap)
                    {
                        returnItems = wrap.Result;
                    }
                    else
                    {
                        throw new ArgumentNullException("Task<IEnumerable<Product>>.Result was null.  This was not expected.");
                    }
                }
                else
                {
                    throw new HttpRequestException(response.ReasonPhrase + " " + response.RequestMessage);
                }
            }

            ShowProducts(returnItems);
        }

        private static void ShowProducts(IEnumerable<Product> prods)
        {
            if (null != prods)
            {
                foreach (Product p in prods)
                {
                    Console.WriteLine("{0}, {1}, {2}, {3}", p.Id, p.Name, p.Price, p.Category);
                }

                Console.WriteLine(string.Empty);
            }
        }
    }
}

You should be able to compile and run and see some Products display in the Console App.

.....

In "ProductsApp.csproj", Add a new Folder.

/WebApiExtensions/

Under this folder, add a new file:

IdentityWhiteListAuthorizationAttribute.cs

Paste in this code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
using System.Web.Http.Controllers;

namespace ProductsApp.WebApiExtensions
{
    public class IdentityWhiteListAuthorizationAttribute : System.Web.Http.AuthorizeAttribute
    {
        public IdentityWhiteListAuthorizationAttribute()
        {
        }

        private string CurrentActionName { get; set; }

        public override void OnAuthorization(HttpActionContext actionContext)
        {
            this.CurrentActionName = actionContext.ActionDescriptor.ActionName;
            base.OnAuthorization(actionContext);
        }

        protected override bool IsAuthorized(HttpActionContext actionContext)
        {

            var test1 = System.Threading.Thread.CurrentPrincipal;
            var test2 = System.Security.Principal.WindowsIdentity.GetCurrent();

            ////string userName = actionContext.RequestContext.Principal.Name;/*  Web API v2  */
            string dingDingDingUserName = string.Empty;
            ApiController castController = actionContext.ControllerContext.Controller as ApiController;
            if (null != castController)
            {
                dingDingDingUserName = castController.User.Identity.Name;
            }

        string status = string.Empty;
        if (string.IsNullOrEmpty(dingDingDingUserName))
        {
            status = "Not Good.  No dingDingDingUserName";
        }
        else
        {
            status = "Finally!";
        }

            return true;
        }
    }
}

Decorate the webapimethod with this attribute.

    [IdentityWhiteListAuthorization]
    public IEnumerable<Product> GetAllProducts()
    {
        return products;
    }

(You'll have to resolve the namespace).

At this point, you should be able to compile....and run.

But dingDingDingUserName will be string.Empty. (The original issue that spanned this post).

Ok..

Click (left-click once) the ProductsApp.csproj in the Solution Explorer.

Look at the properties tab. (This is not the "right-click / properties ::: This is the properties that show up (default would be in the bottom right of VS) when you simply left-click the ProductsApp.csproj.

You'll see several settings, but there are two of interest:

Anonymous Authentication | Enabled
Windows Authentication | Enabled

(Note, the above is how these settings show up in the VS GUI. They show up like this in the .csproj file)

           <IISExpressAnonymousAuthentication>enabled</IISExpressAnonymousAuthentication>
<IISExpressWindowsAuthentication>enabled</IISExpressWindowsAuthentication>

If you set

Anonymous Authentication | Disabled

(which shows up in the .csproj like this:

<IISExpressAnonymousAuthentication>disabled</IISExpressAnonymousAuthentication>
<IISExpressWindowsAuthentication>enabled</IISExpressWindowsAuthentication>

)

VOILA! The "dingDingDingName" value should show up.

The link I have above .. points to the anonymous-authenication-enabled to being the issue.

But here is a long example to show the direct effects...in regards to HttpClient.

One more caveat I learned along the way.

If you cannot alter the

Anonymous Authentication Enabled/Disabled
Windows Authentication Enabled/Disabled

settings, then you need to adjust the "master settings".

In IIS Express, this will be in a file like:

C:\Users\MyUserName\Documents\IISExpress\config\applicationhost.config

The “master settings” need to allow the local settings to be overridden.

<sectionGroup name="security">
    <section name="anonymousAuthentication" overrideModeDefault="Allow" />
    <!-- Other Stuff -->
    <section name="windowsAuthentication" overrideModeDefault="Allow" />
    </sectionGroup>

The authentications themselves need to be turned on at a master level.

<security>
    <authentication>

    <anonymousAuthentication enabled="true" userName="" />

    <windowsAuthentication enabled="true">
                    <providers>
                                    <add value="Negotiate" />
                                    <add value="NTLM" />
                    </providers>
    </windowsAuthentication>

    </authentication>

(Full IIS will have similar settings in

C:\Windows\System32\inetsrv\config\applicationHost.config

)

Bottom line:

HttpClient can send over the WindowsIdentity of the process running the HttpClient code....using HttpClientHandler AND if the WebApiTier is set for WindowsAuthentication AND Anonymous-Authentication turned off.

Ok. I hope that helps somebody in the future.