feat(backend): Implement password reset flow (Phase 3)
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>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user