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.
In a previous post I showed how to implement a basic identity service API using ASP.NET Core Identity that allowed us to register and authenticate users. There is one useful security feature that I did not include, and that was the ability to allow password changes. I am sure that there will be other features that we can cover in future, but the ability to change passwords is a key capability given that security has been a big topic of concern in the past decade.
Password Changes as a Best Practice
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:
- Account name or user login.
- Current password.
- 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 authorized to make account changes.
Implementation of a Password Change Process
When a password change process is implemented, it requires the following sub-tasks to be implemented:
- Check the username is valid.
- Check that user requesting the change corresponds to the logged in user or the logged in user has the role of an administrator.
- Check the user’s original password is valid.
- Rehash the password with the changed password.
- Update the user object with the updated hashed password.
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:
- Check the username is valid within our database.
- Check the currently logged in user is either an administrator or is the user that is having their account password changed.
- 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.
Andrew Halil is a blogger, author and software developer with expertise of many areas in the information technology industry including full-stack web and native cloud based development, test driven development and Devops.