diff --git a/LearningHub.Nhs.WebUI/Controllers/HomeController.cs b/LearningHub.Nhs.WebUI/Controllers/HomeController.cs index 87d01e668..ea18e6cd4 100644 --- a/LearningHub.Nhs.WebUI/Controllers/HomeController.cs +++ b/LearningHub.Nhs.WebUI/Controllers/HomeController.cs @@ -15,13 +15,13 @@ namespace LearningHub.Nhs.WebUI.Controllers using LearningHub.Nhs.WebUI.Helpers; using LearningHub.Nhs.WebUI.Interfaces; using LearningHub.Nhs.WebUI.Models; - using Microsoft.ApplicationInsights.AspNetCore; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; + using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.FeatureManagement; @@ -39,6 +39,7 @@ public class HomeController : BaseController private readonly IDashboardService dashboardService; private readonly IContentService contentService; private readonly IFeatureManager featureManager; + private readonly Microsoft.Extensions.Configuration.IConfiguration configuration; /// /// Initializes a new instance of the class. @@ -53,6 +54,7 @@ public class HomeController : BaseController /// Dashboard service. /// Content service. /// featureManager. + /// config. public HomeController( IHttpClientFactory httpClientFactory, IWebHostEnvironment hostingEnvironment, @@ -63,7 +65,8 @@ public HomeController( LearningHubAuthServiceConfig authConfig, IDashboardService dashboardService, IContentService contentService, - IFeatureManager featureManager) + IFeatureManager featureManager, + Microsoft.Extensions.Configuration.IConfiguration configuration) : base(hostingEnvironment, httpClientFactory, logger, settings.Value) { this.authConfig = authConfig; @@ -72,6 +75,7 @@ public HomeController( this.dashboardService = dashboardService; this.contentService = contentService; this.featureManager = featureManager; + this.configuration = configuration; } /// @@ -133,11 +137,12 @@ public IActionResult CreateAccount() public IActionResult Error(int? httpStatusCode) { string originalPathUrlMessage = null; - + string originalPath = null; if (httpStatusCode.HasValue && httpStatusCode.Value == 404) { var exceptionHandlerPathFeature = this.HttpContext.Features.Get(); - originalPathUrlMessage = $"Page Not Found url: {exceptionHandlerPathFeature?.OriginalPath}. "; + originalPath = exceptionHandlerPathFeature?.OriginalPath; + originalPathUrlMessage = $"Page Not Found url: {originalPath}. "; } if (this.User.Identity.IsAuthenticated) @@ -165,16 +170,26 @@ public IActionResult Error(int? httpStatusCode) } else { - this.ViewBag.ErrorHeader = httpStatusCode.Value switch + if (originalPath == "/TooManyRequests") { - 401 => "You do not have permission to access this page", - 404 => "We cannot find the page you are looking for", - _ => "We cannot find the page you are looking for", - }; + this.ViewBag.Period = this.configuration["IpRateLimiting:GeneralRules:0:Period"]; + this.ViewBag.Limit = this.configuration["IpRateLimiting:GeneralRules:0:Limit"]; - this.ViewBag.HttpStatusCode = httpStatusCode.Value; - this.ViewBag.HomePageUrl = "/home"; - return this.View("CustomError"); + return this.View("TooManyRequests"); + } + else + { + this.ViewBag.ErrorHeader = httpStatusCode.Value switch + { + 401 => "You do not have permission to access this page", + 404 => "We cannot find the page you are looking for", + _ => "We cannot find the page you are looking for", + }; + + this.ViewBag.HttpStatusCode = httpStatusCode.Value; + this.ViewBag.HomePageUrl = "/home"; + return this.View("CustomError"); + } } } diff --git a/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj b/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj index 1461f089a..66ab9d294 100644 --- a/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj +++ b/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj @@ -104,6 +104,7 @@ + diff --git a/LearningHub.Nhs.WebUI/Middleware/LHIPRateLimitMiddleware.cs b/LearningHub.Nhs.WebUI/Middleware/LHIPRateLimitMiddleware.cs new file mode 100644 index 000000000..31f2f62f5 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Middleware/LHIPRateLimitMiddleware.cs @@ -0,0 +1,57 @@ +namespace LearningHub.Nhs.WebUI.Middleware +{ + using System.Threading.Tasks; + using AspNetCoreRateLimit; + using Microsoft.AspNetCore.Http; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; + + /// + /// Defines the . + /// + public class LHIPRateLimitMiddleware : IpRateLimitMiddleware + { + /// + /// Initializes a new instance of the class. + /// + /// The next. + /// The processingStrategy. + /// The options. + /// The policyStore. + /// The config. + /// The logger. + public LHIPRateLimitMiddleware( + RequestDelegate next, + IProcessingStrategy processingStrategy, + IOptions options, + IIpPolicyStore policyStore, + IRateLimitConfiguration config, + ILogger logger) + : base( + next, + processingStrategy, + options, + policyStore, + config, + logger) + { + } + + /// + /// The ReturnQuotaExceededResponse method. + /// + /// The httpContext. + /// The rule. + /// The retryAfter. + /// A representing the asynchronous operation. + public override Task ReturnQuotaExceededResponse( + HttpContext httpContext, + RateLimitRule rule, + string retryAfter) + { + httpContext.Response.Headers["Location"] = "/TooManyRequests"; + httpContext.Response.StatusCode = 302; + return httpContext.Response.WriteAsync(string.Empty); + } + } +} diff --git a/LearningHub.Nhs.WebUI/Program.cs b/LearningHub.Nhs.WebUI/Program.cs index 8aee50089..2de0aa053 100644 --- a/LearningHub.Nhs.WebUI/Program.cs +++ b/LearningHub.Nhs.WebUI/Program.cs @@ -84,7 +84,7 @@ app.UseAuthorization(); app.UseMiddleware(); - + app.UseMiddleware(); app.UseStaticFiles(); app.Map(TimezoneInfoMiddleware.TimezoneInfoUrl, b => b.UseMiddleware()); diff --git a/LearningHub.Nhs.WebUI/ServiceCollectionExtension.cs b/LearningHub.Nhs.WebUI/ServiceCollectionExtension.cs index aa90f135e..1e538331e 100644 --- a/LearningHub.Nhs.WebUI/ServiceCollectionExtension.cs +++ b/LearningHub.Nhs.WebUI/ServiceCollectionExtension.cs @@ -3,6 +3,7 @@ using System; using System.IdentityModel.Tokens.Jwt; using System.Net; + using AspNetCoreRateLimit; using LearningHub.Nhs.Caching; using LearningHub.Nhs.Models.Binders; using LearningHub.Nhs.Models.Enums; @@ -110,6 +111,8 @@ public static void ConfigureServices(this IServiceCollection services, IConfigur } }); + ConfigureIpRateLimiting(services, configuration); + // this method setup so httpcontext is available from controllers services.AddHttpContextAccessor(); services.AddSingleton(learningHubAuthSvcConf); @@ -136,5 +139,17 @@ public static void ConfigureServices(this IServiceCollection services, IConfigur services.AddFeatureManagement(); } + + /// + /// ConfigureIpRateLimiting. + /// + /// The services. + /// The configuration. + private static void ConfigureIpRateLimiting(IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration.GetSection("IpRateLimiting")); + services.AddInMemoryRateLimiting(); + services.AddSingleton(); + } } } \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI/Views/Shared/TooManyRequests.cshtml b/LearningHub.Nhs.WebUI/Views/Shared/TooManyRequests.cshtml new file mode 100644 index 000000000..b0b967408 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Views/Shared/TooManyRequests.cshtml @@ -0,0 +1,22 @@ +@{ + ViewData["Title"] = "Reset limit reached"; + // Get the value from ViewBag + var period = ViewBag.Period.ToString(); + + // Remove the last character (if the string is not empty) + if (!string.IsNullOrEmpty(period) && period.Length > 0) + { + period = period.Substring(0, period.Length - 1); + } +} +
+
+
+
+

