简体   繁体   中英

UserManager.FindAsync returns null after password reset

Following the official documentation ( https://github.com/rustd/AspnetIdentitySample ) and NuGet package, I'm having issues with logging in after a password reset for my MVC5 application. It seems as though Entity Framework doesn't refresh its context in the process, it's only after I restart my application that I can login with the correct credentials.

As far as I can work out, I've done everything that the code samples have done as well. Only I have much more code and settings (eg Unity).

This is the problem area:

public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
    try
    {
        if (ModelState.IsValid)
        {
            ApplicationUser user = await UserManager.FindAsync(model.UserName, model.Password);
            if (user != null)
            {
                await this.SignInAsync(user, false);
                return RedirectToLocal(returnUrl);
            }
            else
            {
                model.State = ViewModelState.Error;
                model.Messages = new List<string>() { "No access buddy!" };
            }
        }

        // If we got this far, something failed, redisplay form
        return View(model);

    }
    catch (Exception ex)
    {
        throw;
    }
}
private async Task SignInAsync(ApplicationUser user, bool isPersistent)
{
    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);

    ClaimsIdentity identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
    AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}

This part works perfectly when I log on for the first time. However, after I have reset my password, logging in with the new credentials isn't possible (it still takes the old version).

Here is my configuration:

public class ApplicationUserManager : UserManager<ApplicationUser>
{
    #region Constructor

    public ApplicationUserManager(IUserStore<ApplicationUser> store)
        : base(store)
    {
        this.UserTokenProvider = new TotpSecurityStampBasedTokenProvider<ApplicationUser, string>();

    }
    #endregion Constructor

    #region Methods

    public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
    {
        ApplicationUserManager manager = new ApplicationUserManager(new UserStore<ApplicationUser>(context.Get<SecurityDbContext>()));
        manager.UserValidator = new UserValidator<ApplicationUser>(manager)
        {
            AllowOnlyAlphanumericUserNames = false,
            RequireUniqueEmail = true
        };

        // Configure validation logic for passwords
        manager.PasswordValidator = new PasswordValidator
        {
            RequiredLength = 6,
            RequireNonLetterOrDigit = true,
            RequireDigit = true,
            RequireLowercase = true,
            RequireUppercase = true,
        };

        // Configure user lockout defaults
        manager.UserLockoutEnabledByDefault = true;
        manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
        manager.MaxFailedAccessAttemptsBeforeLockout = 5;

        // Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user
        // You can write your own provider and plug it in here.
        manager.RegisterTwoFactorProvider("Phone Code", new PhoneNumberTokenProvider<ApplicationUser>
        {
            MessageFormat = "Your security code is {0}"
        });

        manager.RegisterTwoFactorProvider("Email Code", new EmailTokenProvider<ApplicationUser>
        {
            Subject = "Security Code",
            BodyFormat = "Your security code is {0}"
        });

        manager.EmailService = new EmailService();
        manager.SmsService = new SmsService();
        var dataProtectionProvider = options.DataProtectionProvider;
        if (dataProtectionProvider != null)
        {
            manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"));
        }
        return manager;
    }

    #endregion Methods
}

This is what I've configured during Startup:

// Configure the db context, user manager and signin manager to use a single instance per request
app.CreatePerOwinContext(SecurityDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);

// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
// Configure the sign in cookie
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
    LoginPath = new PathString("/Account/Login"),
    Provider = new CookieAuthenticationProvider
    {
        // Enables the application to validate the security stamp when the user logs in.
        // This is a security feature which is used when you change a password or add an external login to your account.
        OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
            validateInterval: TimeSpan.FromMinutes(30),
            regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
    }
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions { });

Ultimately, after a few screens, here is where the user ultimately ends up to create a new password:

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ResetPassword(ResetPasswordViewModel model)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }
    ApplicationUser user = await UserManager.FindByEmailAsync(model.Email);
    if (user == null)
    {
        // Don't reveal that the user does not exist
        return RedirectToAction("ResetPasswordConfirmation", "Account");
    }

    IdentityResult result = await UserManager.ResetPasswordAsync(user.Id, model.Code, model.Password);
    if (result.Succeeded)
    {
        return RedirectToAction("ResetPasswordConfirmation", "Account");
    }
    else
    {
        AddErrors(result);
        return View();
    }
}

No errors here either, it stores the new hashed value and security stamp in the database. I'm thinking of some caching, cookies or dbContext that isn't refreshed at the time the password is reset.

Does anyone have any ideas?

Ok so I have finally found the reason for this odd behavior. I had the following DbConfiguration:

 public class Configuration : DbConfiguration
{
    public Configuration()
    {
        CacheTransactionHandler transactionHandler = new CacheTransactionHandler(new InMemoryCache());
        this.AddInterceptor(transactionHandler);

        Loaded += (sender, args) =>
       {
           args.ReplaceService<DbProviderServices>((s, _) => new CachingProviderServices(s, transactionHandler));
       };
    }
}

Commenting out the callback did the trick, which sounds logical as I replaced the standard DbProviderServices with second-level caching (as provided by https://efcache.codeplex.com/ )

Update:

It's not necessary to entirely remove the second-level caching. Instead, by adding a caching provider, I can choose which tables to cache (and for how long). Here is the updated code:

 public class Configuration : DbConfiguration
{
    public Configuration()
    {
        CacheTransactionHandler transactionHandler = new CacheTransactionHandler(new InMemoryCache());
        this.AddInterceptor(transactionHandler);

        MyCachingPolicy cachingPolicy = new MyCachingPolicy();         
        Loaded += (sender, args) =>
       {
           args.ReplaceService<DbProviderServices>((s, _) => new CachingProviderServices(s, transactionHandler, cachingPolicy));
       };
    }
}

internal class MyCachingPolicy : CachingPolicy
{
    #region Constructor

    internal MyCachingPolicy()
    {
        this.NonCachableTables = new List<string>()
        {
            "AspNetUsers",
            "Resource",
            "Task",
            "Appointment"
        };
    }

    #endregion Constructor

    #region Properties

    private List<string> NonCachableTables { get; set; }

    #endregion Properties

    #region Methods

    #endregion Methods

    protected override bool CanBeCached(ReadOnlyCollection<EntitySetBase> affectedEntitySets, string sql, IEnumerable<KeyValuePair<string, object>> parameters)
    {
        return !affectedEntitySets.Select(e => e.Table ?? e.Name).Any(tableName => this.NonCachableTables.Contains(tableName));
    }

    protected override void GetCacheableRows(ReadOnlyCollection<EntitySetBase> affectedEntitySets, out int minCacheableRows, out int maxCacheableRows)
    {
        base.GetCacheableRows(affectedEntitySets, out minCacheableRows, out maxCacheableRows);
    }

    protected override void GetExpirationTimeout(ReadOnlyCollection<EntitySetBase> affectedEntitySets, out TimeSpan slidingExpiration, out DateTimeOffset absoluteExpiration)
    {
        base.GetExpirationTimeout(affectedEntitySets, out slidingExpiration, out absoluteExpiration);
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM