46

I am looking for a way to disable the user instead of deleting them from the system, this is to keep the data integrity of the related data. But seems ASPNET identity only offers Delete Acccount.

There is a new Lockout feature, but it seems to lockout can be controlled to disable user, but only lock the user out after certain number of incorrect password tries.

Any other options?

Josh Gallagher
  • 5,211
  • 2
  • 33
  • 60
anIBMer
  • 1,159
  • 2
  • 12
  • 20

10 Answers10

81

When you create a site with the Identity bits installed, your site will have a file called "IdentityModels.cs". In this file is a class called ApplicationUser which inherits from IdentityUser.

// You can add profile data for the user by adding more properties to your ApplicationUser class, please visit https://devblogs.microsoft.com/aspnet/customizing-profile-information-in-asp-net-identity-in-vs-2013-templates/ to learn more.
public class ApplicationUser : IdentityUser

There is a nice link in the comments there, for ease click here

This tutorial tells you exactly what you need to do to add custom properties for your user.

And actually, don't even bother looking at the tutorial.

  1. add a property to the ApplicationUser class, eg:

    public bool? IsEnabled { get; set; }

  2. add a column with the same name on the AspNetUsers table in your DB.

  3. boom, that's it!

Now in your AccountController, you have a Register action as follows:

public async Task<ActionResult> Register(RegisterViewModel model)
        {
            if (ModelState.IsValid)
            {
                var user = new ApplicationUser { UserName = model.Email, Email = model.Email, IsEnabled = true };
                var result = await UserManager.CreateAsync(user, model.Password);
                if (result.Succeeded)

I've added the IsEnabled = true on the creation of the ApplicationUser object. The value will now be persisted in your new column in the AspNetUsers table.

You would then need to deal with checking for this value as part of the sign in process, by overriding PasswordSignInAsync in ApplicationSignInManager.

I did it as follows:

public override Task<SignInStatus> PasswordSignInAsync(string userName, string password, bool rememberMe, bool shouldLockout)
    {
        var user = UserManager.FindByEmailAsync(userName).Result;

        if ((user.IsEnabled.HasValue && !user.IsEnabled.Value) || !user.IsEnabled.HasValue)
        {
            return Task.FromResult<SignInStatus>(SignInStatus.LockedOut);
        }

        return base.PasswordSignInAsync(userName, password, rememberMe, shouldLockout);
    }

Your mileage may vary, and you may not want to return that SignInStatus, but you get the idea.

ozz
  • 5,098
  • 1
  • 50
  • 73
  • 3
    Do you need to check if the user exists to prevent a NRE on user.IsEnabled? I have the following: `if (user!= null && !user.IsEnabled) { return Task.FromResult(SignInStatus.LockedOut); }`. The reason I don't check for HasValue is because I am making IsEnabled a required field – Eitan K Sep 09 '16 at 19:58
  • This is a better way. As we are overriding `PasswordSignInAsync`. If we don't do this and `PasswordSignInAsync` returns `Success`. Then the request become authenticated and System can break. As there can be some logic written `Request.IsAuthenticated`. Thanks a lot :) – Jawand Singh Mar 12 '18 at 10:11
  • 2
    Would it be advisable to override the method CanSignIn instead? Overriding the passwordsignin method does not check other methods of signin? You can see the entire signinmanager here https://github.com/aspnet/AspNetCore/blob/master/src/Identity/src/Identity/SignInManager.cs – Watson Jan 02 '19 at 18:20
  • You could change your if statement to if (!user.IsEnabled.HasValue || !user.IsEnabled.Value) or just use a bool instead of bool?. It's not really clear that there's any benefits of making it nullable. – Shoejep May 14 '19 at 08:29
27

The default LockoutEnabled property for a User is not the property indicating if a user is currently being locked out or not. It's a property indicating if the user should be subject to lockout or not once the AccessFailedCount reaches the MaxFailedAccessAttemptsBeforeLockout value. Even if the user is locked out, its only a temporary measure to bar the user for the duration of LockedoutEnddateUtc property. So, to permanently disable or suspend a user account, you might want to introduce your own flag property.

Mohsen Esmailpour
  • 11,224
  • 3
  • 45
  • 66
user2813261
  • 625
  • 1
  • 8
  • 18
16

You don't need to create a custom property. The trick is to set the LockoutEnabled property on the Identity user AND set the LockoutoutEndDateUtc to a future date from your code to lockout a user. Then, calling the UserManager.IsLockedOutAsync(user.Id) will return false.

Both the LockoutEnabled and LockoutoutEndDateUtc must meet the criteria of true and future date to lockout a user. If, for example, the LockoutoutEndDateUtc value is 2014-01-01 00:00:00.000 and LockoutEnabled is true, calling theUserManager.IsLockedOutAsync(user.Id) will still return true. I can see why Microsoft designed it this way so you can set a time span on how long a user is locked out.

However, I would argue that it should be if LockoutEnabled is true then user should be locked out if LockoutoutEndDateUtc is NULL OR a future date. That way you don't have to worry in your code about setting two properties (LockoutoutEndDateUtc is NULL by default). You could just set LockoutEnabled to true and if LockoutoutEndDateUtc is NULL the user is locked out indefinitely.

Ortund
  • 8,095
  • 18
  • 71
  • 139
Stephen Peterson
  • 1,135
  • 6
  • 7
  • 30
    It's been upvoted a lot, but this answer is wrong because, as others have said, you are misunderstanding the LockoutEnabled property. It indicates whether the user "can be locked out". There SHOULD be a built in property, simply called "LockedOut" or similar. Some more info: http://www.jamessturtevant.com/posts/ASPNET-Identity-Lockout/ – ozz Feb 09 '16 at 13:07
  • 1
    LockoutEnabled true, and a LockoutEndDateUtc in the future implies .IsLockedOutAsync TRUE, not false. (1st ppg.) If 2014-01-01 is in the past (as it was when this was originally posted), .IsLockedOutAsync would "still" return FALSE, not true. The third ppg is a vague notion. How did this get any upvotes at all? – fortboise Aug 19 '16 at 20:48
  • Yes, you are correct, 2014-01-01 is wrong - it should have been a future date, like DateTime.MaxValue. If LockoutEnabled is configured to be true, when the user goes over the number of MaxFailedAccessAttempts configured, the LockoutEndate field is set from the current time to whatever the DefaultLockoutTimeSpan is configured for. My 3rd ppg, as you mentioned, is a bit vague. In short, I agree there should be a bool field that flags whether a person is locked out, and not having to manually code to set a future date into LockoutEnd field to accomplish the same thing. – Stephen Peterson Aug 20 '16 at 21:55
11

You would need to introduce your own flag into a custom IdentityUser-derived class and implement/enforce your own logic about enable/disable and preventing the user from logging in if disabled.

Brock Allen
  • 7,385
  • 19
  • 24
11

This all I did actually:

    var lockoutEndDate = new DateTime(2999,01,01);
    UserManager.SetLockoutEnabled(userId,true);
    UserManager.SetLockoutEndDate(userId, lockoutEndDate);

Which is basically to enable lock out (if you don't do this by default already, and then set the Lockout End Date to some distant value.

t_plusplus
  • 4,079
  • 5
  • 45
  • 60
  • This would work, and is probably the method I'll go with, but it seems daft that we have to do things like this, why can't there just be a locked out flag?!?! – ataraxia Aug 22 '20 at 15:35
5

You can use these classes... A clean implemantation of ASP.NET Identity... It's my own code. int is here for primary key if you want different type for primary key you can change it.

IdentityConfig.cs

public class ApplicationUserManager : UserManager<ApplicationUser, int>
{
    public ApplicationUserManager(IUserStore<ApplicationUser, int> store)
        : base(store)
    {
    }
    public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
    {
        var manager = new ApplicationUserManager(new ApplicationUserStore(context.Get<ApplicationContext>()));
        manager.UserValidator = new UserValidator<ApplicationUser, int>(manager)
        {
            AllowOnlyAlphanumericUserNames = false,
            RequireUniqueEmail = true
        };
        manager.PasswordValidator = new PasswordValidator
        {
            RequiredLength = 6,
            RequireNonLetterOrDigit = true,
            RequireDigit = true,
            RequireLowercase = true,
            RequireUppercase = true,
        };
        manager.UserLockoutEnabledByDefault = false;
        var dataProtectionProvider = options.DataProtectionProvider;
        if (dataProtectionProvider != null)
        {
            manager.UserTokenProvider =
                new DataProtectorTokenProvider<ApplicationUser, int>(
                    dataProtectionProvider.Create("ASP.NET Identity"));
        }
        return manager;
    }
}
public class ApplicationSignInManager : SignInManager<ApplicationUser, int>
{
    public ApplicationSignInManager(ApplicationUserManager userManager, IAuthenticationManager authenticationManager) :
        base(userManager, authenticationManager) { }
    public override Task<ClaimsIdentity> CreateUserIdentityAsync(ApplicationUser user)
    {
        return user.GenerateUserIdentityAsync((ApplicationUserManager)UserManager);
    }
    public static ApplicationSignInManager Create(IdentityFactoryOptions<ApplicationSignInManager> options, IOwinContext context)
    {
        return new ApplicationSignInManager(context.GetUserManager<ApplicationUserManager>(), context.Authentication);
    }
}
public class ApplicationRoleManager : RoleManager<ApplicationRole, int>
{
    public ApplicationRoleManager(IRoleStore<ApplicationRole, int> store)
        : base(store)
    {
    }
}
public class ApplicationRoleStore : RoleStore<ApplicationRole, int, ApplicationUserRole>
{
    public ApplicationRoleStore(ApplicationContext db)
        : base(db)
    {
    }
}
public class ApplicationUserStore : UserStore<ApplicationUser, ApplicationRole, int,
ApplicationLogin, ApplicationUserRole, ApplicationClaim>
{
    public ApplicationUserStore(ApplicationContext db)
        : base(db)
    {
    }
}

IdentityModel.cs

public class ApplicationUser : IdentityUser<int, ApplicationLogin, ApplicationUserRole, ApplicationClaim>
{   
    //your property 
    //flag for users state (active, deactive or enabled, disabled)
    //set it false to disable users
    public bool IsActive { get; set; }
    public ApplicationUser()
    {
    }
    public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser, int> manager)
    {
        var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
        return userIdentity;
    }
}
public class ApplicationUserRole : IdentityUserRole<int>
{
}
public class ApplicationLogin : IdentityUserLogin<int>
{
    public virtual ApplicationUser User { get; set; }
}
public class ApplicationClaim : IdentityUserClaim<int>
{
    public virtual ApplicationUser User { get; set; }
}
public class ApplicationRole : IdentityRole<int, ApplicationUserRole>
{
    public ApplicationRole()
    {
    }
}
public class ApplicationContext : IdentityDbContext<ApplicationUser, ApplicationRole, int, ApplicationLogin, ApplicationUserRole, ApplicationClaim>
{
    //web config connectionStringName DefaultConnection change it if required
    public ApplicationContext()
        : base("DefaultConnection")
    {
        Database.SetInitializer<ApplicationContext>(new CreateDatabaseIfNotExists<ApplicationContext>());
    }
    public static ApplicationContext Create()
    {
        return new ApplicationContext();
    }
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
        modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>();
        modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
    }
}  
Community
  • 1
  • 1
Deniz Kısır
  • 231
  • 3
  • 9
5

Ozz is correct, however it may be adviseable to look at the base class and see if you can find a method that is checked for all signin angles - I think it might be CanSignIn?

Now that MS is open source you can see their implementation:

https://github.com/aspnet/AspNetCore/blob/master/src/Identity/src/Identity/SignInManager.cs

(Url has changed to:

https://github.com/aspnet/AspNetCore/blob/master/src/Identity/Core/src/SignInManager.cs)

    public class CustomSignInManager : SignInManager<ApplicationUser>  
{
    public CustomSignInManager(UserManager<ApplicationUser> userManager,
        IHttpContextAccessor contextAccessor,
        IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory,
        IOptions<IdentityOptions> optionsAccessor,
        ILogger<SignInManager<ApplicationUser>> logger,
        IAuthenticationSchemeProvider schemes) : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes)
    {

    }


    public override async Task<bool> CanSignInAsync(ApplicationUser user)
    {
        if (Options.SignIn.RequireConfirmedEmail && !(await UserManager.IsEmailConfirmedAsync(user)))
        {
            Logger.LogWarning(0, "User {userId} cannot sign in without a confirmed email.", await UserManager.GetUserIdAsync(user));
            return false;
        }
        if (Options.SignIn.RequireConfirmedPhoneNumber && !(await UserManager.IsPhoneNumberConfirmedAsync(user)))
        {
            Logger.LogWarning(1, "User {userId} cannot sign in without a confirmed phone number.", await UserManager.GetUserIdAsync(user));
            return false;
        }

        if (UserManager.FindByIdAsync(user.Id).Result.IsEnabled == false)
        {
            Logger.LogWarning(1, "User {userId} cannot sign because it's currently disabled", await UserManager.GetUserIdAsync(user));
            return false;
        }

        return true;
    }
}

Also consider overriding PreSignInCheck, which also calls CanSignIn:

protected virtual async Task<SignInResult> PreSignInCheck(TUser user)
        {
            if (!await CanSignInAsync(user))
            {
                return SignInResult.NotAllowed;
            }
            if (await IsLockedOut(user))
            {
                return await LockedOut(user);
            }
            return null;
        }
Watson
  • 1,385
  • 1
  • 15
  • 36
  • 1
    Identity v3 has added `options.SignIn.RequireConfirmedAccount` & `IUserConfirmation`, so you don't need to replace SignInManager anymore. – Jeremy Lakeman Oct 12 '20 at 05:09
4

I upvoted Watson, as there is another public method in SignInManager that accepts TUser user instead of string userName. The accepted answer only suggests overriding the method with the username signature. Both should really be overridden, otherwise there is a means of signing in a disabled user. Here are the two methods in the base implementation:

public virtual async Task<SignInResult> PasswordSignInAsync(string userName, string password, bool isPersistent, bool lockoutOnFailure)
{
  var user = await UserManager.FindByNameAsync(userName);
  if (user == null)
  {
    return SignInResult.Failed;
  }

  return await PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure);
}

