Added new middleware to protect paths by authorization (also static files)
This commit is contained in:
@@ -4,7 +4,7 @@ using System.Net.Http.Headers;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using AMWD.Common.AspNetCore.BasicAuthentication;
|
||||
using AMWD.Common.AspNetCore.Security.BasicAuthentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
@@ -11,7 +11,7 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace AMWD.Common.AspNetCore.BasicAuthentication
|
||||
namespace AMWD.Common.AspNetCore.Security.BasicAuthentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements the <see cref="AuthenticationHandler{TOptions}"/> for Basic Authentication.
|
||||
@@ -6,7 +6,7 @@ using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace AMWD.Common.AspNetCore.BasicAuthentication
|
||||
namespace AMWD.Common.AspNetCore.Security.BasicAuthentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements a basic authentication.
|
||||
@@ -3,7 +3,7 @@ using System.Security.Claims;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AMWD.Common.AspNetCore.BasicAuthentication
|
||||
namespace AMWD.Common.AspNetCore.Security.BasicAuthentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface representing the validation of a basic authentication.
|
||||
@@ -0,0 +1,19 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
|
||||
namespace AMWD.Common.AspNetCore.Security.PathProtection
|
||||
{
|
||||
/// <summary>
|
||||
/// Extnsion for <see cref="IApplicationBuilder"/> to enable folder protection.
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
public static class ProtectedPathExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Provide protected paths even for static files.
|
||||
/// </summary>
|
||||
/// <param name="app">The <see cref="IApplicationBuilder"/>.</param>
|
||||
/// <param name="options">The <see cref="ProtectedPathOptions"/> with path and policy name.</param>
|
||||
public static IApplicationBuilder UseProtectedPath(this IApplicationBuilder app, ProtectedPathOptions options)
|
||||
=> app.UseMiddleware<ProtectedPathMiddleware>(options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace AMWD.Common.AspNetCore.Security.PathProtection
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements a check to provide protected paths.
|
||||
/// </summary>
|
||||
public class ProtectedPathMiddleware
|
||||
{
|
||||
private readonly RequestDelegate next;
|
||||
private readonly PathString path;
|
||||
private readonly string policyName;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ProtectedPathExtensions"/> class.
|
||||
/// </summary>
|
||||
/// <param name="next">The following delegate in the process chain.</param>
|
||||
/// <param name="options">The options to configure the middleware.</param>
|
||||
public ProtectedPathMiddleware(RequestDelegate next, ProtectedPathOptions options)
|
||||
{
|
||||
this.next = next;
|
||||
path = options.Path;
|
||||
policyName = options.PolicyName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The delegate invokation.
|
||||
/// Performs the protection check.
|
||||
/// </summary>
|
||||
/// <param name="httpContext">The corresponding HTTP context.</param>
|
||||
/// <param name="authorizationService">The <see cref="IAuthorizationService"/>.</param>
|
||||
/// <returns>An awaitable task.</returns>
|
||||
public async Task InvokeAsync(HttpContext httpContext, IAuthorizationService authorizationService)
|
||||
{
|
||||
if (httpContext.Request.Path.StartsWithSegments(path))
|
||||
{
|
||||
var result = await authorizationService.AuthorizeAsync(httpContext.User, null, policyName);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
await httpContext.ChallengeAsync();
|
||||
return;
|
||||
}
|
||||
}
|
||||
await next.Invoke(httpContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace AMWD.Common.AspNetCore.Security.PathProtection
|
||||
{
|
||||
/// <summary>
|
||||
/// Options to define which folder should be protected.
|
||||
/// </summary>
|
||||
public class ProtectedPathOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the path to the protected folder.
|
||||
/// </summary>
|
||||
public PathString Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the policy name to use.
|
||||
/// </summary>
|
||||
public string PolicyName { get; set; }
|
||||
}
|
||||
}
|
||||
13
CHANGELOG.md
13
CHANGELOG.md
@@ -5,11 +5,22 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Upcoming](https://git.am-wd.de/AM.WD/common/compare/v1.12.0...main) - 0000-00-00
|
||||
## [Upcoming](https://git.am-wd.de/AM.WD/common/compare/v1.13.0...main) - 0000-00-00
|
||||
|
||||
_no changes yet_
|
||||
|
||||
|
||||
## [v1.13.0](https://git.am-wd.de/AM.WD/common/compare/v1.12.0...v1.13.0) - 2023-06-27
|
||||
|
||||
### Added
|
||||
|
||||
- `ProtectedPathMiddleware` to secure even static file paths
|
||||
|
||||
### Changed
|
||||
|
||||
- Moved `BasicAuthentication`* into sub-namespace `Security`
|
||||
|
||||
|
||||
## [v1.12.0](https://git.am-wd.de/AM.WD/common/compare/v1.11.1...v1.12.0) - 2023-06-01
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -5,7 +5,7 @@ using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AMWD.Common.AspNetCore.BasicAuthentication;
|
||||
using AMWD.Common.AspNetCore.Security.BasicAuthentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -6,13 +6,13 @@ using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AMWD.Common.AspNetCore.BasicAuthentication;
|
||||
using AMWD.Common.AspNetCore.Security.BasicAuthentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Moq;
|
||||
|
||||
namespace UnitTests.AspNetCore.BasicAuthentication
|
||||
namespace UnitTests.AspNetCore.Security.BasicAuthentication
|
||||
{
|
||||
[TestClass]
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
@@ -0,0 +1,141 @@
|
||||
using System;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using AMWD.Common.AspNetCore.Security.PathProtection;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Moq;
|
||||
|
||||
namespace UnitTests.AspNetCore.Security.PathProtection
|
||||
{
|
||||
[TestClass]
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
public class ProtectedPathMiddlewareTests
|
||||
{
|
||||
private Mock<RequestDelegate> nextMock;
|
||||
private Mock<HttpContext> httpContextMock;
|
||||
private Mock<IAuthorizationService> authorizationServiceMock;
|
||||
private Mock<IAuthenticationService> authenticationServiceMock;
|
||||
|
||||
private ProtectedPathOptions options;
|
||||
|
||||
[TestInitialize]
|
||||
public void InitializeTest()
|
||||
{
|
||||
options = new ProtectedPathOptions
|
||||
{
|
||||
Path = "/secure/protected",
|
||||
PolicyName = "Protection"
|
||||
};
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldValidateAccessSuccessful()
|
||||
{
|
||||
// arrange
|
||||
var middleware = GetMiddleware();
|
||||
var context = GetHttpContext(options.Path);
|
||||
var auth = GetAuthService(true);
|
||||
|
||||
// act
|
||||
await middleware.InvokeAsync(context, auth);
|
||||
|
||||
// assert
|
||||
authorizationServiceMock.Verify(s => s.AuthorizeAsync(It.IsAny<ClaimsPrincipal>(), It.IsAny<object>(), options.PolicyName), Times.Once);
|
||||
authorizationServiceMock.VerifyNoOtherCalls();
|
||||
|
||||
authenticationServiceMock.Verify(s => s.ChallengeAsync(It.IsAny<HttpContext>(), It.IsAny<string>(), It.IsAny<AuthenticationProperties>()), Times.Never);
|
||||
authenticationServiceMock.VerifyNoOtherCalls();
|
||||
|
||||
nextMock.Verify(n => n.Invoke(It.IsAny<HttpContext>()), Times.Once);
|
||||
nextMock.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldNotValidate()
|
||||
{
|
||||
// arrange
|
||||
var middleware = GetMiddleware();
|
||||
var context = GetHttpContext("/some/path");
|
||||
var auth = GetAuthService(true);
|
||||
|
||||
// act
|
||||
await middleware.InvokeAsync(context, auth);
|
||||
|
||||
// assert
|
||||
authorizationServiceMock.Verify(s => s.AuthorizeAsync(It.IsAny<ClaimsPrincipal>(), It.IsAny<object>(), options.PolicyName), Times.Never);
|
||||
authorizationServiceMock.VerifyNoOtherCalls();
|
||||
|
||||
authenticationServiceMock.Verify(s => s.ChallengeAsync(It.IsAny<HttpContext>(), It.IsAny<string>(), It.IsAny<AuthenticationProperties>()), Times.Never);
|
||||
authenticationServiceMock.VerifyNoOtherCalls();
|
||||
|
||||
nextMock.Verify(n => n.Invoke(It.IsAny<HttpContext>()), Times.Once);
|
||||
nextMock.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ShouldValidateAccessFailure()
|
||||
{
|
||||
// arrange
|
||||
var middleware = GetMiddleware();
|
||||
var context = GetHttpContext(options.Path);
|
||||
var auth = GetAuthService(false);
|
||||
|
||||
// act
|
||||
await middleware.InvokeAsync(context, auth);
|
||||
|
||||
// assert
|
||||
authorizationServiceMock.Verify(s => s.AuthorizeAsync(It.IsAny<ClaimsPrincipal>(), It.IsAny<object>(), options.PolicyName), Times.Once);
|
||||
authorizationServiceMock.VerifyNoOtherCalls();
|
||||
|
||||
authenticationServiceMock.Verify(s => s.ChallengeAsync(It.IsAny<HttpContext>(), It.IsAny<string>(), It.IsAny<AuthenticationProperties>()), Times.Once);
|
||||
authenticationServiceMock.VerifyNoOtherCalls();
|
||||
|
||||
nextMock.Verify(n => n.Invoke(It.IsAny<HttpContext>()), Times.Never);
|
||||
nextMock.VerifyNoOtherCalls();
|
||||
}
|
||||
|
||||
private ProtectedPathMiddleware GetMiddleware()
|
||||
{
|
||||
nextMock = new Mock<RequestDelegate>();
|
||||
return new ProtectedPathMiddleware(nextMock.Object, options);
|
||||
}
|
||||
|
||||
private HttpContext GetHttpContext(string requestPath)
|
||||
{
|
||||
var requestMock = new Mock<HttpRequest>();
|
||||
requestMock
|
||||
.Setup(r => r.Path)
|
||||
.Returns(new PathString(requestPath));
|
||||
|
||||
authenticationServiceMock = new Mock<IAuthenticationService>();
|
||||
|
||||
var requestServicesMock = new Mock<IServiceProvider>();
|
||||
requestServicesMock
|
||||
.Setup(s => s.GetService(typeof(IAuthenticationService)))
|
||||
.Returns(authenticationServiceMock.Object);
|
||||
|
||||
httpContextMock = new Mock<HttpContext>();
|
||||
httpContextMock
|
||||
.Setup(c => c.Request)
|
||||
.Returns(requestMock.Object);
|
||||
httpContextMock
|
||||
.Setup(c => c.RequestServices)
|
||||
.Returns(requestServicesMock.Object);
|
||||
|
||||
return httpContextMock.Object;
|
||||
}
|
||||
|
||||
private IAuthorizationService GetAuthService(bool success)
|
||||
{
|
||||
authorizationServiceMock = new Mock<IAuthorizationService>();
|
||||
authorizationServiceMock
|
||||
.Setup(service => service.AuthorizeAsync(It.IsAny<ClaimsPrincipal>(), It.IsAny<object>(), It.IsAny<string>()))
|
||||
.ReturnsAsync(() => success ? AuthorizationResult.Success() : AuthorizationResult.Failed());
|
||||
|
||||
return authorizationServiceMock.Object;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user