Search code examples
c#asp.net-coreasp.net-identityasp.net-core-identity

PasswordSignInAsync() seems to block somewhere without any error


I'm trying to set up the authentication in my existing App with ASP.NET Core Identity 2.0. As I use my own database schema and classes, I have my own User class and I had to create a custom UserStore and a custom UserManager.

So I have my Login function in the AccountController :

    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
    {
        ViewData["ReturnUrl"] = returnUrl;
        if (ModelState.IsValid)
        {
            // This doesn't count login failures towards account lockout
            // To enable password failures to trigger account lockout, set lockoutOnFailure: true
            var result = await _signInManager.PasswordSignInAsync(model.Login, model.Password, model.RememberMe, lockoutOnFailure: false);
            if (result.Succeeded)
            {
                _logger.LogInformation("User logged in.");
                return RedirectToLocal(returnUrl);
            }
            if (result.IsLockedOut)
            {
                _logger.LogWarning("User account locked out.");
                return RedirectToAction(nameof(Lockout));
            }
            else
            {
                ModelState.AddModelError(string.Empty, "Connection failed.");
                return View(model);
            }
        }
        return View(model);
    }

And I have my custom UserManager:

public class CustomUserManager<TUser> : UserManager<TUser> where TUser : User
{
public CustomUserManager(IUserStore<TUser> store, IOptions<IdentityOptions> optionsAccessor,
IPasswordHasher<TUser> passwordHasher, IEnumerable<IUserValidator<TUser>> userValidators,
IEnumerable<IPasswordValidator<TUser>> passwordValidators, ILookupNormalizer keyNormalizer,
IdentityErrorDescriber errors, IServiceProvider services, ILogger<UserManager<TUser>> logger) : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
{
}

public override Task<bool> CheckPasswordAsync(TUser user, string password)
{
    bool passwordIsSimilar = password == user.Password;

    return new Task<bool>(() => passwordIsSimilar);
}
}

And my services declarations :

        services.AddIdentity<User, ProfileUser>().AddUserManager<CustomUserManager<User>>().AddDefaultTokenProviders();
        services.AddTransient<IUserStore<User>, UserIdentity>();
        services.AddTransient<IRoleStore<ProfileUser>, ProfileIdentity>();

        services.Configure<IdentityOptions>(options =>
        {
            // Lockout settings
            options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
            options.Lockout.MaxFailedAccessAttempts = 10;
            options.Lockout.AllowedForNewUsers = true;
        });

        services.ConfigureApplicationCookie(options =>
        {
            // Cookie settings
            options.Cookie.HttpOnly = true;
            options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
            // If the LoginPath isn't set, ASP.NET Core defaults 
            // the path to /Account/Login.
            options.LoginPath = "/Account/Login";
            // If the AccessDeniedPath isn't set, ASP.NET Core defaults 
            // the path to /Account/AccessDenied.
            options.AccessDeniedPath = "/Account/AccessDenied";
            options.SlidingExpiration = true;
        });

My custom UserManager works and the CheckPasswordAsync() is called when the user logs in. However, after this, the function await _signInManager.PasswordSignInAsync(model.Login, model.Password, model.RememberMe, lockoutOnFailure: false); seems to block somewhere. There isn't errors in the debug mode and the execution doesn't pass to the next line (if (result.Succeeded))

Have you an idea for what reason my code blocks in the execution?


Solution

  • The problem here is this line:

    return new Task<bool>(() => passwordIsSimilar);
    

    The Task constructor does not actually run the task; it just initialises it. The code that invokes CheckPasswordAsync is waiting for this task to complete, but as it never starts, that's not going to happen.

    As Stephen Cleary writes: Do not use Task or Task<T> constructors. Instead, in your example, you can simply use Task.FromResult<T>, like so:

    return Task.FromResult(passwordIsSimilar);
    

    Essentially, this creates a new Task for you that has already completed with the result of passwordIsSimilar. Anything that unwraps the task (usually using await) will retrieve the value of passwordIsSimilar.