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:
Yaojia Wang
2025-11-03 21:47:26 +01:00
parent 3dcecc656f
commit 1cf0ef0d9c
19 changed files with 1276 additions and 0 deletions

View File

@@ -1,5 +1,7 @@
using ColaFlow.API.Models;
using ColaFlow.Modules.Identity.Application.Commands.ForgotPassword;
using ColaFlow.Modules.Identity.Application.Commands.Login;
using ColaFlow.Modules.Identity.Application.Commands.ResetPassword;
using ColaFlow.Modules.Identity.Application.Commands.VerifyEmail;
using ColaFlow.Modules.Identity.Application.Services;
using MediatR;
@@ -165,6 +167,47 @@ public class AuthController(
return Ok(new { message = "Email verified successfully" });
}
/// <summary>
/// Initiate password reset flow (sends email with reset link)
/// Always returns success to prevent email enumeration attacks
/// </summary>
[HttpPost("forgot-password")]
[AllowAnonymous]
public async Task<IActionResult> ForgotPassword([FromBody] ForgotPasswordRequest request)
{
var ipAddress = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
var userAgent = HttpContext.Request.Headers["User-Agent"].ToString();
var baseUrl = $"{Request.Scheme}://{Request.Host}";
var command = new ForgotPasswordCommand(
request.Email,
request.TenantSlug,
ipAddress,
userAgent,
baseUrl);
await mediator.Send(command);
// Always return success to prevent email enumeration
return Ok(new { message = "If the email exists, a password reset link has been sent" });
}
/// <summary>
/// Reset password using valid reset token
/// </summary>
[HttpPost("reset-password")]
[AllowAnonymous]
public async Task<IActionResult> ResetPassword([FromBody] ResetPasswordRequest request)
{
var command = new ResetPasswordCommand(request.Token, request.NewPassword);
var success = await mediator.Send(command);
if (!success)
return BadRequest(new { message = "Invalid or expired reset token" });
return Ok(new { message = "Password reset successfully. Please login with your new password." });
}
}
public record LoginRequest(
@@ -174,3 +217,7 @@ public record LoginRequest(
);
public record VerifyEmailRequest(string Token);
public record ForgotPasswordRequest(string Email, string TenantSlug);
public record ResetPasswordRequest(string Token, string NewPassword);