Windows Authentication

There are several ways how you can enable Windows authentication in ASP.NET Core (and thus in your IdentityServer).

  • On Windows using IIS hosting (both in- and out-of process)
  • On Windows using HTTP.SYS hosting
  • On any platform using the Negotiate authentication handler (added in ASP.NET Core 3.0)

On Windows using IIS hosting

The typical ASP.NET Core CreateDefaultBuilder host setup enables support for IIS-based Windows authentication when hosting in IIS. Make sure that Windows authentication is enabled in launchSettings.json or your IIS configuration.

The IIS integration layer will configure a Windows authentication handler into DI that can be invoked via the authentication service. Typically in your IdentityServer it is advisable to disable the automatic behavior.

This is done in ConfigureServices (details vary depending on in-proc vs out-of-proc hosting)::

// configures IIS out-of-proc settings (see https://github.com/aspnet/AspNetCore/issues/14882)
services.Configure<IISOptions>(iis =>
{
    iis.AuthenticationDisplayName = "Windows";
    iis.AutomaticAuthentication = false;
});

// ..or configures IIS in-proc settings
services.Configure<IISServerOptions>(iis =>
{
    iis.AuthenticationDisplayName = "Windows";
    iis.AutomaticAuthentication = false;
});

You trigger Windows authentication by calling ChallengeAsync using the Windows scheme (or if you want to use a constant: Microsoft.AspNetCore.Server.IISIntegration.IISDefaults.AuthenticationScheme).

This will send the Www-Authenticate header back to the browser which will then re-load the current URL including the Windows identity. You can tell that Windows authentication was successful, when you call AuthenticateAsync on the Windows scheme and the principal returned is of type WindowsPrincipal.

The principal will have information like user and group SID and the Windows account name. The following snippet shows how to trigger authentication, and if successful convert the information into a standard ClaimsPrincipal for the temp-Cookie approach::

private async Task<IActionResult> ChallengeWindowsAsync(string returnUrl)
{
    // see if windows auth has already been requested and succeeded
    var result = await HttpContext.AuthenticateAsync("Windows");
    if (result?.Principal is WindowsPrincipal wp)
    {
        // we will issue the external cookie and then redirect the
        // user back to the external callback, in essence, treating windows
        // auth the same as any other external authentication mechanism
        var props = new AuthenticationProperties()
        {
            RedirectUri = Url.Action("Callback"),
            Items =
            {
                { "returnUrl", returnUrl },
                { "scheme", "Windows" },
            }
        };

        var id = new ClaimsIdentity("Windows");

        // the sid is a good sub value
        id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.FindFirst(ClaimTypes.PrimarySid).Value));

        // the account name is the closest we have to a display name
        id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name));

        // add the groups as claims -- be careful if the number of groups is too large
        var wi = wp.Identity as WindowsIdentity;

        // translate group SIDs to display names
        var groups = wi.Groups.Translate(typeof(NTAccount));
        var roles = groups.Select(x => new Claim(JwtClaimTypes.Role, x.Value));
        id.AddClaims(roles);
        
        await HttpContext.SignInAsync(
            IdentityServerConstants.ExternalCookieAuthenticationScheme,
            new ClaimsPrincipal(id),
            props);
        return Redirect(props.RedirectUri);
    }
    else
    {
        // trigger windows auth
        // since windows auth don't support the redirect uri,
        // this URL is re-triggered when we call challenge
        return Challenge("Windows");
    }
}