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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user