diff --git a/src/Security/Authorization/Core/src/AuthorizationPolicyBuilder.cs b/src/Security/Authorization/Core/src/AuthorizationPolicyBuilder.cs
index 369145a52ead..5c41ede9a03e 100644
--- a/src/Security/Authorization/Core/src/AuthorizationPolicyBuilder.cs
+++ b/src/Security/Authorization/Core/src/AuthorizationPolicyBuilder.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Shared;
@@ -141,6 +142,20 @@ public AuthorizationPolicyBuilder RequireClaim(string claimType)
return this;
}
+ ///
+ /// Adds a to the current instance which requires
+ /// that the current user has a claim that satisfies the specified predicate.
+ ///
+ /// The predicate to evaluate the claims.
+ /// A reference to this instance after the operation has completed.
+ public AuthorizationPolicyBuilder RequireClaim(Func match)
+ {
+ ArgumentNullThrowHelper.ThrowIfNull(match);
+
+ Requirements.Add(new ClaimsAuthorizationRequirement(match));
+ return this;
+ }
+
///
/// Adds a to the current instance which enforces that the current user
/// must have at least one of the specified roles.
diff --git a/src/Security/Authorization/Core/src/ClaimsAuthorizationRequirement.cs b/src/Security/Authorization/Core/src/ClaimsAuthorizationRequirement.cs
index 55ae9b5f0445..b5e75d9b7fcf 100644
--- a/src/Security/Authorization/Core/src/ClaimsAuthorizationRequirement.cs
+++ b/src/Security/Authorization/Core/src/ClaimsAuthorizationRequirement.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Shared;
@@ -32,10 +33,22 @@ public ClaimsAuthorizationRequirement(string claimType, IEnumerable? all
_emptyAllowedValues = AllowedValues == null || !AllowedValues.Any();
}
+ ///
+ /// Creates a new instance of .
+ ///
+ /// The predicate to evaluate the claims.
+ public ClaimsAuthorizationRequirement(Func match)
+ {
+ ArgumentNullThrowHelper.ThrowIfNull(match);
+
+ Match = match;
+ _emptyAllowedValues = true;
+ }
+
///
/// Gets the claim type that must be present.
///
- public string ClaimType { get; }
+ public string? ClaimType { get; }
///
/// Gets the optional list of claim values, which, if present,
@@ -43,6 +56,12 @@ public ClaimsAuthorizationRequirement(string claimType, IEnumerable? all
///
public IEnumerable? AllowedValues { get; }
+ ///
+ /// A predicate to evaluate the claims.
+ /// Used if specified instead of and .
+ ///
+ public Func? Match { get; }
+
///
/// Makes a decision if authorization is allowed based on the claims requirements specified.
///
@@ -53,7 +72,12 @@ protected override Task HandleRequirementAsync(AuthorizationHandlerContext conte
if (context.User != null)
{
var found = false;
- if (requirement._emptyAllowedValues)
+
+ if (requirement.Match != null)
+ {
+ found = context.User.HasClaim(new Predicate(requirement.Match));
+ }
+ else if (requirement._emptyAllowedValues)
{
foreach (var claim in context.User.Claims)
{
@@ -76,17 +100,24 @@ protected override Task HandleRequirementAsync(AuthorizationHandlerContext conte
}
}
}
+
if (found)
{
context.Succeed(requirement);
}
}
+
return Task.CompletedTask;
}
///
public override string ToString()
{
+ if (Match != null)
+ {
+ return $"{nameof(ClaimsAuthorizationRequirement)}:Evaluates using a custom predicate";
+ }
+
var value = (_emptyAllowedValues)
? string.Empty
: $" and Claim.Value is one of the following values: ({string.Join("|", AllowedValues!)})";
diff --git a/src/Security/Authorization/Core/src/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/src/Security/Authorization/Core/src/PublicAPI/net10.0/PublicAPI.Unshipped.txt
index 7dc5c58110bf..f5b5801bf502 100644
--- a/src/Security/Authorization/Core/src/PublicAPI/net10.0/PublicAPI.Unshipped.txt
+++ b/src/Security/Authorization/Core/src/PublicAPI/net10.0/PublicAPI.Unshipped.txt
@@ -1 +1,6 @@
#nullable enable
+Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder.RequireClaim(System.Func! match) -> Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder!
+Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.ClaimsAuthorizationRequirement(System.Func! match) -> void
+*REMOVED*Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.ClaimType.get -> string!
+Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.ClaimType.get -> string?
+Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.Match.get -> System.Func?
diff --git a/src/Security/Authorization/Core/src/PublicAPI/net462/PublicAPI.Unshipped.txt b/src/Security/Authorization/Core/src/PublicAPI/net462/PublicAPI.Unshipped.txt
index 7dc5c58110bf..f5b5801bf502 100644
--- a/src/Security/Authorization/Core/src/PublicAPI/net462/PublicAPI.Unshipped.txt
+++ b/src/Security/Authorization/Core/src/PublicAPI/net462/PublicAPI.Unshipped.txt
@@ -1 +1,6 @@
#nullable enable
+Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder.RequireClaim(System.Func! match) -> Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder!
+Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.ClaimsAuthorizationRequirement(System.Func! match) -> void
+*REMOVED*Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.ClaimType.get -> string!
+Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.ClaimType.get -> string?
+Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.Match.get -> System.Func?
diff --git a/src/Security/Authorization/Core/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Security/Authorization/Core/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
index 7dc5c58110bf..f5b5801bf502 100644
--- a/src/Security/Authorization/Core/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
+++ b/src/Security/Authorization/Core/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
@@ -1 +1,6 @@
#nullable enable
+Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder.RequireClaim(System.Func! match) -> Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder!
+Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.ClaimsAuthorizationRequirement(System.Func! match) -> void
+*REMOVED*Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.ClaimType.get -> string!
+Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.ClaimType.get -> string?
+Microsoft.AspNetCore.Authorization.Infrastructure.ClaimsAuthorizationRequirement.Match.get -> System.Func?
diff --git a/src/Security/Authorization/test/ClaimsAuthorizationRequirementTests.cs b/src/Security/Authorization/test/ClaimsAuthorizationRequirementTests.cs
index 365c63c1ee90..0b63931523cc 100644
--- a/src/Security/Authorization/test/ClaimsAuthorizationRequirementTests.cs
+++ b/src/Security/Authorization/test/ClaimsAuthorizationRequirementTests.cs
@@ -1,17 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Security.Claims;
using Microsoft.AspNetCore.Authorization.Infrastructure;
namespace Microsoft.AspNetCore.Authorization.Test;
public class ClaimsAuthorizationRequirementTests
{
- public ClaimsAuthorizationRequirement CreateRequirement(string claimType, params string[] allowedValues)
- {
- return new ClaimsAuthorizationRequirement(claimType, allowedValues);
- }
-
[Fact]
public void ToString_ShouldReturnAndDescriptionWhenAllowedValuesNotNull()
{
@@ -50,4 +46,28 @@ public void ToString_ShouldReturnWithoutAllowedDescriptionWhenAllowedValuesIsEmp
// Assert
Assert.Equal("ClaimsAuthorizationRequirement:Claim.Type=Custom", formattedValue);
}
+
+ [Fact]
+ public void ToString_ShouldReturnPredicateDescriptionWhenPredicateIsUsed()
+ {
+ // Arrange
+ Func match = claim => claim.Type == "Permissions" && claim.Value.Contains("CanViewPage");
+ var requirement = CreateRequirement(match);
+
+ // Act
+ var formattedValue = requirement.ToString();
+
+ // Assert
+ Assert.Equal("ClaimsAuthorizationRequirement:Evaluates using a custom predicate", formattedValue);
+ }
+
+ private ClaimsAuthorizationRequirement CreateRequirement(string claimType, params string[] allowedValues)
+ {
+ return new ClaimsAuthorizationRequirement(claimType, allowedValues);
+ }
+
+ private ClaimsAuthorizationRequirement CreateRequirement(Func match)
+ {
+ return new ClaimsAuthorizationRequirement(match);
+ }
}
diff --git a/src/Security/Authorization/test/DefaultAuthorizationServiceTests.cs b/src/Security/Authorization/test/DefaultAuthorizationServiceTests.cs
index fcab4e0388b1..a87c3f556a48 100644
--- a/src/Security/Authorization/test/DefaultAuthorizationServiceTests.cs
+++ b/src/Security/Authorization/test/DefaultAuthorizationServiceTests.cs
@@ -90,6 +90,27 @@ public async Task Authorize_ShouldAllowIfClaimIsAmongValues()
Assert.True(allowed.Succeeded);
}
+ [Fact]
+ public async Task Authorize_ShouldAllowIfClaimMatchesPredicate()
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddAuthorizationBuilder().AddPolicy("Basic", policy =>
+ {
+ policy.AddAuthenticationSchemes("Basic");
+ policy.RequireClaim(claim => claim.Type == "Permission" && claim.Value == "CanViewPage");
+ });
+ });
+ var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] { new Claim("Permission", "CanViewPage") }, "Basic"));
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, "Basic");
+
+ // Assert
+ Assert.True(allowed.Succeeded);
+ }
+
[Fact]
public async Task Authorize_ShouldInvokeAllHandlersByDefault()
{
@@ -260,6 +281,27 @@ public async Task Authorize_ShouldNotAllowIfClaimValueIsNotPresent()
Assert.False(allowed.Succeeded);
}
+ [Fact]
+ public async Task Authorize_ShouldNotAllowIfClaimDoesNotMatchPredicate()
+ {
+ // Arrange
+ var authorizationService = BuildAuthorizationService(services =>
+ {
+ services.AddAuthorizationBuilder().AddPolicy("Basic", policy =>
+ {
+ policy.AddAuthenticationSchemes("Basic");
+ policy.RequireClaim(claim => claim.Type == "Permission" && claim.Value == "CanViewAnything");
+ });
+ });
+ var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] { new Claim("Permission", "CanViewPage") }, "Basic"));
+
+ // Act
+ var allowed = await authorizationService.AuthorizeAsync(user, "Basic");
+
+ // Assert
+ Assert.False(allowed.Succeeded);
+ }
+
[Fact]
public async Task Authorize_ShouldNotAllowIfNoClaims()
{