20

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

Camilo Terevinto
  • 31,141
  • 6
  • 88
  • 120
Sven
  • 2,345
  • 2
  • 21
  • 43
  • I am getting "Unable to resolve service for type 'Microsoft.AspNetCore.Identity.UserManager`1[Microsoft.AspNetCore.Identity.IdentityUser]' while attempting to Invoke middlewares.AutoLoginMiddleware. Do you know how to solve this? The Startup has app.UseAuthentication(); app.UseAuthorization(); app.UseMiddleware(); – user266909 Jul 09 '20 at 21:59
  • I guess you forgot to also register Identity framework: `services.AddDefaultIdentity()` should do the trick. – Sven Jul 10 '20 at 07:11
  • I did. Here is my ConfigureServices() services.AddIdentity(options => { options.User.RequireUniqueEmail = true; – user266909 Jul 10 '20 at 18:08
  • Then you'll have to add UserManager as parameter to the AutoLoginMiddleware InvokeAsync method. From your error message it seems you mistakenly added the wrong UserManager – Sven Jul 13 '20 at 11:52
  • I'm working on a solution with a nearly same case and the idea of your AutoLoginMiddleware is exactly what i needed. But as soon as I add .AddIdentity() the context.User.Identity never returns a WindowsIdentity - it is always from type ClaimsIdentity. Any hint what I'm missing? – M. Altmann Mar 16 '21 at 15:42

1 Answers1

19

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);
    
Sven
  • 2,345
  • 2
  • 21
  • 43