Search code examples
c#asp.net-corewindows-authenticationasp.net-core-identity

ASP.NET Core Identity with Windows Authentication


I'm using .NET Core 3.0 Preview6.
We have an Intranet application with enabled Windows authentication which means that only valid AD users are allowed to use the application.
However, we like to run our own authentication backend with ASP.NET Identity, because it works "out-of-the-box". I just added a column to AspNetUsers table with the user's Windows login.

What I'd like to accomplish is that Windows users are automatically signed-in to the application with their Windows login.
I already created a custom Authentication middleware, please see code below:

public class AutoLoginMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;

    public AutoLoginMiddleware(RequestDelegate next, ILogger<AutoLoginMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context, UserService userService, UserManager<IntranetUser> userManager, 
        SignInManager<IntranetUser> signInManager)
    {
        if (signInManager.IsSignedIn(context.User))
        {
            _logger.LogInformation("User already signed in");
        }
        else
        {
            if (context.User.Identity as WindowsIdentity != null)
            {
                _logger.LogInformation($"User with Windows Login {context.User.Identity.Name} needs to sign in");
                var windowsLogin = context.User.Identity.Name;


                var user = await userManager.Users.FirstOrDefaultAsync(u => u.NormalizedWindowsLogin == windowsLogin.ToUpperInvariant());

                if (user != null)
                {
                    await signInManager.SignInAsync(user, true, "automatic");
                    _logger.LogInformation($"User with id {user.Id}, name {user.UserName} successfully signed in");

                    // Workaround
                    context.Items["IntranetUser"] = user;
                }
                else
                {
                    _logger.LogInformation($"User cannot be found in identity store.");
                    throw new System.InvalidOperationException($"user not found.");
                }
            }
        }

        // Pass the request to the next middleware
        await _next(context);
    }
}

The doc says that SignInManager.SignInAsync creates a new ClaimsIdentity - but it seems that never happens - HttpContext.User always stays a WindowsIdentity. On every request the user is signed in again, the call to signInManager.IsSignedIn() always returns false.

My question now: is it generally a good idea to have automatic authentication in this way? What other ways do exists?

My next requirement is to have a custom AuthorizationHandler. The problem here is that sometimes in the HandleRequirementAsync method the AuthorizationHandlerContext.User.Identity is a WindowsIdentity and then the call to context.User.Identity.Name raises the following Exception:

System.ObjectDisposedException: Safe handle has been closed.

Object name: 'SafeHandle'.

   at System.Runtime.InteropServices.SafeHandle.DangerousAddRef(Boolean& success)

   at System.StubHelpers.StubHelpers.SafeHandleAddRef(SafeHandle pHandle, Boolean& success)

   at Interop.Advapi32.GetTokenInformation(SafeAccessTokenHandle TokenHandle, UInt32 TokenInformationClass, SafeLocalAllocHandle TokenInformation, UInt32 TokenInformationLength, UInt32& ReturnLength)

   at System.Security.Principal.WindowsIdentity.GetTokenInformation(SafeAccessTokenHandle tokenHandle, TokenInformationClass tokenInformationClass, Boolean nullOnInvalidParam)

   at System.Security.Principal.WindowsIdentity.get_User()

   at System.Security.Principal.WindowsIdentity.<GetName>b__51_0()

   at System.Security.Principal.WindowsIdentity.<>c__DisplayClass67_0.<RunImpersonatedInternal>b__0(Object <p0>)

   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)

My assumption now is that these both parts don't work well together. Sometimes it seems there is a timing issue - my custom AuthorizationHandler is called in between the call to AutoLoginMiddleware


Solution

  • This is fixed now. It's been a bug in the preview releases. Now it's working like intended. Good luck!

    Update: I'd like to post my working code for .NET Core 3.1 Final.

    1. It's essential to register the custom login middleware after framework middlewares in Configure:
        app.UseAuthentication();
        app.UseAuthorization();
        app.UseMiddleware<AutoLoginMiddleware>();
    
    1. In the custom middleware, after signing in the user you must call CreateUserPrincipalAsync and save this principal to the HttpContext.User property.

      await signInManager.SignInAsync(user, true);
      context.User = await signInManager.CreateUserPrincipalAsync(user);
      
    2. For Blazor, we must use AuthenticationStateProvider. It has a property User which contains the ClaimsPrincipal from HttpContext. That's it.

    3. You are now able to get the Identity user like follows:

      var authState = await _authenticationStateProvider.GetAuthenticationStateAsync();
      var intranetUser = await UserManager.GetUserAsync(authState.User);