Enable both Windows authentication and Anonymous authentication in an ASP.NET Core app
Asked Answered
M

6

24

I know that this has been asked many times before, but unfortunately not about ASP.NET Core web apps, just the classic ASP.NET web apps. All the answers i've found on the internet don't help me, because the IIS configuration of ASP.NET Core apps is so different than classic ASP.NET. For example, ASP.NET Core uses the Kestrel proxy, so many of the configurations relevant in ASP.NET are not in ASP.NET Core. I've basically tried everything i could possibly found on the Internet but none helped me. I whish it was as simple as enabling both anonymous and windows authentications on the app in IIS and that's it, but I guess it will more work than that.

What is the procedure of enabling both these authentications in a single asp.net core web app?

Mabellemable answered 27/6, 2017 at 10:58 Comment(3)
ASP.NET Core uses kestral server - but when used in IIS, IIS becomes a proxy... you said kestral is a proxy, which it isn't....Telemeter
One would assume, if IIS is being your reverse proxy, and already has the windows and anonymous authentication enabled then it would just be passed through to kestral?Telemeter
Well, I have this attribute - forwardWindowsAuthToken="true", in the <aspNetCore> element in web.config. Don't know if it's related, but it is there.Mabellemable
C
28

IIS will act as a reverse proxy and will be responsible for setting and transmitting to Kestrel the Windows identity of the user. So first, set up IIS to allow both Windows and Anonymous Authentication:

enter image description here

Then, you need to change your web.config to ask IIS to transmit the Windows identity (in case one is found) to your ASP.NET Core application like that: https://mcmap.net/q/582062/-asp-net-core-web-api-current-user-amp-windows-authentication

At this point, if you create a controller action with an "[Authorize]" attribute, HttpContext.User.Identity.Name; should have the value of the Windows identity used by your client. I replied to something similar here: NTLM authentication on specific route in ASP.NET Core

The good thing is that a standard controller action will still work if your client doesn't pass along Windows identity token, while a protected one (using [Authorize] tag) will fail.

PS: I like to use curl.exe in verbose mode to see what is happening in terms of authorization protocol (Negotiate protocol, NTLM tokens ...)

Cynthla answered 27/6, 2017 at 12:17 Comment(3)
Great! Enjoy .NET Core, I sure do.Cynthla
Nice, I was missing the authorize attribute to trigger the authenticationOuellette
I had to add services.AddAuthentication(IISDefaults.AuthenticationScheme); to my startup to really force it.Oneiromancy
R
20

I have a similar scenario for an ASP.NET Core 2.0 application (use Windows Authentication throughout the app except a single controller) and Daboul's explanation was not enough.

I had to set up a custom middleware as indicated here since anonymous takes precedence.

The middleware

public class NtlmAndAnonymousSetupMiddleware
{
    private readonly RequestDelegate next;

    public NtlmAndAnonymousSetupMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        if (context.User.Identity.IsAuthenticated || context.Request.Path.ToString().StartsWith("/Anonymous"))
        {
            await next(context);
            return;
        }

        await context.ChallengeAsync("Windows");
    }

}

and its usage in Startup.cs:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    app.UseMiddleware<NtlmAndAnonymousSetupMiddleware>();

    // other code here
}

So, the middleware accept anonymous requests for AnonymousController only and will provide a challenge if Windows Authentication info is not provided.

Anonymous controller

Since the middleware makes the differece between what is anonymous and requires authentication, this will look just like any ordinary controller:

[Route("Anonymous")]
public class AnonymousController : Controller
{
    [HttpGet("Echo")]
    public string Echo(string data)
    {
        return data;
    }
}

Tests

