Search code examples
c#asp.net-core.net-coreasp.net-core-mvc

When TwoFactorEnabled is activated, I get an error during login (implementation with Identity)


I have an AccountController that contains all login, register, two-factor and other operations.

I set a status change in the user profile to enable and disable two-step login.

Everything is done correctly until the user activates it and when he tries to login again, he gets an error.

Error:

enter image description here

ArgumentException: Entity type 'IdentityUserToken' is defined with a 2-part composite key, but 3 values were passed to the 'Find' method.

It is better and clearer in the screenshot 👆

Login method:

[HttpPost]
public async Task<IActionResult> Login(LoginViewModel model)
{
    if (ModelState.IsValid)
    {
        var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.IsPersistent, lockoutOnFailure: true);

        if (result.Succeeded)
        {
            return LocalRedirect(model.ReturnUrl);
        }

        if (result.IsLockedOut)
        {
            ModelState.AddModelError("", "User account locked out!");
            return View(model);
        }

        if (result.RequiresTwoFactor)
        {
            return RedirectToAction("ToFactorLogin", new { model.Email, model.IsPersistent });
        }
        else
        {
            ModelState.AddModelError("", "Invalid login attempt!");
            return View(model);
        }
    }

    return View(model);
}

And it does not reach the TwoFactorLogin action at all and returns an error in the same line

TwoFactorLogin (HTTP GET):

[Authorize]
public async Task<IActionResult> ToFactorLogin(string email, bool isPersistent)
{
    var user = await _userManager.FindByEmailAsync(email);

    if (user == null)
    {
        return BadRequest();
    }

    var providers = await _userManager.GetValidTwoFactorProvidersAsync(user);
    TwoFactorLoginViewModel model = new TwoFactorLoginViewModel();

    if (providers.Contains("Email"))
    {
        await SendCode(user);
        model.Provider = "Email";
        model.IsPersistent = isPersistent;
    }
    else if (providers.Contains("Phone"))
    {
        string smsCode = await _userManager.GenerateTwoFactorTokenAsync(user, "Phone");
        // SmsService smsService = new SmsService();
        // smsService.Send(user.PhoneNumber, smsCode);
        model.Provider = "Phone";
        model.IsPersistent = isPersistent;
    }

    return View(model);
}

TwoFactorLogin (HTTP POST):

[Authorize]
[HttpPost]
public async Task<IActionResult> ToFactorLogin(TwoFactorLoginViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();

        if (user == null)
        {
            ModelState.AddModelError("", "The email entered is not valid!");
            return View();
        }

        var result = await _signInManager.TwoFactorSignInAsync(model.Provider, model.Code, model.IsPersistent, true);

        if (result.Succeeded)
        {
            return LocalRedirect("~/Index");
        }
        else if (result.IsLockedOut)
        {
            ModelState.AddModelError("", "User account locked out!");
            return View();
        }
        else
        {
            ModelState.AddModelError("", "The entered code is not correct");
            return View();
        }
    }

    return View(model);
}

Class IdentityDataBaseContext:

    public class IdentityDataBaseContext : IdentityDbContext<User>
{
    public IdentityDataBaseContext(DbContextOptions<IdentityDataBaseContext> options) : base(options)
    {
    }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<IdentityUser<string>>().ToTable("Users", "identity");
        modelBuilder.Entity<IdentityRole<string>>().ToTable("Roles", "identity");
        modelBuilder.Entity<IdentityRoleClaim<string>>().ToTable("RoleClaims", "identity");
        modelBuilder.Entity<IdentityUserClaim<string>>().ToTable("UserClaims", "identity");
        modelBuilder.Entity<IdentityUserLogin<string>>().ToTable("UserLogins", "identity");
        modelBuilder.Entity<IdentityUserRole<string>>().ToTable("UserRoles", "identity");
        modelBuilder.Entity<IdentityUserToken<string>>().ToTable("UserTokens", "identity");

        modelBuilder.Entity<IdentityUserLogin<string>>().HasKey(p => new { p.LoginProvider, p.ProviderKey });
        modelBuilder.Entity<IdentityUserRole<string>>().HasKey(p => new { p.UserId, p.RoleId });
        modelBuilder.Entity<IdentityUserToken<string>>().HasKey(p => new { p.UserId, p.LoginProvider });
    }
}

Class IdentityConfig:

    public static class IdentityConfig
{
    public static IServiceCollection AddIdentityService(this IServiceCollection services, IConfiguration configuration)
    {
        string connection = configuration["ConnectionString:SqlServer"];
        services.AddDbContext<IdentityDataBaseContext>(option => option.UseSqlServer(connection));

        services.AddIdentity<User, IdentityRole>().AddEntityFrameworkStores<IdentityDataBaseContext>()
            .AddDefaultTokenProviders().AddRoles<IdentityRole>().AddErrorDescriber<CustomIdentityError>();

        services.Configure<IdentityOptions>(options =>
        {
            options.Password.RequireDigit = true;
            options.Password.RequiredLength = 8;
            options.Password.RequireLowercase = true;
            options.Password.RequireNonAlphanumeric = true;
            options.Password.RequireUppercase = true;
            options.Password.RequiredUniqueChars = 1;
            options.User.RequireUniqueEmail = true;
            options.Lockout.MaxFailedAccessAttempts = 5;
            options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
        });
        return services;
    }
}

Thank you for helping me to solve this problem ❤


Solution

  • As the error said, you could try following to config 3-part composite key.

    modelBuilder.Entity<IdentityUserToken<string>>().HasKey(p => new { p.UserId, p.LoginProvider,p.Name });