Reset password implementation inside .NET Core Web API

Anıl Karaşah
4 min readSep 11, 2023

Everybody must have used the “Forgot Password?” button at least once in their lifetime. You enter your email address, and you get back a password reset link to your mailbox. It does seem easy, right? Well, not really.

A typical example of a password reset page.

I’ve wanted to cover this topic, because it contains many fields of Computer Science. Cryptography, race conditions, cybersecurity, etc.

Have I got your attention? Then, let’s dive right in.

To begin with, let me explain why this is an important task to consider thoroughly.

  • An attacker might try to attack using a brute-force algorithm. If the back-end returns 200 OK or 404 Not Found result, it would be a piece of cake for the attacker, as they would know if that email address exists in the system.
  • You may have noticed the token in the password reset URL. Tokens look like a randomly generated string. Some implementations use Json Web Tokens for this URL, which is a developer-friendly approach but not secure enough. If an attacker were to gather access to the JWT’s secret key, they would generate these password reset links as much as they want. Maybe concatenating the user’s ID or password hash to the secret key is better? Well, certainly. But now, you are out of the developer-friendly zone.

Let’s see how to implement a secure “Forgot Password” link.

202 Accepted is the best status code to return in this implementation. This way, back-end will always return the same response, no matter the email address exists, or not. To go a step further, queue these operations to a background service, and send a response right away.

// AuthModule.cs

// generates a password reset URL and sends it to the user
app.MapGet("reset-password", (
string emailAddress,
IBackgroundJobClient jobClient) =>
{
jobClient.Enqueue<ForgotPasswordJobHandler>(
handler => handler.RunAsync(emailAddress));
return Results.Accepted();
});

// validates token and updates user's password
app.MapPatch("reset-password", (
[FromQuery]string token,
[FromBody]string newPassword,
UserService userService) =>
{
var result = await userService.ResetPasswordAsync(token, newPassword);

return result.IsFailure
? Results.BadRequest(result.Error)
: Results.Ok();
});
// ForgotPasswordJobHandler.cs
public async Task RunAsync(string emailAddress)
{
// check if any user exists with the given email address
User? user = await _userRepository.GetByEmailAddressAsync(emailAddress);
if (user is null) return;

// generate a random token
string token = PasswordUtils.GenerateBase62(length: 64);
string hashedToken = PasswordUtils.EncryptWithSha256(token);
DateTimeOffset expiryDate = DateTimeOffset.UtcNow.AddMinutes(15);

// persist the token in database
var resetPasswordEntry = new ResetPasswordEntry(user.Id, hashedToken, expiryDate);
await _resetPasswordEntryRepository.CreateAsync(resetPasswordEntry);

// send the reset URL to the user via email
string baseUrl = _configuration["Application:BaseHost"]; // this host can be your front-end
string resetPasswordUrl = $"{baseUrl}/auth/reset-password?token={token}";

await _emailSender.SendAsync(new UserPasswordConfirmationEmailDto(
emailAddress,
user.Id,
resetPasswordUrl,
expiryDate));
}

We have sent a password reset link the user. Now, we have to validate the token when the user tries to visit this URL.

// UserService.cs
public async Task<Result> ResetPasswordAsync(
string token,
string emailAddress)
{
// hash the token, again
string hashedToken = PasswordUtils.EncryptWithSha256(token);

// check if any entry exists with the given token
ResetPasswordEntry? resetPasswordEntry = await _resetPasswordEntryRepository
.GetResetPasswordEntryAsync(hashedToken);

if (resetPasswordEntry is null || resetPasswordEntry.TokenExpiresAtUtc < DateTimeOffset.UtcNow)
return Result.Failure(DomainErrors.ResetPasswordEntry.InvalidToken);

// run the password reset logic (main operation)
bool isSuccessful = await _resetPasswordEntryRepository.RunPasswordResetOperationsAsync(
resetPasswordEntry.UserId, hashedToken, request.Password);

if (!isSuccessful) return Result.Failure(DomainErrors.ResetPasswordEntry.InvalidToken);

// additionally, notify the user that their password have changed

return Result.Success();
}

Until this point, everything seems familiar. But, the main operation is a bit more complex. Before showing off the code, let me go step-by-step:

  1. Validate that the token exists in database and it hasn’t expired.
  2. Start a database transaction.
  3. Query a SELECT command on ResetPasswordEntry table which has the same user ID, and add “FOR UPDATE” statement at the end of the query. This will lock every row for the same user and prevent race conditions. (please note that syntax may change based on the database management system)
  4. If none of the tokens match with the requested one, rollback and terminate.
  5. If any token matches, delete every entry from the database for the same user. This way, after password reset operation completes, none of the URL’s will be valid.
  6. Hash the new password and update the user.
  7. Commit changes.

C# code looks like this:

// ResetPasswordEntryRepository.cs

public async Task<bool> RunPasswordResetOperationsAsync(
UserId userId,
string token,
string newPassword)
{
// 1) we have already validated the token

// 2) start the transaction
await using IDbContextTransaction transaction = await _dbContext.Database.BeginTransactionAsync();

try
{
// retrieve all tokens for the given user, and lock the rows for update
var allTokensForUser = await _dbContext.Set<ResetPasswordEntry>()
.FromSqlRaw(
"""SELECT * FROM reset_password_entry WHERE "UserId" = @UserId FOR UPDATE""",
new NpgsqlParameter("@UserId", userId.Value))
.ToListAsync(cancellationToken);

// check if the token is still valid
ResetPasswordEntry? matchedToken = allTokensForUser.FirstOrDefault(entry => entry.Token == token);

if (matchedToken is null)
{
await transaction.RollbackAsync(cancellationToken);
return false;
}

// delete all tokens for the given user
await _dbContext.Set<ResetPasswordEntry>()
.Where(entry => entry.UserId == userId)
.ExecuteDeleteAsync(cancellationToken);

// update password of user
string hashedPassword = PasswordUtils.EncryptPassword(newPassword);

await _dbContext.Staves
.Where(user => user.Id == userId)
.ExecuteUpdateAsync(
calls => calls.SetProperty(user => user.Password, hashedPassword),
cancellationToken);

await transaction.CommitAsync(cancellationToken);
return true;
}
catch (Exception)
{
await transaction.RollbackAsync(cancellationToken);
return false;
}
}

You can additionally set a Cron Job to remove expired tokens from the database, just in case the user decides not to change their password.

As I have said in the beginning, you have to consider too many side cases to implement such an easy-looking task.

Since this topic interested me so much, I wanted to share my implementation with you. Please, feel free to share your thoughts.

Some topics of Computer Science are really fascinating 🤩

References:

--

--