(all done on a Windows machine)

  1. Chrome + access non-anonymous controller action => works fine (both @User.Identity.Name and @Context.User.Identity.Name return the correct user

  2. Chrome + anonymous action => works directly

  3. Firefox (which does not directly transfer NTLM ticket from OS) + non-anonymous => a modal asks for user/pass => if provided correctly, it works fine

  4. Firefox + anonymous action => works directly

Rutharuthann answered 17/4, 2018 at 11:29 Comment(6)
Brilliant!! This answer resolved my issue. My issue was 'OPTIONS' request failing before the actual xhr 'POST' request, as 'OPTIONS' does not pass authentication data. So I enabled Anonymous Authentication, but some how was not giving desired results.Forgat
This is Awesome! I found this answer worked for me, after I've tried Daboul's Answer. I'm using .net core 3.1 and running under IIS using InProcess. I've used [AllowAnonymous] filter on Api controllers that I want to open to public and Added Filter And applied AuthorizeFilter on all other controllersPhyllous
I have the same issue after migrating from 2.2 to 3.1. I'm trying this as a solution but in our case we can't use the Path of the request as a way to bypass the anonymous endpoints (we use the AllowAnonymous attribute). @Phyllous how did you implement the middleware?Elinaelinor
@Elinaelinor Not sure about a decent solution, but the context should provide the controller (name) and you can use reflection to check for AllowAnonymous attributes. If it works, it is more elegant than checking in the request path.Rutharuthann
Unless I'm just not seeing it there doesn't seem to be any info about the controller or controller method on HttpContext. I tried to get access to an Mvc ActionContext but it's null (presumably because this middleware runs before the Mvc action is selected). So it looks like my only option is to pre-emptively reflect on all Controllers/Actions to build a map of all anonymous paths... Seems like this should have been flagged as a breaking change...Elinaelinor
How to detect if user canceled authentication popup? The middleware will send a challenge again, but should redirect to "LoginFailedPage". How to redirect to "LoginFailedPage"?Flashing
V
8

In case anyone wonders, I modified @Alexei's answer to use Attributes rather than request path in Netcore 3.X

First create the class and get the endpoints metadata

public class NtlmAndAnonymousSetupMiddleware
{
    private readonly RequestDelegate next;

    public NtlmAndAnonymousSetupMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context)
    {

        if (context.User.Identity.IsAuthenticated || HasAnonymousAttribute(context))
        {
            await next(context);
            return;
        }

        await context.ChallengeAsync("Windows");
    }

    private bool HasAnonymousAttribute(HttpContext context)
    {
        var endpoint = context.GetEndpoint();
        var retVal = (endpoint?.Metadata?.GetMetadata<IAllowAnonymous>() != null);

        return retVal;
    }
}

Then modify public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

        app.UseAuthentication();
        app.UseAuthorization();
        app.UseMiddleware<NtlmAndAnonymousSetupMiddleware>();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapRazorPages();
            endpoints.MapControllers();
        });
Virgate answered 17/7, 2020 at 17:24 Comment(1)
10x for improving the solution. ASP.NET Core 3.1 indeed allows for better solutions now.Rutharuthann
D
2

I have a solution with Windows authentication disabled on IIS. All you need to do is NTLM authentication. Simply put this recursive code in your controller login action:

var result = await HttpContext.AuthenticateAsync(IISDefaults.AuthenticationScheme);
        if (!result.Succeeded) {
            await HttpContext.ChallengeAsync(IISDefaults.AuthenticationScheme); //performs NTLM handshake
            return StatusCode(Response.StatusCode);  // sends 401
        }

        // windows login has already succeed
        // get user name and domain
        WindowsIdentity winIdentity = (WindowsIdentity)result.Principal.Identity;

..... and so on

Devilkin answered 26/5, 2020 at 11:22 Comment(0)
P
0

When IIS is not under our control - probably a rare case - and we don't know whether windows authentication is even enabled or not the prior solution does not work as an exception is thrown before we reach the endpoint.

The following approach - neither elegant nor performant - might help in this case. The idea is basically to trigger another call to an endpoint to determine if the user is actually a windows loging or not. If this call is successful, then we know we have a windows user and can act accordingly, for example do a redirect to an endpoint that requires windows authentication. However, for most cases, the solution already posted is probably the way to go as this one covers an edge case.

see also: similar entry

Code example (.Net 6):

    [HttpGet]
    [AllowAnonymous]
    public async Task<IActionResult> TestWindowsAuthAsync(CancellationToken cancellationToken)
    {
        using var client = new HttpClient(new HttpClientHandler()
        {
            UseDefaultCredentials = true
        });

        var response = await client.GetAsync($"{HttpContext.Request.Scheme}://{HttpContext.Request.Host}{HttpContext.Request.PathBase}{HttpContext.Request.Path}/HasUserWindowsAuth");
        if (response.IsSuccessStatusCode)
        {
            // Yes, now we know that user indeed has windows authentication and we can act upon it
            return RedirectToAction("QuickLogin", input);
        }

        // No windows credentials have been passed at this point
        return View();
    }


    [HttpGet("HasUserWindowsAuth")]
    [Authorize(AuthenticationSchemes = IISDefaults.AuthenticationScheme)]
    public IActionResult HasUserWindowsAuth() => Ok();


    [HttpGet("QuickLogin")]
    [Authorize(AuthenticationSchemes = IISDefaults.AuthenticationScheme)]
    public async Task<IActionResult> QuickLoginAsync(LoginModel input, CancellationToken cancellationToken)
    {
        var user = this.User.Identities.FirstOrDefault(i => i System.Security.Principal.WindowsIdentity && i.IsAuthenticatd);
        // do something with that user
    }
Pablopabon answered 17/5, 2022 at 10:28 Comment(0)
S
0

I read all I can find about this subject but can not get it to work. When I set anonymous=true the Windows account can not be read and stays empty. This is when I run the Core web app with IISexpress. When I use Challengeasync the result stays the same.

Sarajane answered 22/7 at 19:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.