diff --git a/API/Constants/CacheProfiles.cs b/API/Constants/CacheProfiles.cs
new file mode 100644
index 000000000..80e6f39d3
--- /dev/null
+++ b/API/Constants/CacheProfiles.cs
@@ -0,0 +1,10 @@
+namespace API.Constants;
+
+public static class EasyCacheProfiles
+{
+ ///
+ /// Not in use
+ ///
+ public const string RevokedJwt = "revokedJWT";
+ public const string Favicon = "favicon";
+}
diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs
index ec2b1156d..73f0a8bd6 100644
--- a/API/Controllers/AccountController.cs
+++ b/API/Controllers/AccountController.cs
@@ -18,6 +18,7 @@ using API.Middleware.RateLimit;
using API.Services;
using API.SignalR;
using AutoMapper;
+using EasyCaching.Core;
using Hangfire;
using Kavita.Common;
using Kavita.Common.EnvironmentInfo;
@@ -44,6 +45,7 @@ public class AccountController : BaseApiController
private readonly IAccountService _accountService;
private readonly IEmailService _emailService;
private readonly IEventHub _eventHub;
+ private readonly IEasyCachingProviderFactory _cacheFactory;
///
public AccountController(UserManager userManager,
@@ -51,7 +53,8 @@ public class AccountController : BaseApiController
ITokenService tokenService, IUnitOfWork unitOfWork,
ILogger logger,
IMapper mapper, IAccountService accountService,
- IEmailService emailService, IEventHub eventHub)
+ IEmailService emailService, IEventHub eventHub,
+ IEasyCachingProviderFactory cacheFactory)
{
_userManager = userManager;
_signInManager = signInManager;
@@ -62,6 +65,7 @@ public class AccountController : BaseApiController
_accountService = accountService;
_emailService = emailService;
_eventHub = eventHub;
+ _cacheFactory = cacheFactory;
}
///
@@ -187,8 +191,9 @@ public class AccountController : BaseApiController
var result = await _signInManager
.CheckPasswordSignInAsync(user, loginDto.Password, true);
- if (result.IsLockedOut)
+ if (result.IsLockedOut) // result.IsLockedOut
{
+ await _userManager.UpdateSecurityStampAsync(user);
return Unauthorized("You've been locked out from too many authorization attempts. Please wait 10 minutes.");
}
diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs
index ef3a65710..6c8e67026 100644
--- a/API/Extensions/ApplicationServiceExtensions.cs
+++ b/API/Extensions/ApplicationServiceExtensions.cs
@@ -1,4 +1,5 @@
using System.IO.Abstractions;
+using API.Constants;
using API.Data;
using API.Helpers;
using API.Services;
@@ -68,7 +69,7 @@ public static class ApplicationServiceExtensions
services.AddEasyCaching(options =>
{
- options.UseInMemory("favicon");
+ options.UseInMemory(EasyCacheProfiles.Favicon);
});
}
diff --git a/API/Middleware/JWTRevocationMiddleware.cs b/API/Middleware/JWTRevocationMiddleware.cs
new file mode 100644
index 000000000..2bcba3425
--- /dev/null
+++ b/API/Middleware/JWTRevocationMiddleware.cs
@@ -0,0 +1,57 @@
+using System.Threading.Tasks;
+using API.Constants;
+using EasyCaching.Core;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace API.Middleware;
+
+///
+/// Responsible for maintaining an in-memory. Not in use
+///
+public class JwtRevocationMiddleware
+{
+ private readonly RequestDelegate _next;
+ private readonly IEasyCachingProviderFactory _cacheFactory;
+ private readonly ILogger _logger;
+
+ public JwtRevocationMiddleware(RequestDelegate next, IEasyCachingProviderFactory cacheFactory, ILogger logger)
+ {
+ _next = next;
+ _cacheFactory = cacheFactory;
+ _logger = logger;
+ }
+
+ public async Task InvokeAsync(HttpContext context)
+ {
+ if (context.User.Identity is {IsAuthenticated: false})
+ {
+ await _next(context);
+ return;
+ }
+
+ // Get the JWT from the request headers or wherever you store it
+ var token = context.Request.Headers["Authorization"].ToString()?.Replace("Bearer ", string.Empty);
+
+ // Check if the token is revoked
+ if (await IsTokenRevoked(token))
+ {
+ _logger.LogWarning("Revoked token detected: {Token}", token);
+ context.Response.StatusCode = StatusCodes.Status401Unauthorized;
+ return;
+ }
+
+ await _next(context);
+ }
+
+ private async Task IsTokenRevoked(string token)
+ {
+ // Check if the token exists in the revocation list stored in the cache
+ var isRevoked = await _cacheFactory.GetCachingProvider(EasyCacheProfiles.RevokedJwt)
+ .GetAsync(token);
+
+
+ return isRevoked.HasValue;
+ }
+}
diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs
index 656b427a6..85ec4a041 100644
--- a/API/Services/ImageService.cs
+++ b/API/Services/ImageService.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
+using API.Constants;
using API.Entities.Enums;
using API.Extensions;
using EasyCaching.Core;
@@ -212,7 +213,7 @@ public class ImageService : IImageService
var baseUrl = uri.Scheme + "://" + uri.Host;
- var provider = _cacheFactory.GetCachingProvider("favicon");
+ var provider = _cacheFactory.GetCachingProvider(EasyCacheProfiles.Favicon);
var res = await provider.GetAsync(baseUrl);
if (res.HasValue)
{
diff --git a/API/Services/TokenService.cs b/API/Services/TokenService.cs
index 443cea5b9..ef04757f1 100644
--- a/API/Services/TokenService.cs
+++ b/API/Services/TokenService.cs
@@ -23,6 +23,7 @@ public interface ITokenService
Task CreateToken(AppUser user);
Task ValidateRefreshToken(TokenRequestDto request);
Task CreateRefreshToken(AppUser user);
+ Task GetJwtFromUser(AppUser user);
}
@@ -59,7 +60,7 @@ public class TokenService : ITokenService
var tokenDescriptor = new SecurityTokenDescriptor()
{
Subject = new ClaimsIdentity(claims),
- Expires = DateTime.UtcNow.AddDays(14),
+ Expires = DateTime.UtcNow.AddDays(2),
SigningCredentials = credentials
};
@@ -124,4 +125,11 @@ public class TokenService : ITokenService
return null;
}
}
+
+ public async Task GetJwtFromUser(AppUser user)
+ {
+ var userClaims = await _userManager.GetClaimsAsync(user);
+ var jwtClaim = userClaims.FirstOrDefault(claim => claim.Type == "jwt");
+ return jwtClaim?.Value;
+ }
}
diff --git a/openapi.json b/openapi.json
index ae7ef1ae4..30a6a846f 100644
--- a/openapi.json
+++ b/openapi.json
@@ -7,7 +7,7 @@
"name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
},
- "version": "0.7.2.21"
+ "version": "0.7.2.22"
},
"servers": [
{