From 49de0351c1476dda26c489a1443bfb63b16327ae Mon Sep 17 00:00:00 2001 From: Alex Bezdieniezhnykh Date: Sat, 18 Jan 2025 14:36:50 +0200 Subject: [PATCH] add Cache.cs fix hardware hash stack in the jwt token claims --- Azaion.Api/Program.cs | 16 ++++++++--- Azaion.Api/appsettings.json | 2 +- Azaion.Common/Configs/Constants.cs | 6 ----- Azaion.Services/AuthService.cs | 37 +++++++++++--------------- Azaion.Services/Azaion.Services.csproj | 1 + Azaion.Services/Cache.cs | 27 +++++++++++++++++++ Azaion.Services/UserService.cs | 21 ++++++++++++++- 7 files changed, 77 insertions(+), 33 deletions(-) delete mode 100644 Azaion.Common/Configs/Constants.cs create mode 100644 Azaion.Services/Cache.cs diff --git a/Azaion.Api/Program.cs b/Azaion.Api/Program.cs index a277b82..4d8c1ce 100644 --- a/Azaion.Api/Program.cs +++ b/Azaion.Api/Program.cs @@ -98,10 +98,13 @@ builder.Services.Configure(builder.Configuration.GetSection(nameof(Jw builder.Services.Configure(builder.Configuration.GetSection(nameof(ConnectionStrings))); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddLazyCache(); +builder.Services.AddScoped(); + builder.Services.AddValidatorsFromAssemblyContaining(); var app = builder.Build(); @@ -145,7 +148,7 @@ app.MapPost("/resources/{dataFolder?}", app.MapPost("/resources/get/{dataFolder?}", //Need to have POST method for secure password async ([FromBody]GetResourceRequest request, [FromRoute]string? dataFolder, IAuthService authService, IUserService userService, IResourcesService resourcesService, CancellationToken cancellationToken) => { - var user = authService.CurrentUser; + var user = await authService.GetCurrentUser(); if (user == null) throw new UnauthorizedAccessException(); @@ -159,8 +162,13 @@ app.MapPost("/resources/get/{dataFolder?}", //Need to have POST method for secur .WithOpenApi(op => new OpenApiOperation(op){ Summary = "Gets encrypted by users Password and HardwareHash resources. POST method for secure password"}); app.MapPut("/resources/reset-hardware", - async (string email, IUserService userService, CancellationToken cancellationToken) - => await userService.UpdateHardware(email, new HardwareInfo(), cancellationToken)) + async (string email, IUserService userService, ICache cache, CancellationToken cancellationToken) => + { + await userService.UpdateHardware(email, new HardwareInfo(), cancellationToken); + var user = await userService.GetByEmail(email, cancellationToken); + cache.Invalidate($"{nameof(User)}.{user?.Id}"); + }) + .RequireAuthorization(apiAdminPolicy) .WithOpenApi(op => new OpenApiOperation(op){ Summary = "Resets hardware id in case of hardware change"}); app.Run(); diff --git a/Azaion.Api/appsettings.json b/Azaion.Api/appsettings.json index 6ae882f..1a02728 100644 --- a/Azaion.Api/appsettings.json +++ b/Azaion.Api/appsettings.json @@ -12,6 +12,6 @@ "JwtConfig": { "Issuer": "AzaionApi", "Audience": "Annotators/OrangePi/Admins", - "TokenLifetimeHours": 2.5 + "TokenLifetimeHours": 4 } } diff --git a/Azaion.Common/Configs/Constants.cs b/Azaion.Common/Configs/Constants.cs deleted file mode 100644 index f050783..0000000 --- a/Azaion.Common/Configs/Constants.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Azaion.Common.Configs; - -public class Constants -{ - public const string HARDWARE_ID = nameof(HARDWARE_ID); -} \ No newline at end of file diff --git a/Azaion.Services/AuthService.cs b/Azaion.Services/AuthService.cs index 5d6a8a8..1b6a638 100644 --- a/Azaion.Services/AuthService.cs +++ b/Azaion.Services/AuthService.cs @@ -11,31 +11,28 @@ namespace Azaion.Services; public interface IAuthService { - User? CurrentUser { get; } + Guid? GetCurrentUserId(); + Task GetCurrentUser(); string CreateToken(User user); } -public class AuthService(IHttpContextAccessor httpContextAccessor, IOptions jwtConfig) : IAuthService +public class AuthService(IHttpContextAccessor httpContextAccessor, IOptions jwtConfig, IUserService userService) : IAuthService { - public User? CurrentUser + + public Guid? GetCurrentUserId() { - get - { - var claims = httpContextAccessor.HttpContext?.User.Claims.ToDictionary(x => x.Type); - if (claims == null) - return null; + var claims = httpContextAccessor.HttpContext?.User.Claims.ToDictionary(x => x.Type); + if (claims == null) + return null; - if (!Enum.TryParse(claims[ClaimTypes.Role].Value, out RoleEnum role)) - throw new ApplicationException("Invalid role"); + var id = Guid.Parse(claims[ClaimTypes.NameIdentifier].Value); + return id; + } - return new User - { - Id = Guid.Parse(claims[ClaimTypes.NameIdentifier].Value), - Email = claims[ClaimTypes.Name].Value, - Role = role, - HardwareHash = claims[Constants.HARDWARE_ID].Value, - }; - } + public async Task GetCurrentUser() + { + var id = GetCurrentUserId(); + return await userService.GetById(id); } public string CreateToken(User user) @@ -47,9 +44,7 @@ public class AuthService(IHttpContextAccessor httpContextAccessor, IOptions + diff --git a/Azaion.Services/Cache.cs b/Azaion.Services/Cache.cs new file mode 100644 index 0000000..3b6548d --- /dev/null +++ b/Azaion.Services/Cache.cs @@ -0,0 +1,27 @@ +using LazyCache; + +namespace Azaion.Services; + +public interface ICache +{ + Task GetFromCacheAsync(string key, Func> fetchFunc, TimeSpan? expiration = null); + void Invalidate(string key); +} + +public class MemoryCache : ICache +{ + private readonly IAppCache _cache = new CachingService(); + + public async Task GetFromCacheAsync(string key, Func> fetchFunc, TimeSpan? expiration = null) + { + expiration ??= TimeSpan.FromHours(4); + return await _cache.GetOrAddAsync(key, async entry => + { + var result = await fetchFunc(); + entry.AbsoluteExpirationRelativeToNow = expiration; + return result; + }); + } + + public void Invalidate(string key) => _cache.Remove(key); +} \ No newline at end of file diff --git a/Azaion.Services/UserService.cs b/Azaion.Services/UserService.cs index 36b3f64..23c03e7 100644 --- a/Azaion.Services/UserService.cs +++ b/Azaion.Services/UserService.cs @@ -12,12 +12,14 @@ public interface IUserService { Task RegisterUser(RegisterUserRequest request, CancellationToken cancellationToken = default); Task ValidateUser(LoginRequest request, CancellationToken cancellationToken = default); + Task GetById(Guid? id, CancellationToken cancellationToken = default); + Task GetByEmail(string email, CancellationToken cancellationToken = default); Task UpdateHardware(string email, HardwareInfo hardwareInfo, CancellationToken cancellationToken = default); Task> GetUsers(string? searchEmail, RoleEnum? searchRole, CancellationToken cancellationToken); Task CheckHardware(User user, GetResourceRequest request); } -public class UserService(IDbFactory dbFactory) : IUserService +public class UserService(IDbFactory dbFactory, ICache cache) : IUserService { public async Task RegisterUser(RegisterUserRequest request, CancellationToken cancellationToken = default) { @@ -37,6 +39,15 @@ public class UserService(IDbFactory dbFactory) : IUserService }); } + public async Task GetById(Guid? id, CancellationToken cancellationToken = default) => + await cache.GetFromCacheAsync($"{nameof(User)}.{id}", + async () => await dbFactory.Run(async db => + await db.Users.FirstOrDefaultAsync(x => x.Id == id, cancellationToken)), TimeSpan.FromHours(2)); + + public async Task GetByEmail(string email, CancellationToken cancellationToken = default) => + await dbFactory.Run(async db => + await db.Users.FirstOrDefaultAsync(x => x.Email == email, cancellationToken)); + public async Task ValidateUser(LoginRequest request, CancellationToken cancellationToken = default) => await dbFactory.Run(async db => { @@ -82,6 +93,14 @@ public class UserService(IDbFactory dbFactory) : IUserService user.HardwareHash = request.Hardware.Hash; } + var hwHash = await dbFactory.Run(async db => + await db.Users + .Where(x => x.Email == user.Email) + .Select(x => x.HardwareHash) + .FirstOrDefaultAsync()); + if (hwHash != user.HardwareHash) + user.HardwareHash = hwHash; + if (user.HardwareHash != request.Hardware.Hash) throw new BusinessException(ExceptionEnum.HardwareIdMismatch); }