Complete implementation of secure password reset functionality per DAY7-PRD.md specifications. Changes: - Domain: PasswordResetToken entity with 1-hour expiration and single-use constraint - Domain Events: PasswordResetRequestedEvent and PasswordResetCompletedEvent - Repository: IPasswordResetTokenRepository with token management and invalidation - Commands: ForgotPasswordCommand and ResetPasswordCommand with handlers - Security: MemoryRateLimitService (3 requests/hour) and IRateLimitService interface - API: POST /api/Auth/forgot-password and POST /api/Auth/reset-password endpoints - Infrastructure: EF Core configuration and database migration for password_reset_tokens table - Features: Email enumeration prevention, SHA-256 token hashing, refresh token revocation on password reset - Test: PowerShell test script for password reset flow verification Security Enhancements: - Rate limiting: 3 forgot-password requests per hour per email - Token security: SHA-256 hashing, 1-hour expiration, single-use only - Privacy: Always return success message to prevent email enumeration - Audit trail: IP address and User Agent logging for security monitoring - Session revocation: All refresh tokens revoked after successful password reset Database: - New table: password_reset_tokens with indexes for performance - Columns: id, user_id, token_hash, expires_at, used_at, ip_address, user_agent, created_at 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
82 lines
2.6 KiB
C#
82 lines
2.6 KiB
C#
using ColaFlow.Shared.Kernel.Common;
|
|
using ColaFlow.Modules.Identity.Domain.Aggregates.Users;
|
|
|
|
namespace ColaFlow.Modules.Identity.Domain.Entities;
|
|
|
|
/// <summary>
|
|
/// Password reset token entity with enhanced security.
|
|
/// Lifetime: 1 hour (short expiration for security).
|
|
/// Single-use only (cannot be reused).
|
|
/// </summary>
|
|
public sealed class PasswordResetToken : Entity
|
|
{
|
|
public UserId UserId { get; private set; } = null!;
|
|
public string TokenHash { get; private set; } = string.Empty;
|
|
public DateTime ExpiresAt { get; private set; }
|
|
public DateTime? UsedAt { get; private set; }
|
|
public string? IpAddress { get; private set; }
|
|
public string? UserAgent { get; private set; }
|
|
public DateTime CreatedAt { get; private set; }
|
|
|
|
// Private constructor for EF Core
|
|
private PasswordResetToken() : base()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Factory method to create new password reset token.
|
|
/// </summary>
|
|
/// <param name="userId">User ID requesting password reset</param>
|
|
/// <param name="tokenHash">SHA-256 hash of the reset token</param>
|
|
/// <param name="expiresAt">Expiration time (typically 1 hour from creation)</param>
|
|
/// <param name="ipAddress">IP address of the requester</param>
|
|
/// <param name="userAgent">User agent of the requester</param>
|
|
/// <returns>New password reset token instance</returns>
|
|
public static PasswordResetToken Create(
|
|
UserId userId,
|
|
string tokenHash,
|
|
DateTime expiresAt,
|
|
string? ipAddress = null,
|
|
string? userAgent = null)
|
|
{
|
|
return new PasswordResetToken
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
UserId = userId,
|
|
TokenHash = tokenHash,
|
|
ExpiresAt = expiresAt,
|
|
IpAddress = ipAddress,
|
|
UserAgent = userAgent,
|
|
CreatedAt = DateTime.UtcNow
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if token has expired.
|
|
/// </summary>
|
|
public bool IsExpired => DateTime.UtcNow > ExpiresAt;
|
|
|
|
/// <summary>
|
|
/// Check if token has been used.
|
|
/// </summary>
|
|
public bool IsUsed => UsedAt.HasValue;
|
|
|
|
/// <summary>
|
|
/// Check if token is valid (not expired and not used).
|
|
/// </summary>
|
|
public bool IsValid => !IsExpired && !IsUsed;
|
|
|
|
/// <summary>
|
|
/// Mark the token as used.
|
|
/// Can only be used once for security.
|
|
/// </summary>
|
|
/// <exception cref="InvalidOperationException">Thrown if token is not valid</exception>
|
|
public void MarkAsUsed()
|
|
{
|
|
if (!IsValid)
|
|
throw new InvalidOperationException("Token is not valid for password reset");
|
|
|
|
UsedAt = DateTime.UtcNow;
|
|
}
|
|
}
|