File size: 8,269 Bytes
af8cc55 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 | using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using OptimAI.BRE.RuleEngine.Infrastructure;
using OptimAI.BRE.Shared.Domain;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
namespace OptimAI.BRE.IdentityService.Api;
[ApiController]
[Route("api/v1/auth")]
public sealed class AuthController : ControllerBase
{
private readonly BREDbContext _db;
private readonly JwtOptions _jwt;
private readonly ILogger<AuthController> _logger;
public AuthController(BREDbContext db, IOptions<JwtOptions> jwt, ILogger<AuthController> logger)
{
_db = db;
_jwt = jwt.Value;
_logger = logger;
}
[HttpPost("login")]
[ProducesResponseType(typeof(LoginResponse), 200)]
[ProducesResponseType(typeof(ProblemDetails), 401)]
public async Task<IActionResult> Login([FromBody] LoginRequest request, CancellationToken ct)
{
var user = await _db.Users
.Include(u => u.UserRoles)
.ThenInclude(ur => ur.Role)
.ThenInclude(r => r.RolePermissions)
.ThenInclude(rp => rp.Permission)
.FirstOrDefaultAsync(u =>
u.Email == request.Email.ToLower() &&
u.IsActive, ct);
if (user == null || !VerifyPassword(request.Password, user.PasswordHash))
{
if (user != null)
{
user.FailedLoginCount++;
if (user.FailedLoginCount >= 5)
user.LockedUntil = DateTime.UtcNow.AddMinutes(30);
await _db.SaveChangesAsync(ct);
}
return Unauthorized(new { error = "Invalid email or password" });
}
if (user.LockedUntil.HasValue && user.LockedUntil > DateTime.UtcNow)
return Unauthorized(new { error = "Account locked. Try again later." });
user.FailedLoginCount = 0;
user.LastLoginAt = DateTime.UtcNow;
user.LastLoginIp = HttpContext.Connection.RemoteIpAddress?.ToString();
await _db.SaveChangesAsync(ct);
var permissions = user.UserRoles
.SelectMany(ur => ur.Role.RolePermissions)
.Select(rp => rp.Permission.PermissionCode)
.Distinct()
.ToList();
var accessToken = GenerateAccessToken(user, permissions);
var refreshToken = GenerateRefreshToken();
_db.RefreshTokens.Add(new RefreshToken
{
UserId = user.Id,
TokenHash = HashToken(refreshToken),
ExpiresAt = DateTime.UtcNow.AddDays(_jwt.RefreshTokenExpiryDays),
IpAddress = user.LastLoginIp,
UserAgent = Request.Headers.UserAgent.ToString()
});
await _db.SaveChangesAsync(ct);
return Ok(new LoginResponse
{
AccessToken = accessToken,
RefreshToken = refreshToken,
ExpiresIn = _jwt.AccessTokenExpiryMinutes * 60,
TokenType = "Bearer",
User = new UserDto
{
Id = user.Id,
Email = user.Email,
FullName = user.FullName,
TenantId = user.TenantId,
Permissions = permissions,
Roles = user.UserRoles.Select(ur => ur.Role.RoleCode).ToList()
}
});
}
[HttpPost("refresh")]
public async Task<IActionResult> Refresh([FromBody] RefreshRequest request, CancellationToken ct)
{
var hash = HashToken(request.RefreshToken);
var token = await _db.RefreshTokens
.Include(t => t.User)
.FirstOrDefaultAsync(t =>
t.TokenHash == hash &&
!t.IsRevoked &&
t.ExpiresAt > DateTime.UtcNow, ct);
if (token == null)
return Unauthorized(new { error = "Invalid or expired refresh token" });
// Rotate refresh token
token.IsRevoked = true;
var newRefreshToken = GenerateRefreshToken();
_db.RefreshTokens.Add(new RefreshToken
{
UserId = token.UserId,
TokenHash = HashToken(newRefreshToken),
ExpiresAt = DateTime.UtcNow.AddDays(_jwt.RefreshTokenExpiryDays)
});
await _db.SaveChangesAsync(ct);
var permissions = await GetUserPermissionsAsync(token.UserId, ct);
var accessToken = GenerateAccessToken(token.User!, permissions);
return Ok(new { accessToken, refreshToken = newRefreshToken });
}
[HttpPost("logout")]
public async Task<IActionResult> Logout([FromBody] LogoutRequest request, CancellationToken ct)
{
var hash = HashToken(request.RefreshToken);
await _db.RefreshTokens
.Where(t => t.TokenHash == hash)
.ExecuteUpdateAsync(s => s.SetProperty(t => t.IsRevoked, true), ct);
return Ok(new { message = "Logged out successfully" });
}
private string GenerateAccessToken(User user, List<string> permissions)
{
var claims = new List<Claim>
{
new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
new(JwtRegisteredClaimNames.Email, user.Email),
new(JwtRegisteredClaimNames.Name, user.FullName),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new("tenant_id", user.TenantId.ToString()),
};
foreach (var p in permissions)
claims.Add(new Claim("permission", p));
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwt.SecretKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _jwt.Issuer,
audience: _jwt.Audience,
claims: claims,
expires: DateTime.UtcNow.AddMinutes(_jwt.AccessTokenExpiryMinutes),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
private static string GenerateRefreshToken()
{
var bytes = new byte[64];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(bytes);
return Convert.ToBase64String(bytes);
}
private static string HashToken(string token)
{
using var sha = SHA256.Create();
return Convert.ToHexString(sha.ComputeHash(Encoding.UTF8.GetBytes(token))).ToLower();
}
private static bool VerifyPassword(string password, string hash)
=> BCrypt.Net.BCrypt.Verify(password, hash);
private async Task<List<string>> GetUserPermissionsAsync(Guid userId, CancellationToken ct)
{
return await _db.UserRoles
.Where(ur => ur.UserId == userId)
.SelectMany(ur => ur.Role.RolePermissions)
.Select(rp => rp.Permission.PermissionCode)
.Distinct()
.ToListAsync(ct);
}
}
// Models
public class JwtOptions
{
public string SecretKey { get; set; } = default!;
public string Issuer { get; set; } = default!;
public string Audience { get; set; } = default!;
public int AccessTokenExpiryMinutes { get; set; } = 60;
public int RefreshTokenExpiryDays { get; set; } = 30;
}
public record LoginRequest
{
public string Email { get; init; } = default!;
public string Password { get; init; } = default!;
public bool RememberMe { get; init; }
}
public record LoginResponse
{
public string AccessToken { get; init; } = default!;
public string RefreshToken { get; init; } = default!;
public int ExpiresIn { get; init; }
public string TokenType { get; init; } = "Bearer";
public UserDto User { get; init; } = default!;
}
public record UserDto
{
public Guid Id { get; init; }
public string Email { get; init; } = default!;
public string FullName { get; init; } = default!;
public Guid TenantId { get; init; }
public List<string> Permissions { get; init; } = new();
public List<string> Roles { get; init; } = new();
}
public record RefreshRequest { public string RefreshToken { get; init; } = default!; }
public record LogoutRequest { public string RefreshToken { get; init; } = default!; }
|