First Commit
This commit is contained in:
@@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
158
Helpers/ADS.Helpers.Auth/Implementation/AdsTokenService.cs
Normal file
158
Helpers/ADS.Helpers.Auth/Implementation/AdsTokenService.cs
Normal 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));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user