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