diff --git a/KeycloakAuthorizationServicesDotNet.sln b/KeycloakAuthorizationServicesDotNet.sln
index 14032f0e..1883ce6a 100644
--- a/KeycloakAuthorizationServicesDotNet.sln
+++ b/KeycloakAuthorizationServicesDotNet.sln
@@ -62,6 +62,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Keycloak.AuthServices.Sdk.T
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Keycloak.AuthServices.Authentication.Tests", "tests\Keycloak.AuthServices.Authentication.Tests\Keycloak.AuthServices.Authentication.Tests.csproj", "{FE34728A-25AA-44E1-A3A6-AB500307C406}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Keycloak.AuthServices.Authorization.Tests", "tests\Keycloak.AuthServices.Authorization.Tests\Keycloak.AuthServices.Authorization.Tests.csproj", "{DCFB0819-72C9-42DF-8AC6-3FE1E25C4103}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -124,6 +126,10 @@ Global
{FE34728A-25AA-44E1-A3A6-AB500307C406}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FE34728A-25AA-44E1-A3A6-AB500307C406}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FE34728A-25AA-44E1-A3A6-AB500307C406}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DCFB0819-72C9-42DF-8AC6-3FE1E25C4103}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DCFB0819-72C9-42DF-8AC6-3FE1E25C4103}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DCFB0819-72C9-42DF-8AC6-3FE1E25C4103}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DCFB0819-72C9-42DF-8AC6-3FE1E25C4103}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -144,6 +150,7 @@ Global
{5BA4F4CA-9C35-4D9F-A3AC-1E4273DA0807} = {AEBE10B1-96B1-4060-B8C1-1F9BFA7A586C}
{A85B6B1E-2030-47BE-9B9F-F645B08E501D} = {96857509-627A-4FD2-AC82-34387619A7B1}
{FE34728A-25AA-44E1-A3A6-AB500307C406} = {96857509-627A-4FD2-AC82-34387619A7B1}
+ {DCFB0819-72C9-42DF-8AC6-3FE1E25C4103} = {96857509-627A-4FD2-AC82-34387619A7B1}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E1907BFD-C144-4B48-AA40-972F499D4E08}
diff --git a/src/Keycloak.AuthServices.Authorization/UriBasedResourceProtection/UriBasedResourceProtectionMiddleware.cs b/src/Keycloak.AuthServices.Authorization/UriBasedResourceProtection/UriBasedResourceProtectionMiddleware.cs
index b4d79d12..bc0e7884 100644
--- a/src/Keycloak.AuthServices.Authorization/UriBasedResourceProtection/UriBasedResourceProtectionMiddleware.cs
+++ b/src/Keycloak.AuthServices.Authorization/UriBasedResourceProtection/UriBasedResourceProtectionMiddleware.cs
@@ -26,7 +26,7 @@ public class UriBasedResourceProtectionMiddleware
/// Thrown, if is null.
public UriBasedResourceProtectionMiddleware(RequestDelegate next, IKeycloakProtectionClient client)
{
- this.next = next;
+ this.next = next ?? throw new ArgumentNullException(nameof(next));
this.client = client ?? throw new ArgumentNullException(nameof(client));
}
@@ -72,10 +72,10 @@ private async Task EvaluateAuthorization(HttpContext context)
if (isAuthorized)
{
- context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return true;
}
+ context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return false;
}
diff --git a/tests/Keycloak.AuthServices.Authorization.Tests/Keycloak.AuthServices.Authorization.Tests.csproj b/tests/Keycloak.AuthServices.Authorization.Tests/Keycloak.AuthServices.Authorization.Tests.csproj
new file mode 100644
index 00000000..13b89eea
--- /dev/null
+++ b/tests/Keycloak.AuthServices.Authorization.Tests/Keycloak.AuthServices.Authorization.Tests.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Keycloak.AuthServices.Authorization.Tests/UriBasedResourceProtection/UriBasedResourceProtectionMiddlewareTests.cs b/tests/Keycloak.AuthServices.Authorization.Tests/UriBasedResourceProtection/UriBasedResourceProtectionMiddlewareTests.cs
new file mode 100644
index 00000000..232b3a0b
--- /dev/null
+++ b/tests/Keycloak.AuthServices.Authorization.Tests/UriBasedResourceProtection/UriBasedResourceProtectionMiddlewareTests.cs
@@ -0,0 +1,223 @@
+namespace Keycloak.AuthServices.Authorization.Tests;
+
+using FluentAssertions;
+using Keycloak.AuthServices.Sdk.AuthZ;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Moq;
+
+#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
+[System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "")]
+public class UriBasedResourceProtectionMiddlewareTests
+{
+ [Fact]
+ public void Constructor_DelegateNull_ThrowArgumentNullException()
+ {
+ // Arrange
+ var clientMock = new Mock();
+
+ // Act & Assert
+ Assert.Throws(() => _ = new UriBasedResourceProtectionMiddleware(null, clientMock.Object));
+ }
+
+ [Fact]
+ public void Constructor_KeycloakClientNull_ThrowArgumentNullException()
+ {
+ // Arrange
+ var requestDelegateMock = new Mock();
+
+ // Act & Assert
+ Assert.Throws(() => _ = new UriBasedResourceProtectionMiddleware(requestDelegateMock.Object, null));
+ }
+
+ [Fact]
+ public async void EvaluateAuthorization_NoAdditionalAttributesGiven_CallAuthorizationClientWithExpectedParameters()
+ {
+ // Arrange
+ var resourceName = "/resourceName";
+ var scope = "GET";
+
+ var clientMock = new Mock();
+ var requestDelegateMock = new Mock();
+ clientMock.Setup(x => x.VerifyAccessToResource(It.IsAny(), It.IsAny(), It.IsAny()))
+ .ReturnsAsync(true);
+
+ var httpRequestMock = new Mock();
+ httpRequestMock.Setup(x => x.Path).Returns(resourceName);
+ httpRequestMock.Setup(x => x.Method).Returns(scope);
+
+ var httpContextMock = new Mock();
+ httpContextMock.Setup(x => x.Request).Returns(httpRequestMock.Object);
+
+ // Act
+ var target = new UriBasedResourceProtectionMiddleware(requestDelegateMock.Object, clientMock.Object);
+ await target.InvokeAsync(httpContextMock.Object);
+
+ // Assert
+ clientMock.Verify(x => x.VerifyAccessToResource(resourceName, scope, CancellationToken.None), Times.Once);
+ }
+
+ [Fact]
+ public async void EvaluateAuthorization_DisableAttributesGiven_DontCallAuthorizationClientButCallDelegate()
+ {
+ // Arrange
+ var clientMock = new Mock();
+ var requestDelegateMock = new Mock();
+
+ var httpContextMock = new Mock();
+ httpContextMock.Setup(x => x.Request).Returns(new Mock().Object);
+ SetAttributesOnContextMock(httpContextMock,
+ new List() { new DisableUriBasedResourceProtectionAttribute() },
+ requestDelegateMock.Object);
+
+ // Act
+ var target = new UriBasedResourceProtectionMiddleware(requestDelegateMock.Object, clientMock.Object);
+ await target.InvokeAsync(httpContextMock.Object);
+
+ // Assert
+ clientMock.Verify(x => x.VerifyAccessToResource(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
+ requestDelegateMock.Verify(x => x.Invoke(httpContextMock.Object), Times.Once);
+ }
+
+ [Fact]
+ public async void EvaluateAuthorization_AllowAnonymousAttributesGiven_DontCallAuthorizationClientButCallDelegate()
+ {
+ // Arrange
+ var clientMock = new Mock();
+ var requestDelegateMock = new Mock();
+
+ var httpContextMock = new Mock();
+ httpContextMock.Setup(x => x.Request).Returns(new Mock().Object);
+ SetAttributesOnContextMock(httpContextMock,
+ new List() { new AllowAnonymousAttribute() },
+ requestDelegateMock.Object);
+
+ // Act
+ var target = new UriBasedResourceProtectionMiddleware(requestDelegateMock.Object, clientMock.Object);
+ await target.InvokeAsync(httpContextMock.Object);
+
+ // Assert
+ clientMock.Verify(x => x.VerifyAccessToResource(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
+ requestDelegateMock.Verify(x => x.Invoke(httpContextMock.Object), Times.Once);
+ }
+
+ [Fact]
+ public async void EvaluateAuthorization_DisableAndExplicitAuthAttributesGiven_DontCallAuthorizationClientButCallDelegate()
+ {
+ // Arrange
+ var clientMock = new Mock();
+ var requestDelegateMock = new Mock();
+
+ var httpContextMock = new Mock();
+ httpContextMock.Setup(x => x.Request).Returns(new Mock().Object);
+ SetAttributesOnContextMock(httpContextMock,
+ new List() {
+ new DisableUriBasedResourceProtectionAttribute(),
+ new ExplicitResourceProtectionAttribute(It.IsAny(), It.IsAny())
+ },
+ requestDelegateMock.Object);
+
+ // Act
+ var target = new UriBasedResourceProtectionMiddleware(requestDelegateMock.Object, clientMock.Object);
+ await target.InvokeAsync(httpContextMock.Object);
+
+ // Assert
+ clientMock.Verify(x => x.VerifyAccessToResource(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
+ requestDelegateMock.Verify(x => x.Invoke(httpContextMock.Object), Times.Once);
+ }
+
+ [Fact]
+ public async void EvaluateAuthorization_ExplicitAuthAttributesGiven_CallAuthorizationClientWithAttributeProperties()
+ {
+ // Arrange
+ var resourceName = "/resourceName";
+ var scope = "GET";
+ var resourceNameFromAttribute = "/resourceNameFromAttribute";
+ var scopeFromAttribute = "GET";
+
+ var clientMock = new Mock();
+ clientMock.Setup(x => x.VerifyAccessToResource(It.IsAny(), It.IsAny(), It.IsAny()))
+ .ReturnsAsync(true);
+ var requestDelegateMock = new Mock();
+
+ var httpRequestMock = new Mock();
+ httpRequestMock.Setup(x => x.Path).Returns(resourceName);
+ httpRequestMock.Setup(x => x.Method).Returns(scope);
+
+ var httpContextMock = new Mock();
+ httpContextMock.Setup(x => x.Request).Returns(httpRequestMock.Object);
+ SetAttributesOnContextMock(httpContextMock,
+ new List() {
+ new ExplicitResourceProtectionAttribute(resourceNameFromAttribute, scopeFromAttribute)
+ },
+ requestDelegateMock.Object);
+
+ // Act
+ var target = new UriBasedResourceProtectionMiddleware(requestDelegateMock.Object, clientMock.Object);
+ await target.InvokeAsync(httpContextMock.Object);
+
+ // Assert
+ clientMock.Verify(x => x.VerifyAccessToResource(resourceNameFromAttribute, scopeFromAttribute, CancellationToken.None), Times.Once);
+ clientMock.Verify(x => x.VerifyAccessToResource(resourceName, scope, CancellationToken.None), Times.Never);
+ }
+
+ [Fact]
+ public async void EvaluateAuthorization_ClientReturnsTrue_CallDelegate()
+ {
+ // Arrange
+ var clientMock = new Mock();
+ clientMock.Setup(x => x.VerifyAccessToResource(It.IsAny(), It.IsAny(), It.IsAny()))
+ .ReturnsAsync(true);
+ var requestDelegateMock = new Mock();
+
+ var httpContextMock = new Mock();
+ httpContextMock.Setup(x => x.Request).Returns(new Mock().Object);
+
+ // Act
+ var target = new UriBasedResourceProtectionMiddleware(requestDelegateMock.Object, clientMock.Object);
+ await target.InvokeAsync(httpContextMock.Object);
+
+ // Assert
+ requestDelegateMock.Verify(x => x.Invoke(httpContextMock.Object), Times.Once);
+ }
+
+ [Fact]
+ public async void EvaluateAuthorization_ClientReturnsFalse_DontCallDelegateAndSet401()
+ {
+ // Arrange
+
+ var clientMock = new Mock();
+ clientMock.Setup(x => x.VerifyAccessToResource(It.IsAny(), It.IsAny(), It.IsAny()))
+ .ReturnsAsync(false);
+ var requestDelegateMock = new Mock();
+
+ var httpResponseMock = new Mock();
+ httpResponseMock.SetupAllProperties();
+
+ var httpContextMock = new Mock();
+ httpContextMock.Setup(x => x.Request).Returns(new Mock().Object);
+ httpContextMock.Setup(x => x.Response).Returns(httpResponseMock.Object);
+
+ // Act
+ var target = new UriBasedResourceProtectionMiddleware(requestDelegateMock.Object, clientMock.Object);
+ await target.InvokeAsync(httpContextMock.Object);
+
+ // Assert
+ requestDelegateMock.Verify(x => x.Invoke(httpContextMock.Object), Times.Never);
+ httpContextMock.Object.Response.StatusCode.Should().Be(StatusCodes.Status401Unauthorized);
+ }
+
+ private static void SetAttributesOnContextMock(Mock httpContextMock, IEnumerable attributes, RequestDelegate requestDelegate)
+ {
+ var endpoint = new Endpoint(requestDelegate, new EndpointMetadataCollection(attributes), null);
+
+ var endpointFeatureMock = new Mock();
+ endpointFeatureMock.Setup(x => x.Endpoint).Returns(endpoint);
+
+ var featuresMock = new Mock();
+ featuresMock.Setup(x => x.Get()).Returns(endpointFeatureMock.Object);
+
+ httpContextMock.Setup(x => x.Features).Returns(featuresMock.Object);
+ }
+}
\ No newline at end of file