Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 27 additions & 12 deletions LearningHub.Nhs.WebUI/Controllers/HomeController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

/// <summary>
/// Initializes a new instance of the <see cref="HomeController"/> class.
Expand All @@ -53,6 +54,7 @@ public class HomeController : BaseController
/// <param name="dashboardService">Dashboard service.</param>
/// <param name="contentService">Content service.</param>
/// <param name="featureManager"> featureManager.</param>
/// <param name="configuration"> config.</param>
public HomeController(
IHttpClientFactory httpClientFactory,
IWebHostEnvironment hostingEnvironment,
Expand All @@ -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;
Expand All @@ -72,6 +75,7 @@ public HomeController(
this.dashboardService = dashboardService;
this.contentService = contentService;
this.featureManager = featureManager;
this.configuration = configuration;
}

/// <summary>
Expand Down Expand Up @@ -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<IStatusCodeReExecuteFeature>();
originalPathUrlMessage = $"Page Not Found url: {exceptionHandlerPathFeature?.OriginalPath}. ";
originalPath = exceptionHandlerPathFeature?.OriginalPath;
originalPathUrlMessage = $"Page Not Found url: {originalPath}. ";
}

if (this.User.Identity.IsAuthenticated)
Expand Down Expand Up @@ -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");
}
}
}

Expand Down
1 change: 1 addition & 0 deletions LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@


<ItemGroup>
<PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.1" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.23.0" />
<PackageReference Include="elfhHub.Nhs.Models" Version="3.0.9" />
Expand Down
57 changes: 57 additions & 0 deletions LearningHub.Nhs.WebUI/Middleware/LHIPRateLimitMiddleware.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Defines the <see cref="LHIPRateLimitMiddleware" />.
/// </summary>
public class LHIPRateLimitMiddleware : IpRateLimitMiddleware
{
/// <summary>
/// Initializes a new instance of the <see cref="LHIPRateLimitMiddleware"/> class.
/// </summary>
/// <param name="next">The next.</param>
/// <param name="processingStrategy">The processingStrategy.</param>
/// <param name="options">The options.</param>
/// <param name="policyStore">The policyStore.</param>
/// <param name="config">The config.</param>
/// <param name="logger">The logger.</param>
public LHIPRateLimitMiddleware(
RequestDelegate next,
IProcessingStrategy processingStrategy,
IOptions<IpRateLimitOptions> options,
IIpPolicyStore policyStore,
IRateLimitConfiguration config,
ILogger<IpRateLimitMiddleware> logger)
: base(
next,
processingStrategy,
options,
policyStore,
config,
logger)
{
}

/// <summary>
/// The ReturnQuotaExceededResponse method.
/// </summary>
/// <param name="httpContext">The httpContext.</param>
/// <param name="rule">The rule.</param>
/// <param name="retryAfter">The retryAfter.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
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);
}
}
}
2 changes: 1 addition & 1 deletion LearningHub.Nhs.WebUI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
app.UseAuthorization();

app.UseMiddleware<NLogMiddleware>();

app.UseMiddleware<LHIPRateLimitMiddleware>();
app.UseStaticFiles();

app.Map(TimezoneInfoMiddleware.TimezoneInfoUrl, b => b.UseMiddleware<TimezoneInfoMiddleware>());
Expand Down
15 changes: 15 additions & 0 deletions LearningHub.Nhs.WebUI/ServiceCollectionExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -136,5 +139,17 @@ public static void ConfigureServices(this IServiceCollection services, IConfigur

services.AddFeatureManagement();
}

/// <summary>
/// ConfigureIpRateLimiting.
/// </summary>
/// <param name="services">The services.</param>
/// <param name="configuration">The configuration.</param>
private static void ConfigureIpRateLimiting(IServiceCollection services, IConfiguration configuration)
{
services.Configure<IpRateLimitOptions>(configuration.GetSection("IpRateLimiting"));
services.AddInMemoryRateLimiting();
services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
}
}
}
22 changes: 22 additions & 0 deletions LearningHub.Nhs.WebUI/Views/Shared/TooManyRequests.cshtml
Original file line number Diff line number Diff line change
@@ -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);
}
}
<div class="bg-white">
<div class="nhsuk-width-container app-width-container">
<div class="nhsuk-grid-row">
<div class="nhsuk-grid-column-full nhsuk-u-padding-top-9 nhsuk-u-padding-bottom-7">
<h1 class="nhsuk-heading-xl"> @ViewData["Title"]</h1>
<p>You've requested the maximum number of password resets (@ViewBag.Limit). Please wait @period minutes before trying again or contact the <a href="@ViewBag.SupportFormUrl" target="_blank">support team</a>.</p>
<p>@DateTimeOffset.Now.ToString("d MMMM yyyy HH:mm:ss")</p>
</div>
</div>
</div>
</div>
17 changes: 15 additions & 2 deletions LearningHub.Nhs.WebUI/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@
},
"EnableTempDebugging": "false",
"LimitScormToAdmin": "false"

},
"LearningHubAuthServiceConfig": {
"Authority": "",
Expand Down Expand Up @@ -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
}
]
}
}
Loading