Best Practices for Password Reset Flow with C# examples

Osama HaiDer
3 min readAug 2, 2024

--

Password reset functionality is a crucial part of any web application that handles user accounts. Implementing a secure and user-friendly password reset flow helps protect users’ accounts from unauthorized access. Here are some best practices to follow, along with examples in C#.

1. Use Secure Random Tokens

Wrong Way:

Generating simple tokens like a 4-digit or 6-digit code is highly insecure as they can be easily guessed or brute-forced.

// Using a simple 4-digit code is not secure
public static string GenerateInsecureToken()
{
Random random = new Random();
int token = random.Next(1000, 9999); // Generates a 4-digit token
return token.ToString();
}

Right Way:

Ensure tokens are generated from a secure random source and are sufficiently long (at least 64 characters). Using Base64 encoding makes the token both secure and manageable.

using System.Security.Cryptography;

public static string GenerateSecureToken(int length = 64)
{
using (var rng = new RNGCryptoServiceProvider())
{
var tokenData = new byte[length];
rng.GetBytes(tokenData);
return Convert.ToBase64String(tokenData);
}
}

2. Hash Tokens in the Database

Wrong Way:

Storing tokens in plain text in the database can lead to serious security issues if the database is compromised.

// Storing the token directly in the database
string token = GenerateSecureToken();
// Save `token` to the database

Right Way:

Store only the hashed version of tokens in the database to protect against database theft.

using System.Security.Cryptography;
using System.Text;

public static string HashToken(string token)
{
using (var sha256 = SHA256.Create())
{
var tokenBytes = Encoding.UTF8.GetBytes(token);
var hashedBytes = sha256.ComputeHash(tokenBytes);
return Convert.ToBase64String(hashedBytes);
}
}

// Storing the hashed token
string token = GenerateSecureToken();
string hashedToken = HashToken(token);
// Save `hashedToken` to the database

3. Expire Tokens

Wrong Way:

Allowing tokens to remain valid indefinitely can expose the application to prolonged risk.

// Token with no expiration
PasswordResetToken resetToken = new PasswordResetToken
{
Token = HashToken(GenerateSecureToken())
};

// Save `resetToken` to the database without an expiration

Right Way:

Set a reasonable expiration time for tokens to limit the window of opportunity for an attack.

public class PasswordResetToken
{
public string Token { get; set; }
public DateTime Expiration { get; set; }
}

// Generating a token with an expiration time
PasswordResetToken resetToken = new PasswordResetToken
{
Token = HashToken(GenerateSecureToken()),
Expiration = DateTime.UtcNow.AddHours(1) // Token expires in 1 hour
};

// Save `resetToken` to the database

4. Implement Rate Limiting

Wrong Way:

Allowing unlimited password reset requests can open the application to abuse and denial of service attacks.

// No rate limiting in place
public void RequestPasswordReset(string userEmail)
{
// Generate and send reset token
}

Right Way:

To prevent abuse, limit the number of password reset requests that can be made within a given time frame.

public class RateLimiter
{
private readonly Dictionary<string, DateTime> _requestLog = new Dictionary<string, DateTime>();
private readonly TimeSpan _timeFrame = TimeSpan.FromMinutes(15);
private readonly int _maxRequests = 5;

public bool IsAllowed(string userEmail)
{
if (_requestLog.TryGetValue(userEmail, out DateTime lastRequestTime))
{
if (lastRequestTime > DateTime.UtcNow.Subtract(_timeFrame))
{
return false; // Limit exceeded
}
}

_requestLog[userEmail] = DateTime.UtcNow;
return true;
}
}

// Check if the request is allowed before generating a token
RateLimiter rateLimiter = new RateLimiter();
if (rateLimiter.IsAllowed(userEmail))
{
// Generate and send reset token
}
else
{
// Inform the user about rate limiting
}

5. Secure Token Delivery

Wrong Way:

Sending tokens via insecure channels or exposing them in URLs can lead to interception and misuse.

// Sending token in plain text via email
public void SendPasswordResetEmail(string email, string token)
{
string resetLink = $"http://example.com/reset-password?token={token}";

// Code to send email
// Sending email without secure protocols
}

Right Way:

Send tokens via secure email channels and consider using additional layers of authentication, such as two-factor authentication.

public void SendPasswordResetEmail(string email, string token)
{
string resetLink = $"https://example.com/reset-password?token={token}";

// Code to send email
// Ensure the email sending service uses secure protocols (e.g., TLS)
}

// Generate token and send email
string token = GenerateSecureToken();
SendPasswordResetEmail(userEmail, token);

Conclusion

Implementing a secure password reset flow is essential to protect user accounts. By generating secure tokens, hashing them in the database, expiring them appropriately, implementing rate limiting, and delivering tokens securely, you can significantly enhance the security of your password reset process. Incorporate these practices into your .NET application to ensure a robust and secure password reset mechanism.

--

--

Osama HaiDer

SSE at TEO International | .Net | Azure | AWS | Web APIs | C#