Stable user lockout, role and claim calls
This commit is contained in:
@@ -52,6 +52,9 @@ public interface IUserService
|
||||
[Log]
|
||||
Task<Result<Domain.Models.Security.Claim[]>> GetUserClaimsAsync(Guid targetUserId, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
Task<Result<Domain.Models.Security.Claim[]>> GetRoleClaimsAsync(Guid roleId, Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
[Log]
|
||||
Task<Result<Role[]>> GetSystemRolesAsync(Guid userId, CancellationToken cancellationToken = default);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using PostFundManagement.Application.Handlers;
|
||||
using PostFundManagement.Application.Interceptors;
|
||||
using PostFundManagement.Application.Services.Shared;
|
||||
using PostFundManagement.Domain.Abstractions;
|
||||
@@ -24,19 +25,10 @@ public static class Security
|
||||
|
||||
if (!authOptions.Authority.EndsWith('/')) authOptions.Authority += "/";
|
||||
|
||||
services.AddClientCredentialsTokenManagement()
|
||||
.AddClient(SkorubaCredentialClient, client =>
|
||||
{
|
||||
client.TokenEndpoint = new Uri($"{authOptions.Authority}connect/token");
|
||||
client.ClientId = ClientId.Parse(authOptions.ClientId!);
|
||||
client.ClientSecret = ClientSecret.Parse(authOptions.ClientSecret!);
|
||||
client.Scope = Duende.AccessTokenManagement.Scope.ParseOrDefault(authOptions.Scope);
|
||||
});
|
||||
|
||||
services.AddHttpClient(SkorubaHttpClient, client =>
|
||||
{
|
||||
client.BaseAddress = new Uri(configuration.GetConnectionString("SkorubaApi")!);
|
||||
}).AddClientCredentialsTokenHandler(ClientCredentialsClientName.Parse(SkorubaCredentialClient));
|
||||
services.AddTransient<ClientCredentialsTokenHandler>();
|
||||
|
||||
services.AddHttpClient(SkorubaHttpClient, client => client.BaseAddress = new Uri(configuration.GetConnectionString("SkorubaApi")!))
|
||||
.AddHttpMessageHandler<ClientCredentialsTokenHandler>();
|
||||
|
||||
services.AddScoped<AuditInterceptor>();
|
||||
services.AddScoped<LoggingInterceptor>();
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
using IdentityModel.Client;
|
||||
using PostFundManagement.Domain.Configuration.Security;
|
||||
|
||||
namespace PostFundManagement.Application.Handlers;
|
||||
|
||||
public class ClientCredentialsTokenHandler(ILogger<ClientCredentialsTokenHandler> logger,
|
||||
IOptions<MachineIdentitySettings> options) : DelegatingHandler
|
||||
{
|
||||
private readonly MachineIdentitySettings _settings = options.Value;
|
||||
private string? _cachedToken;
|
||||
private DateTime _tokenExpiry = DateTime.MinValue;
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var token = await GetAccessTokenAsync(cancellationToken);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
logger.LogError("Failed to acquire access token for Skoruba API");
|
||||
|
||||
throw new InvalidOperationException("Unable to acquire access token");
|
||||
}
|
||||
|
||||
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
logger.LogDebug("ClientCredentialsTokenHandler: Added bearer token to request");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error acquiring client credentials token");
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var response = await base.SendAsync(request, cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
logger.LogWarning("HTTP request failed with status {StatusCode} for {Method} {RequestUri}",
|
||||
response.StatusCode, request.Method, request.RequestUri);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error sending HTTP request with client credentials token");
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string?> GetAccessTokenAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_cachedToken) && DateTime.UtcNow < _tokenExpiry)
|
||||
{
|
||||
logger.LogDebug("Using cached access token");
|
||||
|
||||
return _cachedToken;
|
||||
}
|
||||
|
||||
logger.LogDebug("Acquiring new access token from {Authority}", _settings.Authority);
|
||||
|
||||
using var tokenClient = new HttpClient();
|
||||
|
||||
try
|
||||
{
|
||||
if (!_settings.Authority!.EndsWith('/')) _settings.Authority += "/";
|
||||
|
||||
var scope = _settings.Scope;
|
||||
|
||||
var tokenRequest = new ClientCredentialsTokenRequest
|
||||
{
|
||||
Address = $"{_settings.Authority}connect/token",
|
||||
ClientId = _settings.ClientId!,
|
||||
ClientSecret = _settings.ClientSecret,
|
||||
GrantType = "client_credentials",
|
||||
Scope = scope,
|
||||
};
|
||||
|
||||
logger.LogDebug("Sending token request to {TokenEndpoint} for client {ClientId} with scope {Scope}",
|
||||
tokenRequest.Address, tokenRequest.ClientId, tokenRequest.Scope);
|
||||
|
||||
var response = await tokenClient.RequestClientCredentialsTokenAsync(tokenRequest, cancellationToken);
|
||||
|
||||
if (response.IsError)
|
||||
{
|
||||
logger.LogError("Token request error: {Error} - {ErrorDescription}. Full response: {Raw}",
|
||||
response.Error, response.ErrorDescription, response.Raw);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
_cachedToken = response.AccessToken;
|
||||
|
||||
var expiresIn = response.ExpiresIn > 0 ? response.ExpiresIn : 3600;
|
||||
|
||||
_tokenExpiry = DateTime.UtcNow.AddSeconds(expiresIn * 0.9);
|
||||
|
||||
logger.LogDebug("Successfully acquired access token, expires at {Expiry}", _tokenExpiry);
|
||||
|
||||
return _cachedToken;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Exception occurred while acquiring client credentials token");
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,31 @@ public sealed class UserCacheService(UserService service, IDistributedCache cach
|
||||
public async Task<Result> DeleteUserRoleAsync(Guid targetUserId, Guid roleId, Guid userId, CancellationToken cancellationToken = default) =>
|
||||
await service.DeleteUserRoleAsync(targetUserId, roleId, userId, cancellationToken);
|
||||
|
||||
public async Task<Result<Claim[]>> GetRoleClaimsAsync(Guid roleId, Guid userId, CancellationToken cancellationToken = default) =>
|
||||
await Result.Try((Func<Task<Result<Claim[]>>>)(async () =>
|
||||
{
|
||||
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
|
||||
return await service.GetRoleClaimsAsync(roleId, userId, cancellationToken);
|
||||
|
||||
var cacheKey = HashService.ToMd5Hash($"getsystemroles-{userId}").Value;
|
||||
|
||||
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cached))
|
||||
{
|
||||
var claimResult = await service.GetRoleClaimsAsync(roleId, userId, cancellationToken);
|
||||
|
||||
if (claimResult.IsFailed) return claimResult;
|
||||
|
||||
await cache.SetStringAsync(cacheKey, claimResult.Value.ToJson(),
|
||||
cacheOptions, cancellationToken);
|
||||
|
||||
return claimResult;
|
||||
}
|
||||
|
||||
return Result.Ok(cached.FromJson<Claim[]>());
|
||||
}));
|
||||
|
||||
public async Task<Result<Role[]>> GetSystemRolesAsync(Guid userId, CancellationToken cancellationToken = default) =>
|
||||
await Result.Try((Func<Task<Result<Role[]>>>)(async () =>
|
||||
{
|
||||
|
||||
@@ -321,4 +321,8 @@ public sealed class UserService(IDbContextFactory<ApplicationDbContext> contextF
|
||||
return Result.OkIf(roleAddResult.IsSuccess && changedTotal > 0, ValidationError.Create("Failed to add user role.")
|
||||
.CausedBy(roleAddResult.Errors));
|
||||
}));
|
||||
|
||||
public async Task<Result<Claim[]>> GetRoleClaimsAsync(Guid roleId, Guid userId, CancellationToken cancellationToken = default) =>
|
||||
await Result.Try((Func<Task<Result<Claim[]>>>)(async () =>
|
||||
await skorubaService.GetRoleClaimsAsync(roleId, cancellationToken)));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user