Files
ColaFlow/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Entities/PasswordResetToken.cs
Yaojia Wang 1cf0ef0d9c 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>
2025-11-03 21:47:26 +01:00

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;
}
}