Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 364
Sven Avatar asked Jul 03 '19 19:07

Sven


People also ask

Does ASP.NET support Windows authentication?

The ASP.NET Development Web Server also supports NTLM authentication. You can enable NTLM authentication by right-clicking the name of your project in the Solution Explorer window and selecting Properties.

Does Kestrel support Windows authentication?

Authentication. Negotiate NuGet package can be used with Kestrel to support Windows Authentication using Negotiate and Kerberos on Windows, Linux, and macOS. Credentials can be persisted across requests on a connection.

How do I use Windows authentication?

On the taskbar, click Start, and then click Control Panel. In Control Panel, click Programs and Features, and then click Turn Windows Features on or off. Expand Internet Information Services, then World Wide Web Services, then Security. Select Windows Authentication, and then click OK.


1 Answers

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);
    
like image 160
Sven Avatar answered Sep 26 '22 08:09

Sven