@ViewData["Title"]

+

You've requested the maximum number of password resets (@ViewBag.Limit). Please wait @period minutes before trying again or contact the support team.

+

@DateTimeOffset.Now.ToString("d MMMM yyyy HH:mm:ss")

+
+
+
+
\ No newline at end of file diff --git a/LearningHub.Nhs.WebUI/appsettings.json b/LearningHub.Nhs.WebUI/appsettings.json index b459b9b6b..e50f1ad55 100644 --- a/LearningHub.Nhs.WebUI/appsettings.json +++ b/LearningHub.Nhs.WebUI/appsettings.json @@ -113,7 +113,7 @@ }, "EnableTempDebugging": "false", "LimitScormToAdmin": "false" - + }, "LearningHubAuthServiceConfig": { "Authority": "", @@ -158,5 +158,18 @@ "FeatureManagement": { "ContributeAudioVideoResource": true, "DisplayAudioVideoResource": true - } + }, + "IpRateLimiting": { + "EnableEndpointRateLimiting": true, + "StackBlockedRequests": false, + "RealIpHeader": "X-Real-IP", + "HttpStatusCode": 429, + "GeneralRules": [ + { + "Endpoint": "post:/Account/ForgotPassword", + "Period": "1m", + "Limit": 5 + } + ] + } }