using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Authentication; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; #nullable disable namespace MarcoBMS.Services.Service { public class RefreshTokenService { private readonly ApplicationDbContext _context; private readonly IMemoryCache _cache; // For optional JWT blacklisting private readonly ILoggingService _logger; public RefreshTokenService(ApplicationDbContext context, IMemoryCache cache, ILoggingService logger) { _context = context; _cache = cache; _logger = logger; } public string GenerateJwtTokenWithOrganization(string username, Guid organizationId, JwtSettings _jwtSettings) { // Custom claims var claims = new List { new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new Claim(JwtRegisteredClaimNames.Sub, username), new Claim("OrganizationId", organizationId.ToString()), // Add OrganizationId claim new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) }; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.Key)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: _jwtSettings.Issuer, audience: _jwtSettings.Audience, claims: claims, expires: DateTime.UtcNow.AddMinutes(_jwtSettings.ExpiresInMinutes), signingCredentials: creds); return new JwtSecurityTokenHandler().WriteToken(token); } public async Task CreateRefreshTokenWithOrganization(string userId, Guid organizationId, JwtSettings jwtSettings) { try { var claims = new[] { new Claim(ClaimTypes.NameIdentifier, userId), new Claim("OrganizationId", organizationId.ToString()), new Claim("token_type", "refresh") }; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Key)); var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(claims), Expires = DateTime.UtcNow.AddDays(jwtSettings.RefreshTokenExpiresInDays), Issuer = jwtSettings.Issuer, Audience = jwtSettings.Audience, SigningCredentials = credentials }; var tokenHandler = new JwtSecurityTokenHandler(); var refreshTokenString = tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor)); var refreshToken = new RefreshToken { Token = refreshTokenString, UserId = userId, ExpiryDate = DateTime.UtcNow.AddDays(jwtSettings.RefreshTokenExpiresInDays), IsRevoked = false }; // Check if the record exists var existingEntity = await _context.RefreshTokens.AnyAsync(c => c.UserId == userId); if (existingEntity) { _context.RefreshTokens.Update(refreshToken); } else { _context.RefreshTokens.Add(refreshToken); } await _context.SaveChangesAsync(); return refreshTokenString; } catch (Exception ex) { _logger.LogError(ex, "Error occured while creating new JWT token for user {UserId}", userId); throw; } } public string GenerateJwtToken(string username, Guid tenantId, Guid organizationId, JwtSettings _jwtSettings) { // Custom claims var claims = new List { new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new Claim(JwtRegisteredClaimNames.Sub, username), new Claim("TenantId", tenantId.ToString()), // Add TenantId claim new Claim("OrganizationId", organizationId.ToString()), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) }; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.Key)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: _jwtSettings.Issuer, audience: _jwtSettings.Audience, claims: claims, expires: DateTime.UtcNow.AddMinutes(_jwtSettings.ExpiresInMinutes), signingCredentials: creds); return new JwtSecurityTokenHandler().WriteToken(token); } public async Task CreateRefreshToken(string userId, string tenantId, Guid organizationId, JwtSettings jwtSettings) { try { var claims = new[] { new Claim(ClaimTypes.NameIdentifier, userId), new Claim("TenantId", tenantId), new Claim("OrganizationId", organizationId.ToString()), new Claim("token_type", "refresh") }; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Key)); var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(claims), Expires = DateTime.UtcNow.AddDays(jwtSettings.RefreshTokenExpiresInDays), Issuer = jwtSettings.Issuer, Audience = jwtSettings.Audience, SigningCredentials = credentials }; var tokenHandler = new JwtSecurityTokenHandler(); var refreshTokenString = tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor)); var refreshToken = new RefreshToken { Token = refreshTokenString, UserId = userId, ExpiryDate = DateTime.UtcNow.AddDays(jwtSettings.RefreshTokenExpiresInDays), IsRevoked = false }; // Check if the record exists var existingEntity = await _context.RefreshTokens.AnyAsync(c => c.UserId == userId); if (existingEntity) { _context.RefreshTokens.Update(refreshToken); } else { _context.RefreshTokens.Add(refreshToken); } await _context.SaveChangesAsync(); return refreshTokenString; } catch (Exception ex) { _logger.LogError(ex, "Error occured while creating new JWT token for user {UserId}", userId); throw; } } public string CreateMPINToken(string userId, string organizationId, JwtSettings jwtSettings) { try { var claims = new[] { new Claim(ClaimTypes.NameIdentifier, userId), new Claim("OrganizationId", organizationId), new Claim("token_type", "mpin") }; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Key)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(claims), Issuer = jwtSettings.Issuer, Audience = jwtSettings.Audience, SigningCredentials = creds // No 'Expires' means the token won't expire }; var tokenHandler = new JwtSecurityTokenHandler(); var MPINToken = tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor)); return MPINToken; } catch (Exception ex) { _logger.LogError(ex, "Error creating MPIN token for userId: {UserId}, organizationId: {OrganizationId}", userId, organizationId); throw; } } public async Task GetRefreshToken(string token) { return await _context.RefreshTokens.FirstOrDefaultAsync(rt => rt.Token == token && !rt.IsRevoked && !rt.IsUsed) ?? new RefreshToken(); } public async Task MarkRefreshTokenAsUsed(RefreshToken refreshToken) { refreshToken.IsUsed = true; _context.RefreshTokens.Update(refreshToken); await _context.SaveChangesAsync(); } public async Task RevokeRefreshToken(RefreshToken refreshToken) { refreshToken.IsRevoked = true; refreshToken.RevokedAt = DateTime.UtcNow; _context.RefreshTokens.Update(refreshToken); await _context.SaveChangesAsync(); } // Revoke refresh token public async Task RevokeRefreshTokenAsync(string refreshToken) { var token = await _context.RefreshTokens.FirstOrDefaultAsync(t => t.Token == refreshToken); if (token == null || token.IsRevoked || token.ExpiryDate <= DateTime.UtcNow) return false; token.IsRevoked = true; token.RevokedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); return true; } // Optional: Blacklist JWT token public Task BlacklistJwtTokenAsync(string jwtToken) { // Store the JWT token in memory cache with its expiry var jwtExpiry = GetJwtExpiry(jwtToken); if (jwtExpiry.HasValue) { _cache.Set(jwtToken, true, jwtExpiry.Value - DateTime.UtcNow); } return Task.CompletedTask; } private DateTime? GetJwtExpiry(string token) { var handler = new JwtSecurityTokenHandler(); var jwtToken = handler.ReadToken(token) as JwtSecurityToken; return jwtToken?.ValidTo; } public ClaimsPrincipal ValidateToken(string token, JwtSettings jwtSettings) { var tokenHandler = new JwtSecurityTokenHandler(); var key = System.Text.Encoding.ASCII.GetBytes(jwtSettings.Key); var validationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(key), ValidateIssuer = true, ValidIssuer = jwtSettings.Issuer, ValidateAudience = true, ValidAudience = jwtSettings.Audience, ValidateLifetime = false, // Disable lifetime validation (ignores expiration) ClockSkew = TimeSpan.Zero // Optional: Remove time skew buffer }; try { var principal = tokenHandler.ValidateToken(token, validationParameters, out SecurityToken validatedToken); return principal; } catch (Exception ex) { // Token is invalid _logger.LogError(ex, "Token validation failed"); return null; } } } }