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

How to Implement Password Changes in ASP.NET Core Identity

Welcome to today’s blog.

In today’s blog I will be discussing password changes to user accounts and showing how this can be done using ASP.NET Core Identity.

As you will have experienced when using your own account that you will have had to change your own password on more than one occasion. These will have occurred for one of the following reasons:

1. Forgetting the password

You have simply forgotten your own password after not logging in for a while. It happens to nearly all of us.  I even forgot my own ATM pin as I do not use it that often!

2. Unusual activity on your account

When you get unusual emails relating to changes to your account that you have not accessed for a while, this can be due to compromised account details. In this case resetting the credentials and security questions on the account can re-secure the account.

3. Recent security incidents enforce credentials to be reset.

There have been some high-profile cases of well-known companies that have experienced security breaches, and the customer accounts of those companies have possibly been compromised. In some cases, they have been due to a hack on servers, and in other cases, entire lists of user account information gone into the wrong hands, compromising the credentials of those user accounts. Even though passwords are in many cases hashed, and even salted, the hackers can still use brute-force techniques to try to determine the hashed passwords. In these cases, all account credentials are reset.

I will now go over a password change process.

To change a password, we require the following inputs:

  1. Account name or user login.
  2. Current password.
  3. New password.

We will also require the user that is attempting to change the account be logged in before they can change the password. This is to ensure that the action is audited. There may be cases where someone else who has an account attempts to change the password of another account. This would in most cases considered to be a breach of security unless the account making the change is an account administrator who is authorised to make account changes.

The source to change the password is shown below:

public async Task<ChangePasswordResponseModel> ChangePassword(string userName, 
string originalPassword, string newPassword)
{
    var responseStatus = new ChangePasswordResponseModel();
    var responseErrors = new List<ChangePasswordErrorResponseModel>();

    var user = _userManager.Users.Where(u => 
  	    u.UserName == userName).SingleOrDefault();
    if (user == null)
    {
  	    responseErrors.Add(new ChangePasswordErrorResponseModel()
        {
            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 password 
    // is being modified.
    if ((!this._context.User.IsInRole(Constants.ADMINISTRATOR_ROLE) &&
        (this._context.User.Identity.Name != userName)))
    {
        responseErrors.Add(new ChangePasswordErrorResponseModel()
        {
            ErrorCode = Constants.USER_UNAUTHORIZED,
            ErrorDescription = 
    $"Only an Administrator or the user {userName} can perform this operation."
       	});
        responseStatus.IsSuccessful = false;
        responseStatus.errors = responseErrors;
        return responseStatus;
    }

    // Verify current password matches specified password.
    PasswordHasher<ApplicationUser> passwordHasher = 
  	    new PasswordHasher<ApplicationUser>();
    if (passwordHasher.VerifyHashedPassword(user, 
  	    user.PasswordHash, originalPassword) == PasswordVerificationResult.Failed)
    {
        _logger.LogInformation(
            "The original and specified user passwords do not match.");

       	// Bump up the access failed attempts for the user.
        await _userManager.AccessFailedAsync(user);

        responseErrors.Add(new ChangePasswordErrorResponseModel()
        {
            ErrorCode = Constants.INVALID_USER_PASSWORD_CODE,
            ErrorDescription = 
    "The original and specified user passwords do not match."
        });
        responseStatus.IsSuccessful = false;
        responseStatus.errors = responseErrors;
        return responseStatus;
    }

    // Save new hashed password to user record.
    user.PasswordHash = passwordHasher.HashPassword(user, newPassword);
            
    var rslt = await _userManager.UpdateAsync(user);
    if (!rslt.Succeeded)
    {
        foreach (IdentityError err in rslt.Errors)
        {
            responseErrors.Add(new ChangePasswordErrorResponseModel()
            {
                ErrorCode = err.Code,
                ErrorDescription = err.Description
            });
        }
    }
    responseStatus.errors = responseErrors;
    responseStatus.IsSuccessful = !responseStatus.errors.Any();

    return responseStatus;
}

The steps we are taking are as follows:

  1. Check the username is valid within our database.
  2. Check the currently logged in user is either an administrator or is the user that is having their account password changed.
  3. Verify that the new password’s hash and the current hashed password for the account are the same.

If the above three conditions are satisfied, then the password is changed using the following lines of code:

user.PasswordHash = passwordHasher.HashPassword(user, newPassword);
var rslt = await _userManager.UpdateAsync(user);

If the password hash condition is not satisfied, then the following code is executed:

await _userManager.AccessFailedAsync(user);

if (user.AccessFailedCount > Constants.MAXIMUM_LOGIN_ATTEMPTS)
{
	..
    user.LockoutEnd = DateTime.Now.AddYears(200);
}

As we saw in our pervious post, this will increase the invalid account login attempt count. If this exceeds the maximum permitted, then the account will be locked out with a lockout expiry date.

To verify the password has been changed we can view the password salt field in the [AspNetUsers] in the identity database.

The controller method is shown below:

[HttpPost("api/[controller]/ChangePassword")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public async Task<IActionResult> ChangePassword(
[FromBody]ChangePasswordViewModel changePasswordViewModel)
{
    try
    {
       	var response = await _userService.ChangePassword(
            changePasswordViewModel.UserName,
            changePasswordViewModel.OriginalPassword, 
            changePasswordViewModel.NewPassword);
        if (response.IsSuccessful)
            return Ok(response);
        return BadRequest(response);
    }
    catch (AppException ex)
    {	
        return BadRequest(new { message = ex.Message });	
    }
}

As you can see, the controller has JWT bearer authentication enforced with the [Authorize] attribute. In a previous post I showed how this was implemented.

After authentication, the request header of the web session carries the access token along with the claims of the authenticated user. The claims also include the user login name and the roles they are in.

In a previous post I showed how to obtain the HTTP context correctly within ASP.NET Core using the HttpContextAccessor and dependency injection. This allowed us to the use following context methods to fine-grain our security checks:

context.User.IsInRole(role)
context.User.Identity.Name

The above implementation of enforcing account password changes is just one of many variations that are possible, and you are encouraged to experiment with .NET Core Identity API and the various password security API methods. Most importantly, thorough testing of your security libraries is critical in development and testing environments before deploying them for use in a production environment.

That is all for today’s post.

I hope you found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial