First Commit

This commit is contained in:
Jorge Luis caudillo esparza
2026-05-09 16:36:57 -07:00
parent 96ad4c0d3e
commit d0c284d9c0
131 changed files with 4009 additions and 0 deletions

View File

@@ -0,0 +1,66 @@
using System.Security.Claims;
using ADS.Helpers.Auth.Abstractions;
using ADS.Helpers.Auth.Claims;
using ADS.Helpers.Auth.Models;
using Microsoft.AspNetCore.Http;
namespace ADS.Helpers.Auth.Implementation;
internal sealed class AdsPrincipalAccessor : IAdsPrincipalAccessor
{
private readonly IHttpContextAccessor _httpContextAccessor;
private AdsPrincipal? _cached;
public AdsPrincipalAccessor(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public bool IsAuthenticated =>
_httpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated == true;
public AdsPrincipal Current
{
get
{
if (_cached is not null)
return _cached;
var user = _httpContextAccessor.HttpContext?.User
?? throw new UnauthorizedAccessException("No HttpContext user.");
_cached = Map(user);
return _cached;
}
}
private static AdsPrincipal Map(ClaimsPrincipal user)
{
int ParseInt(string type) =>
int.TryParse(user.FindFirst(type)?.Value, out var val) ? val : 0;
int[] ParseIntArray(string type) =>
user.FindAll(type)
.Select(x => int.TryParse(x.Value, out var v) ? v : 0)
.Where(x => x > 0)
.ToArray();
string[] ParseStringArray(string type) =>
user.FindAll(type)
.Select(x => x.Value)
.ToArray();
return new AdsPrincipal
{
CveUser = ParseInt(AdsClaimTypes.CveUser),
TenantId = AdsClaimTypes.TenantId,
UserName = user.FindFirst(AdsClaimTypes.UserName)?.Value,
CveRole = ParseInt(AdsClaimTypes.CveRole),
CveEmpresa = ParseInt(AdsClaimTypes.CveEmpresa),
CveCliente = ParseInt(AdsClaimTypes.CveCliente),
CveModulos = ParseIntArray(AdsClaimTypes.CveModulos),
Permissions = ParseStringArray(AdsClaimTypes.Permissions),
Claims = user.Claims.ToDictionary(x => x.Type, x => x.Value)
};
}
}

View File

@@ -0,0 +1,158 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using ADS.Helpers.Auth.Abstractions;
using ADS.Helpers.Auth.Claims;
using ADS.Helpers.Auth.Models;
using ADS.Helpers.Auth.Token;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
namespace ADS.Helpers.Auth.Implementation;
internal sealed class AdsTokenService : IAdsTokenService
{
private readonly JwtOptions _options;
private readonly IAdsUserCredentialValidator _userValidator;
private readonly IAdsClientCredentialValidator _clientValidator;
public AdsTokenService(
IOptions<JwtOptions> options,
IAdsUserCredentialValidator userValidator,
IAdsClientCredentialValidator clientValidator)
{
_options = options.Value ?? throw new InvalidOperationException("JwtOptions missing.");
_userValidator = userValidator ?? throw new ArgumentNullException(nameof(userValidator));
_clientValidator = clientValidator ?? throw new ArgumentNullException(nameof(clientValidator));
}
public async Task<AdsTokenResult> IssueAsync(AdsTokenRequest request, CancellationToken ct = default)
{
if (request is null) throw new ArgumentNullException(nameof(request));
if (string.IsNullOrWhiteSpace(request.GrantType)) throw new InvalidOperationException("GrantType is required.");
var grant = request.GrantType.Trim().ToLowerInvariant();
if (grant == AdsGrantTypes.Password)
return await IssuePasswordAsync(request, ct);
if (grant == AdsGrantTypes.ClientCredentials)
return await IssueClientCredentialsAsync(request, ct);
throw new InvalidOperationException($"Unsupported GrantType: {request.GrantType}");
}
private async Task<AdsTokenResult> IssuePasswordAsync(AdsTokenRequest request, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(request.UserName))
throw new InvalidOperationException("UserName is required for password grant.");
if (string.IsNullOrWhiteSpace(request.Password))
throw new InvalidOperationException("Password is required for password grant.");
var validation = await _userValidator.ValidateAsync(
request.UserName.Trim(),
request.Password,
request.TenantId,
request.Scope,
ct);
if (!validation.IsValid)
throw new UnauthorizedAccessException(validation.Error ?? "Invalid credentials.");
var claims = new List<Claim>
{
new(AdsClaimTypes.CveUser, validation.CveUser.ToString()),
new(AdsClaimTypes.UserName, validation.UserName ?? request.UserName.Trim()),
new(AdsClaimTypes.CveRole, validation.CveRole.ToString()),
new(AdsClaimTypes.CveEmpresa, validation.CveEmpresa.ToString()),
new(AdsClaimTypes.CveCliente, validation.CveCliente.ToString()),
};
if (!string.IsNullOrWhiteSpace(request.TenantId))
claims.Add(new Claim(AdsClaimTypes.TenantId, request.TenantId));
foreach (var m in validation.CveModulos.Where(x => x > 0))
claims.Add(new Claim(AdsClaimTypes.CveModulos, m.ToString()));
foreach (var p in validation.Permissions.Where(x => !string.IsNullOrWhiteSpace(x)))
claims.Add(new Claim(AdsClaimTypes.Permissions, p));
if (validation.ExtraClaims.Count > 0)
claims.AddRange(validation.ExtraClaims);
return BuildToken(claims, request.Scope);
}
private async Task<AdsTokenResult> IssueClientCredentialsAsync(AdsTokenRequest request, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(request.ClientId))
throw new InvalidOperationException("ClientId is required for client_credentials grant.");
if (string.IsNullOrWhiteSpace(request.ClientSecret))
throw new InvalidOperationException("ClientSecret is required for client_credentials grant.");
var validation = await _clientValidator.ValidateAsync(
request.ClientId.Trim(),
request.ClientSecret,
request.TenantId,
request.Scope,
ct);
if (!validation.IsValid)
throw new UnauthorizedAccessException(validation.Error ?? "Invalid client credentials.");
var claims = new List<Claim>
{
new(AdsClaimTypes.UserName, validation.ClientId),
};
var tenantId = validation.TenantId ?? request.TenantId;
if (!string.IsNullOrWhiteSpace(request.TenantId))
claims.Add(new Claim(AdsClaimTypes.TenantId, request.TenantId));
if (validation.ExtraClaims.Count > 0)
claims.AddRange(validation.ExtraClaims);
return BuildToken(claims, request.Scope);
}
private AdsTokenResult BuildToken(IReadOnlyCollection<Claim> claims, string? scope)
{
if (string.IsNullOrWhiteSpace(_options.SigningKey))
throw new InvalidOperationException("JwtOptions.SigningKey is required.");
var now = DateTime.UtcNow;
var expires = now.AddMinutes(_options.AccessTokenMinutes <= 0 ? 60 : _options.AccessTokenMinutes);
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.SigningKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var jwt = new JwtSecurityToken(
issuer: _options.Issuer,
audience: _options.Audience,
claims: claims,
notBefore: now,
expires: expires,
signingCredentials: creds
);
var accessToken = new JwtSecurityTokenHandler().WriteToken(jwt);
return new AdsTokenResult
{
AccessToken = accessToken,
TokenType = "Bearer",
ExpiresIn = (int)Math.Max(0, (expires - now).TotalSeconds),
Scope = scope
};
}
public Task<AdsTokenResult> IssueFromClaimsAsync(IReadOnlyCollection<Claim> claims, string? scope = null, CancellationToken ct = default)
{
if (claims is null) throw new ArgumentNullException(nameof(claims));
// ct no se usa, pero lo dejamos por consistencia
return Task.FromResult(BuildToken(claims, scope));
}
}