Application security
.NET .NET Core ASP.NET Core Identity Best Practices C# OWASP Security Visual Studio

How to Implement Account Unlocking in ASP.NET Core Identity

Welcome to today’s post.

In today’s post I will be discussing the unlocking of accounts after lockouts, and how we can implement them using ASP.NET Core identity.

In previous posts I showed how to implement account lockouts and account password changes. In this post I will be showing how to release or unlock an account. There are a few reasons why we would need to lock an account, however there is one reason why we would need to unlock an account, and that is be able to login to the account after a lockout.

When are Accounts Unlocked and the Security Implications

There are many ways in which a locked account can be unlocked. One such way is to request a password reset, which sends us an email with new default password, which we can use to log back into our account. During the process, we could also be sent an SMS code (a 2-factor code) that we are required to enter in a provided link before we can receive another email with a new default password.

Under what situations should we allow an account to be unlocked?

Can we allow any user to request any account to be unlocked? No. The reason is that we do not want anyone who is unauthorized to be able to unlock an account. In the same way that we ring up our bank to request our account to be unlocked, the customer support will ask us what our personal details are: DOB, secret questions etc. to establish that we say who we are.  

There are a few ways we can guarantee that an authorized person will be able to unlock an account through an online system:

  1. They are already logged in and authenticated.
  2. They have requested a link to be sent to their email or mobile.
  3. They are already an account or security administrator of the system.

Account Unlocking with ASP.NET Identity Core

I will be showing how to implement unlocking using method 1.

We will be using a protected API to allow the user to login before an attempt to run the API is permitted. The API will have one parameter posted to it, the username or login unique within the system user accounts. Provided the API is protected, the other details including the currently logged in username and the roles the user is a member of from their claims will be sufficient to ascertain whether the user is permitted to unlock the nominated account.

So, the rules we have in place are:

  1. Ensure the user is authenticated.
  2. Ensure the target username exists within the user account table.
  3. Ensure the authenticated user matches the target username / account they wish to release or they are an administrator.

The two important fields to understand within the [AspNetUsers] users table within the unlocking process are:

[AccessFailedCount] 

and

[LockoutEnd]

When the account is unlocked, the following changes occur within the user account table:

The AccessFailedCount resets the failed attempts count from the maximum down to zero.

The [LockoutEnd] field resets the lockout expiry date back to NULL.

The state of the locked record is shown when querying the [AspNetUsers] users table:

In this case, I set the maximum attempts threshold to 3, by default it is 5.

The above changes ensure that the account after unlocking can be logged in.

Implementation of Account Unlocking using the ASP.NET Identity Core API

Based on the outline of the account unlocking method in the previous section, an implementation contained a method is shown below:

public async Task<AccountUnlockResponseModel> UnlockAccount(string userName)
{
    var responseStatus = new AccountUnlockResponseModel();
    var responseErrors = new List<AccountUnlockErrorResponseModel>();

    var user = _userManager.Users.Where(
        u => u.UserName == userName).SingleOrDefault();
    if (user == null)
    {
       	responseErrors.Add(new AccountUnlockErrorResponseModel()
            {
                ErrorCode = Constants.INVALID_USER_NAME,
                ErrorDescription = $"The user {userName} does not exist."
            });
        responseStatus.IsSuccessful = false;
        responseStatus.errors = responseErrors;
        return responseStatus;
    }

    // Verify current user is an administrator or the target user whose account 
    // is being unlocked.
    if ((!this._context.User.IsInRole(Constants.ADMINISTRATOR_ROLE) &&
         (this._context.User.Identity.Name != userName)))
    {
        responseErrors.Add(new AccountUnlockErrorResponseModel()
        {
            ErrorCode = Constants.USER_UNAUTHORIZED,
            ErrorDescription = 
  	$"The user {userName} is not authorized to perform this operation."
        });
              
  		responseStatus.IsSuccessful = false;
        responseStatus.errors = responseErrors;
        return responseStatus;
  	}

    // set [AccessFailedCount] and [LockoutEnd] back to defaults
    user.AccessFailedCount = 0;
    user.LockoutEnd = null;

    var rslt = await _userManager.UpdateAsync(user);
    if (!rslt.Succeeded)
    {
       	foreach (IdentityError err in rslt.Errors)
        {
            responseErrors.Add(new AccountUnlockErrorResponseModel()
            {
                ErrorCode = err.Code,
                ErrorDescription = err.Description
                	});
            	}
    	}
       
    responseStatus.errors = responseErrors;
    responseStatus.IsSuccessful = !responseStatus.errors.Any();

    return responseStatus;
}

Our controller API method does what it needs to do to satisfy the first condition, which is to ensure the user is authenticated. It then returns a response after the attempted unlocking of the account. It is shown below:

[HttpPost("api/[controller]/UnlockAccount")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public async Task<IActionResult> UnlockAccount(
    [FromBody] UnlockAccountViewModel unlockAccountViewModel)
{
    try
    {
       	var response = await _userService.UnlockAccount(unlockAccountViewModel.UserName);
        if (response.IsSuccessful)
          	 return Ok(response);
        return BadRequest(response);
    }
    catch (AppException ex)
    {
       	// return error message if there was an exception
        return BadRequest(new { message = ex.Message });
    }
}

Following the unlocking of the record, the record will after execution of a query on the user account table:

As I have demonstrated in the explanation and implementation, the recovery and unlocking of accounts should be part of a suite of tools that cover security best practices used in all applications whose goal is securing password protected accounts, protecting them from multiple access attempts and account recovery. The above code can also be extended to provide an email notification that lets the user know their account is locked with support details can be sent to the user to contact support to request that the account be unlocked. In addition, a separate email with a link that can be used to unlock the account to reinstate access. An additional layer of security using 2-factor SMS authentication is highly recommended for unlocking public user accounts.

That is all for today’s post.

I hope you found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial