C# HttpListener multiple authentication schemes and Chrome
Asked Answered
A

1

6

Ok, this is a long question, but I think it worth this. What we have:

  1. A sample dummy C# console application, that start self-hosted owin ASP.Net WebAPI service (Microsoft.AspNet.WebApi.OwinSelfHost NuGet package):

    class Program
    {
        static void Main(string[] args)
        {
            var url = "http://localhost:8080/";
    
            Console.WriteLine($"Starting on {url}");
            using (WebApp.Start<Startup>(url))
            {
                Console.WriteLine("Success! Press any key to stop...");
                Console.ReadKey();
            }
        }
    }
    
  2. OWIN startup class:

    class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            // enable Windows AND Anonymous authentication
            var listener = app.Properties["System.Net.HttpListener"] as HttpListener;
            listener.AuthenticationSchemes = AuthenticationSchemes.Anonymous |
                AuthenticationSchemes.IntegratedWindowsAuthentication;
    
            // configure WebAPI
            var config = new HttpConfiguration();
            config.MapHttpAttributeRoutes();
    
            app.UseWebApi(config);
        }
    }
    
  3. Sample WebAPI controller with two public methods:

    [RoutePrefix("sample"), Authorize]
    public class SampleController : ApiController
    {
        [Route("public"), AllowAnonymous]
        public object GetPublicSample()
        {
            var message = $"Hi there, mr. {User?.Identity?.Name ?? "ANONYMOUS"}";
            return new { sample = 0, message };
        }
    
        [Route("protected")]
        public object GetProtectedSample()
        {
            var message = $"Hi there, mr. {User?.Identity?.Name ?? "ANONYMOUS"}";
            return new { sample = 42, message };
        }
    }
    

Now, when we run our project and point chrome to http://localhost:8080/sample/public this request is invoked:

GET /sample/public HTTP/1.1
Host: localhost:8080

HTTP/1.1 200 OK
Content-Length: 50
Content-Type: application/json; charset=utf-8
Server: Microsoft-HTTPAPI/2.0
Date: Wed, 28 Feb 2018 08:05:56 GMT

{"sample":0,"message":"Hi there, mr. ANONYMOUS"}

But when we go to http://localhost:8080/sample/protected we have this:

GET /sample/protected HTTP/1.1
Host: localhost:8080

HTTP/1.1 401 Unauthorized
date: Wed, 28 Feb 2018 08:19:01 GMT
www-authenticate: Negotiate,NTLM
server: Microsoft-HTTPAPI/2.0
content-length: 61
content-type: application/json; charset=utf-8

{"Message":"Authorization has been denied for this request."}

And this is almost "as expected" except 1 thing. I expect that when my browser receives 401 HTTP response with www-authenticate header(s), he will try to repeate same request with specified authentication (if there where no any other Authorization header in request). But he does not, for some reason:(

To make things more intresting, we can see that AuthenticationSchemes.IntegratedWindowsAuthentication is actualy AuthenticationSchemes.Negotiate | AuthenticationSchemes.Ntlm and when I remove one of them things starts to work as expected! For example: replace IntegratedWindowsAuthentication with Negotiate in our Startup class and head your brouser to http://localhost:8080/sample/protected

GET /sample/protected HTTP/1.1
Host: localhost:8080

HTTP/1.1 200 OK
date: Wed, 28 Feb 2018 08:29:55 GMT
www-authenticate: tOkeN1231351234153=
server: Microsoft-HTTPAPI/2.0
content-length: 59
content-type: application/json; charset=utf-8

{"sample":42,"message":"Hi there, mr. DOMAIN\\username"}

Generally, our server first response with 401 HTTP status and set header www-authenticate: Negotiate and then browser repeat request with additional Authorization header. Same thing if we replace IntegratedWindowsAuthentication with Ntlm.

And one more example to make things clearer. If we remove AuthenticationSchemes.Anonymous and leave only AuthenticationSchemes.IntegratedWindowsAuthentication we will notice 2 things in result:

  1. /sample/public endpoint is not available for anonymous requests anymore (as expected)
  2. /sample/protected endpoint now working as it should (!)

And if we look at first 401 server response, we will notice that there is two www-authenticate headers instead of one (as before):

GET /sample/protected HTTP/1.1
Host: localhost:8080

HTTP/1.1 401 Unauthorized
Content-Length: 0
Server: Microsoft-HTTPAPI/2.0
WWW-Authenticate: Negotiate
WWW-Authenticate: NTLM
Date: Wed, 28 Feb 2018 08:44:04 GMT

So, my questions is: Is it "OK" to put multiple authentication schemes in single www-authenticate header? If "Yes, it's OK", why my chrome not nandling this situation? If "No! it's all way wrong!", why HttpListener act like this and how can I bypass this? Pls hlp!

Annalist answered 28/2, 2018 at 9:1 Comment(1)
I don't see any reason for people to downvote this question so much. Especially without any comment. Leaving one myself, I found this question both helpful and interesting.Quarterback
P
3

This has been asked a while back, but I'd like to provide my .02. The AuthenticationSchemes.IntegratedWindowsAuthentication is a bit strange. IWA is the same as setting Negotiate and NTLM. Negotiate, on the other hand, will fall back to NTLM if Kerberos fails. ASP.NET Core doesn't have IWA in the enum: See ASP.NET Core AuthenticationSchemes Enum

I use:

listener.AuthenticationSchemes = AuthenticationSchemes.Anonymous | AuthenticationSchemes.Negotiate;
Playlet answered 4/9, 2018 at 1:56 Comment(3)
This enum is from another namespace: see thisAnnalist
Also, it maybe unclear, but my question is about "why www-authenticate: Negotiate,NTLM is not working on chrome, but WWW-Authenticate: Negotiate AND WWW-Authenticate: NTLM works?"Annalist
But Core is a different story.Quarterback

© 2022 - 2024 — McMap. All rights reserved.