public virtual async Task<SignInResult> PasswordSignInAsync(User user, string password, bool isPersistent, bool lockoutOnFailure)
{
  if (user == null)
  {
    throw new ArgumentNullException(nameof(user));
  }

  var attempt = await CheckPasswordSignInAsync(user, password, lockoutOnFailure);
  return attempt.Succeeded
      ? await SignInOrTwoFactorAsync(user, isPersistent)
      : attempt;
}

Overriding CanSignIn seems like a better solution to me, as it gets called by PreSignInCheck, which is called in CheckPasswordSignInAsync. From what I can tell from the source, overriding CanSignIn should cover all scenarios. Here is a simple implementation that could be used:

public override async Task<bool> CanSignInAsync(User user)
{
  var canSignIn = user.IsEnabled;

  if (canSignIn) { 
    canSignIn = await base.CanSignInAsync(user);
  }
  return canSignIn;
}
DanO
  • 911
  • 1
  • 8
  • 16
3

In asp.net Core Identity v3, a new way of preventing a user from signing in has been added. Previously you could require that an account has a confirmed email address or phone number, now you can specify .RequireConfirmedAccount. The default implementation of the IUserConfirmation<> service will behave the same as requiring a confirmed email address, provide your own service to define what confirmation means.

    public class User : IdentityUser<string>{
        public bool IsEnabled { get; set; }
    }

    public class UserConfirmation : IUserConfirmation<User>
    {
        public Task<bool> IsConfirmedAsync(UserManager<User> manager, User user) => 
            Task.FromResult(user.IsEnabled);
    }

    services.AddScoped<IUserConfirmation<User>, UserConfirmation>();
    services.AddIdentity<User, IdentityRole>(options => { 
        options.SignIn.RequireConfirmedAccount = true;
    } );

Jeremy Lakeman
  • 9,515
  • 25
  • 29
0

You need to implement your own UserStore to remove the identity.

Also this might help you.

MusicLovingIndianGirl
  • 5,909
  • 9
  • 34
  • 65