diff --git a/src/Api/Middleware/CustomIpRateLimitMiddleware.cs b/src/Api/Middleware/CustomIpRateLimitMiddleware.cs new file mode 100644 index 0000000000..0b5544ecad --- /dev/null +++ b/src/Api/Middleware/CustomIpRateLimitMiddleware.cs @@ -0,0 +1,39 @@ +using AspNetCoreRateLimit; +using Bit.Api.Models.Response; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using System.Threading.Tasks; + +namespace Bit.Api.Middleware +{ + public class CustomIpRateLimitMiddleware : IpRateLimitMiddleware + { + private readonly IpRateLimitOptions _options; + + public CustomIpRateLimitMiddleware( + RequestDelegate next, + IOptions options, + IRateLimitCounterStore counterStore, + IIpPolicyStore policyStore, + ILogger logger, + IIpAddressParser ipParser = null + ) : base(next, options, counterStore, policyStore, logger, ipParser) + { + _options = options.Value; + } + + 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; + httpContext.Response.Headers["Retry-After"] = retryAfter; + httpContext.Response.StatusCode = _options.HttpStatusCode; + + httpContext.Response.ContentType = "application/json"; + var errorModel = new ErrorResponseModel { Message = message }; + return httpContext.Response.WriteAsync(JsonConvert.SerializeObject(errorModel)); + } + } +} diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 50f2b590a2..fb9f856e11 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -23,6 +23,8 @@ using System.Linq; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Net.Http.Headers; using Newtonsoft.Json.Serialization; +using AspNetCoreRateLimit; +using Bit.Api.Middleware; namespace Bit.Api { @@ -61,6 +63,8 @@ namespace Bit.Api var globalSettings = new GlobalSettings(); ConfigurationBinder.Bind(Configuration.GetSection("GlobalSettings"), globalSettings); services.AddSingleton(s => globalSettings); + services.Configure(Configuration.GetSection("IpRateLimitOptions")); + services.Configure(Configuration.GetSection("IpRateLimitPolicies")); // Repositories services.AddSingleton(); @@ -70,6 +74,13 @@ namespace Bit.Api // Context services.AddScoped(); + // Caching + services.AddMemoryCache(); + + // Rate limiting + services.AddSingleton(); + services.AddSingleton(); + // Identity services.AddTransient(); services.AddJwtBearerIdentity(options => @@ -176,6 +187,10 @@ namespace Bit.Api globalSettings.Loggr.ApiKey); } + // Rate limiting + app.UseMiddleware(); + + // Insights app.UseApplicationInsightsRequestTelemetry(); app.UseApplicationInsightsExceptionTelemetry(); diff --git a/src/Api/project.json b/src/Api/project.json index 4234a22c5a..afd779016d 100644 --- a/src/Api/project.json +++ b/src/Api/project.json @@ -18,8 +18,10 @@ "Microsoft.AspNetCore.Diagnostics": "1.0.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0", "Microsoft.Extensions.Configuration.Binder": "1.0.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0", "Loggr.Extensions.Logging": "1.0.0", - "Microsoft.ApplicationInsights.AspNetCore": "1.0.0" + "Microsoft.ApplicationInsights.AspNetCore": "1.0.0", + "AspNetCoreRateLimit": "1.0.2" }, "tools": { diff --git a/src/Api/settings.json b/src/Api/settings.json index 26b846e48c..be0384f957 100644 --- a/src/Api/settings.json +++ b/src/Api/settings.json @@ -24,5 +24,60 @@ "gcmApiKey": "SECRET", "gcmAppPackageName": "com.x8bit.bitwarden" } + }, + "IpRateLimitOptions": { + "EnableEndpointRateLimiting": true, + "StackBlockedRequests": false, + "RealIpHeader": "X-Forwarded-For", + "ClientIdHeader": "X-ClientId", + "HttpStatusCode": 429, + "IpWhitelist": [], + "EndpointWhitelist": [], + "ClientWhitelist": [], + "GeneralRules": [ + { + "Endpoint": "post:/auth/token", + "Period": "1m", + "Limit": 10 + }, + { + "Endpoint": "post:/auth/token/two-factor", + "Period": "1m", + "Limit": 5 + }, + { + "Endpoint": "post:/accounts/register", + "Period": "1m", + "Limit": 2 + }, + { + "Endpoint": "post:/account/password-hint", + "Period": "1m", + "Limit": 2 + }, + { + "Endpoint": "post:/account/email-token", + "Period": "1m", + "Limit": 2 + }, + { + "Endpoint": "post:/account/email", + "Period": "1m", + "Limit": 5 + }, + { + "Endpoint": "put:/account/email", + "Period": "1m", + "Limit": 5 + }, + { + "Endpoint": "get:/alive", + "Period": "1m", + "Limit": 5 + } + ] + }, + "IpRateLimitPolicies": { + "IpRules": [] } }