1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-04 01:22:50 -05:00

[PS-93] Distributed Ip rate limiting (#2060)

* Upgrade AspNetCoreRateLimiter and enable redis distributed cache for rate limiting.

- Upgrades AspNetCoreRateLimiter to 4.0.2, which required updating NewtonSoft.Json to 13.0.1.
- Replaces Microsoft.Extensions.Caching.Redis with Microsoft.Extensions.Caching.StackExchangeRedis as the original was deprecated and conflicted with the latest AspNetCoreRateLimiter
- Adds startup task to Program.cs for Api/Identity projects to support AspNetCoreRateLimiters breaking changes for seeding its stores.
- Adds a Redis connection string option to GlobalSettings

Signed-off-by: Shane Melton <smelton@bitwarden.com>

* Cleanup Redis distributed cache registration

- Add new AddDistributedCache service collection extension to add either a Memory or Redis distributed cache.
- Remove distributed cache registration from Identity service collection extension.
- Add IpRateLimitSeedStartupService.cs to run at application startup to seed the Ip rate limiting policies.

Signed-off-by: Shane Melton <smelton@bitwarden.com>

* Add caching configuration to SSO Startup.cs

Signed-off-by: Shane Melton <smelton@bitwarden.com>

* Add ProjectName as an instance name for Redis options

Signed-off-by: Shane Melton <smelton@bitwarden.com>

* Use distributed cache in CustomIpRateLimitMiddleware.cs

Signed-off-by: Shane Melton <smelton@bitwarden.com>

* Undo changes to Program.cs and launchSettings.json

* Move new service collection extensions to SharedWeb

* Upgrade Caching.StackExchangeRedis package to v6

* Cleanup and fix leftover merge conflicts

* Remove use of Newtonsoft.Json in distributed cache extensions

* Cleanup more formatting

* Fix formatting

* Fix startup issue caused by merge and fix integration test

Signed-off-by: Shane Melton <smelton@bitwarden.com>

* Linting fix

Signed-off-by: Shane Melton <smelton@bitwarden.com>
This commit is contained in:
Shane Melton
2022-07-19 11:58:32 -07:00
committed by GitHub
parent 1764d2446e
commit 7d40b38352
39 changed files with 2331 additions and 1910 deletions

View File

@ -2,7 +2,7 @@
using Bit.Core.Models.Api;
using Bit.Core.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@ -10,13 +10,13 @@ namespace Bit.Core.Utilities
{
public class CustomIpRateLimitMiddleware : IpRateLimitMiddleware
{
private readonly IpRateLimitOptions _options;
private readonly IMemoryCache _memoryCache;
private readonly IBlockIpService _blockIpService;
private readonly ILogger<CustomIpRateLimitMiddleware> _logger;
private readonly IDistributedCache _distributedCache;
private readonly IpRateLimitOptions _options;
public CustomIpRateLimitMiddleware(
IMemoryCache memoryCache,
IDistributedCache distributedCache,
IBlockIpService blockIpService,
RequestDelegate next,
IProcessingStrategy processingStrategy,
@ -26,7 +26,7 @@ namespace Bit.Core.Utilities
ILogger<CustomIpRateLimitMiddleware> logger)
: base(next, processingStrategy, options, policyStore, rateLimitConfiguration, logger)
{
_memoryCache = memoryCache;
_distributedCache = distributedCache;
_blockIpService = blockIpService;
_options = options.Value;
_logger = logger;
@ -34,12 +34,13 @@ namespace Bit.Core.Utilities
public override Task ReturnQuotaExceededResponse(HttpContext httpContext, RateLimitRule rule, string retryAfter)
{
var message = string.IsNullOrWhiteSpace(_options.QuotaExceededMessage) ?
$"Slow down! Too many requests. Try again in {rule.Period}." : _options.QuotaExceededMessage;
var message = string.IsNullOrWhiteSpace(_options.QuotaExceededMessage)
? $"Slow down! Too many requests. Try again in {rule.Period}."
: _options.QuotaExceededMessage;
httpContext.Response.Headers["Retry-After"] = retryAfter;
httpContext.Response.StatusCode = _options.HttpStatusCode;
var errorModel = new ErrorResponseModel { Message = message };
return httpContext.Response.WriteAsJsonAsync(errorModel, cancellationToken: httpContext.RequestAborted);
return httpContext.Response.WriteAsJsonAsync(errorModel, httpContext.RequestAborted);
}
protected override void LogBlockedRequest(HttpContext httpContext, ClientRequestIdentity identity,
@ -48,7 +49,7 @@ namespace Bit.Core.Utilities
base.LogBlockedRequest(httpContext, identity, counter, rule);
var key = $"blockedIp_{identity.ClientIp}";
_memoryCache.TryGetValue(key, out int blockedCount);
_distributedCache.TryGetValue(key, out int blockedCount);
blockedCount++;
if (blockedCount > 10)
@ -61,8 +62,8 @@ namespace Bit.Core.Utilities
{
_logger.LogInformation(Constants.BypassFiltersEventId, null,
"Request blocked {0}. \nInfo: \n{1}", identity.ClientIp, GetRequestInfo(httpContext));
_memoryCache.Set(key, blockedCount,
new MemoryCacheEntryOptions().SetSlidingExpiration(new TimeSpan(0, 5, 0)));
_distributedCache.Set(key, blockedCount,
new DistributedCacheEntryOptions().SetSlidingExpiration(new TimeSpan(0, 5, 0)));
}
}

View File

@ -0,0 +1,48 @@
using System.Text.Json;
using Microsoft.Extensions.Caching.Distributed;
namespace Bit.Core.Utilities
{
public static class DistributedCacheExtensions
{
public static void Set<T>(this IDistributedCache cache, string key, T value)
{
Set(cache, key, value, new DistributedCacheEntryOptions());
}
public static void Set<T>(this IDistributedCache cache, string key, T value,
DistributedCacheEntryOptions options)
{
var bytes = JsonSerializer.SerializeToUtf8Bytes(value);
cache.Set(key, bytes, options);
}
public static Task SetAsync<T>(this IDistributedCache cache, string key, T value)
{
return SetAsync(cache, key, value, new DistributedCacheEntryOptions());
}
public static Task SetAsync<T>(this IDistributedCache cache, string key, T value,
DistributedCacheEntryOptions options)
{
var bytes = JsonSerializer.SerializeToUtf8Bytes(value);
return cache.SetAsync(key, bytes, options);
}
public static bool TryGetValue<T>(this IDistributedCache cache, string key, out T value)
{
var val = cache.Get(key);
value = default;
if (val == null) return false;
try
{
value = JsonSerializer.Deserialize<T>(val);
}
catch
{
return false;
}
return true;
}
}
}