feat(backend): Implement Refresh Token mechanism (Day 5 Phase 1)

Implemented secure refresh token rotation with the following features:
- RefreshToken domain entity with IsExpired(), IsRevoked(), IsActive(), Revoke() methods
- IRefreshTokenService with token generation, rotation, and revocation
- RefreshTokenService with SHA-256 hashing and token family tracking
- RefreshTokenRepository for database operations
- Database migration for refresh_tokens table with proper indexes
- Updated LoginCommandHandler and RegisterTenantCommandHandler to return refresh tokens
- Added POST /api/auth/refresh endpoint (token rotation)
- Added POST /api/auth/logout endpoint (revoke single token)
- Added POST /api/auth/logout-all endpoint (revoke all user tokens)
- Updated JWT access token expiration to 15 minutes (from 60)
- Refresh token expiration set to 7 days
- Security features: token reuse detection, IP address tracking, user-agent logging

Changes:
- Domain: RefreshToken.cs, IRefreshTokenRepository.cs
- Application: IRefreshTokenService.cs, updated LoginResponseDto and RegisterTenantResult
- Infrastructure: RefreshTokenService.cs, RefreshTokenRepository.cs, RefreshTokenConfiguration.cs
- API: AuthController.cs (3 new endpoints), RefreshTokenRequest.cs, LogoutRequest.cs
- Configuration: appsettings.Development.json (updated JWT settings)
- DI: DependencyInjection.cs (registered new services)
- Migration: AddRefreshTokens migration

🤖 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 14:44:36 +01:00
parent 1f66b25f30
commit 9e2edb2965
32 changed files with 4669 additions and 28 deletions

View File

@@ -1,6 +1,11 @@
using ColaFlow.API.Models;
using ColaFlow.Modules.Identity.Application.Commands.Login;
using ColaFlow.Modules.Identity.Application.Services;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System.Security.Claims;
namespace ColaFlow.API.Controllers;
@@ -9,10 +14,17 @@ namespace ColaFlow.API.Controllers;
public class AuthController : ControllerBase
{
private readonly IMediator _mediator;
private readonly IRefreshTokenService _refreshTokenService;
private readonly ILogger<AuthController> _logger;
public AuthController(IMediator mediator)
public AuthController(
IMediator mediator,
IRefreshTokenService refreshTokenService,
ILogger<AuthController> logger)
{
_mediator = mediator;
_refreshTokenService = refreshTokenService;
_logger = logger;
}
/// <summary>
@@ -29,10 +41,106 @@ public class AuthController : ControllerBase
/// Get current user (requires authentication)
/// </summary>
[HttpGet("me")]
// [Authorize] // TODO: Add after JWT middleware is configured
public async Task<IActionResult> GetCurrentUser()
[Authorize]
public IActionResult GetCurrentUser()
{
// TODO: Implement after JWT middleware
return Ok(new { message = "Current user endpoint - to be implemented" });
// Extract user information from JWT Claims
var userId = User.FindFirst("user_id")?.Value;
var tenantId = User.FindFirst("tenant_id")?.Value;
var email = User.FindFirst(ClaimTypes.Email)?.Value;
var fullName = User.FindFirst("full_name")?.Value;
var tenantSlug = User.FindFirst("tenant_slug")?.Value;
return Ok(new
{
userId,
tenantId,
email,
fullName,
tenantSlug,
claims = User.Claims.Select(c => new { c.Type, c.Value })
});
}
/// <summary>
/// Refresh access token using refresh token
/// </summary>
[HttpPost("refresh")]
[AllowAnonymous]
public async Task<IActionResult> RefreshToken([FromBody] RefreshTokenRequest request)
{
try
{
var ipAddress = HttpContext.Connection.RemoteIpAddress?.ToString();
var userAgent = HttpContext.Request.Headers["User-Agent"].ToString();
var (accessToken, newRefreshToken) = await _refreshTokenService.RefreshTokenAsync(
request.RefreshToken,
ipAddress,
userAgent,
HttpContext.RequestAborted);
return Ok(new
{
accessToken,
refreshToken = newRefreshToken,
expiresIn = 900, // 15 minutes in seconds
tokenType = "Bearer"
});
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, "Refresh token failed");
return Unauthorized(new { message = "Invalid or expired refresh token" });
}
}
/// <summary>
/// Logout (revoke refresh token)
/// </summary>
[HttpPost("logout")]
[AllowAnonymous]
public async Task<IActionResult> Logout([FromBody] LogoutRequest request)
{
try
{
var ipAddress = HttpContext.Connection.RemoteIpAddress?.ToString();
await _refreshTokenService.RevokeTokenAsync(
request.RefreshToken,
ipAddress,
HttpContext.RequestAborted);
return Ok(new { message = "Logged out successfully" });
}
catch (Exception ex)
{
_logger.LogError(ex, "Logout failed");
return BadRequest(new { message = "Logout failed" });
}
}
/// <summary>
/// Logout from all devices (revoke all user refresh tokens)
/// </summary>
[HttpPost("logout-all")]
[Authorize]
public async Task<IActionResult> LogoutAllDevices()
{
try
{
var userId = Guid.Parse(User.FindFirstValue("user_id")!);
await _refreshTokenService.RevokeAllUserTokensAsync(
userId,
HttpContext.RequestAborted);
return Ok(new { message = "Logged out from all devices successfully" });
}
catch (Exception ex)
{
_logger.LogError(ex, "Logout from all devices failed");
return BadRequest(new { message = "Logout failed" });
}
}
}