dotnet / dotnet/aspnetcore

RequireClaim ignores identity-specific claim-type comparison

Open
#69,145 1 comment 0 reactions 0 assignees View on GitHub
area-auth
Dominant language
C#
Stars
38.4k
Forks
10.9k
Avg merge
2d 10h
Merged PRs (30d)
281

Description

## Summary

ClaimsAuthorizationRequirement, used by RequireClaim, bypasses identity-specific claim-type lookup. Both its presence-only and allowed-values branches enumerate User.Claims and compare Claim.Type using StringComparison.OrdinalIgnoreCase. Allowed values are already compared using StringComparer.Ordinal.

Consequently, RequireClaim("role", "admin") accepts an available ROLE=admin claim even when it belongs to CaseSensitiveClaimsIdentity, whose named lookup rejects that type-case mismatch. It does not accept role=Admin. The supplied claim type is not resolved through RoleClaimType.

The MVC claims tutorial advertises identity-dependent type matching in a workflow demonstrated with RequireClaim. The implementation does not preserve that behavior for case-sensitive identities. This is an authorization-correctness and documentation/implementation mismatch that warrants a coordinated code and documentation fix.

## Verified implementation

The behavior was inspected at public dotnet/aspnetcore main commit ccae41d7332af860d5e48d8caf8e3ffa3227f8b1 on 2026-09-08:

* [AuthorizationPolicyBuilder.RequireClaim overloads](https://github.com/dotnet/aspnetcore/blob/ccae41d7332af860d5e48d8caf8e3ffa3227f8b1/src/Security/Authorization/Core/src/AuthorizationPolicyBuilder.cs#L108) add ClaimsAuthorizationRequirement.
* [The requirement handler](https://github.com/dotnet/aspnetcore/blob/ccae41d7332af860d5e48d8caf8e3ffa3227f8b1/src/Security/Authorization/Core/src/ClaimsAuthorizationRequirement.cs#L51) uses OrdinalIgnoreCase for types in both branches and Ordinal for allowed values.
* [The constructor](https://github.com/dotnet/aspnetcore/blob/ccae41d7332af860d5e48d8caf8e3ffa3227f8b1/src/Security/Authorization/Core/src/ClaimsAuthorizationRequirement.cs#L26) treats null or empty allowedValues as presence-only. An existing matching type is still required.
* [ClaimsPrincipal.FindAll(string)](https://github.com/dotnet/runtime/blob/040defaa3b808c418df16029e98c5af3b5273583/src/libraries/System.Security.Claims/src/System/Security/Claims/ClaimsPrincipal.cs#L308) delegates to each identity's named lookup in its default implementation.
* [CaseSensitiveClaimsIdentity.FindAll(string)](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/cce7c7fea3faa6281443b105a277b9cc0676f933/src/Microsoft.IdentityModel.Tokens/CaseSensitiveClaimsIdentity.cs#L94) uses ordinal type comparison. The requirement bypasses this extension point.

The source snapshots establish current implementation behavior, not a complete affected-release range. No first-fixed version has been selected.

## Documented behavior

The [MVC claims article](https://github.com/dotnet/AspNetCore.Docs/blob/59f307f054d866e95064e8e9813352289350c23c/aspnetcore/mvc/security/authorization/claims.md#L177) describes claim-type comparison as being:

> "used to locate claims by their type, such as `email`"

and says it may be case-sensitive or case-insensitive depending on the ClaimsIdentity implementation. The paragraph identifies CaseSensitiveClaimsIdentity produced by Microsoft.IdentityModel token validation in ASP.NET Core 8.0 or later. It distinguishes the default identity's case-insensitive lookup, then says:

> "This also applies to roles when they're represented as claims."

The [directly included modern sample](https://github.com/dotnet/AspNetCore.Docs.Samples/blob/8dbb6f86c84f78cfd4a61f9207d55199635595bf/security/authorization/claims/7.x/WebAll/Program.cs#L10) uses RequireClaim("EmployeeNumber"), and its second policy uses RequireClaim with allowed values. Neither the examples nor the case-sensitivity section discloses a fixed-comparer exception for RequireClaim. Read in the tutorial's context, this is affirmative guidance about the demonstrated claim checks, not solely about role authorization.

The distinct [general claims article](https://github.com/dotnet/AspNetCore.Docs/blob/59f307f054d866e95064e8e9813352289350c23c/aspnetcore/security/authorization/claims.md#L340) currently qualifies its type discussion as role-claim lookup. It also fails to distinguish RequireClaim's independent comparison. These two articles should not be conflated.

The MVC guidance does not promise ordinal type matching for every identity. It promises identity-dependent behavior. Its ordinal allowed-value statement agrees with the handler. The section is not locally moniker-conditioned; the ASP.NET Core 8.0 statement concerns identity production, not a RequireClaim fix shipped in that version. These are current source snapshots, not a documentation-history claim.

The [API builder XML](https://github.com/dotnet/AspNetApiDocs/blob/61990ff7e39ccce2faa75dd741ab8513342b02a5/aspnet-core/xml/Microsoft.AspNetCore.Authorization/AuthorizationPolicyBuilder.xml#L454) has "To be added." remarks for all three RequireClaim overloads. The [requirement API XML](https://github.com/dotnet/AspNetApiDocs/blob/61990ff7e39ccce2faa75dd741ab8513342b02a5/aspnet-core/xml/Microsoft.AspNetCore.Authorization.Infrastructure/ClaimsAuthorizationRequirement.xml#L44) likewise lacks the comparison explanation. Those omissions do not negate the MVC tutorial's affirmative guidance.

## Specification findings

* [RFC 7519 section 7.3](https://www.rfc-editor.org/rfc/rfc7519.html#section-7.3) requires JSON string comparison rules for both names and values, except when a member's definition specifies a different value-comparison rule. Compare decoded strings, not JSON escape spellings. The typ and cty header values are explicit exceptions in that RFC.
* [RFC 7519 section 10.1.1](https://www.rfc-editor.org/rfc/rfc7519.html#section-10.1.1) states of a registered claim name: "This name is case sensitive." The separate restriction against registering case-insensitively colliding names is registry policy, not permission to fold names during lookup.
* [OpenID Connect Core section 14](https://openid.net/specs/openid-connect-core-1_0.html#StringOps) requires removing JSON escaping, forbids Unicode normalization, and requires Unicode code-point equality. Differently cased claim names remain distinct.

These protocol rules explain why preserving case-sensitive identity semantics matters; they do not require every protocol-independent ClaimsPrincipal to become ordinal. Inbound mapping and transformations can change names before authorization. This issue concerns the comparison of the resulting claims, not a promise that every raw JWT field becomes an unchanged Claim.Type.

## Recommended code change

Select claims through context.User.FindAll(requirement.ClaimType), then retain the explicit ordinal allowed-value check. For example, replace the body of HandleRequirementAsync with:

```csharp
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
ClaimsAuthorizationRequirement requirement)
{
if (context.User != null)
{
foreach (var claim in context.User.FindAll(requirement.ClaimType))
{
if (requirement._emptyAllowedValues ||
requirement.AllowedValues!.Contains(claim.Value, StringComparer.Ordinal))
{
context.Succeed(requirement);
break;
}
}
}

return Task.CompletedTask;
}
```

This proposed implementation has not been compiled or executed. It preserves constructor validation, null/empty allowed-values semantics, same-claim type/value matching, success signaling, and completed-task behavior. It adds no production IdentityModel dependency and requires no new public overload.

Do not add an authentication requirement, issuer filter, wildcard handling, role alias, or name normalization as part of this change. Do not replace the ordinal value check with HasClaim(type, value), which would also delegate value-comparison semantics to overrides.

## Compatibility considerations

* Default-configured ordinary ClaimsIdentity instances retain their documented case-insensitive type lookup. Specialized or explicitly configured identities can supply different rules. This is not a universal ordinal switch.
* Case-sensitive identities intentionally stop satisfying case-variant requirements. Custom ClaimsPrincipal.FindAll or identity lookup overrides may narrow or broaden the selected set, so this requires compatibility review.
* A custom principal that overrides only Claims may expose a different set than inherited FindAll. Explicitly test and decide this extension-point interaction rather than assuming the change is nonbreaking.
* The default principal searches all identities. A case-sensitive identity does not veto a match returned by another case-insensitive identity in the same principal.
* Type and allowed value must come from the same returned Claim. A wrong-valued earlier match must not hide a later matching claim.
* An empty allowed-values collection means unrestricted value, not the same as a nonempty collection containing an empty string.
* Neither the existing requirement nor this proposal authenticates the matching identity or establishes claim-source authority. RequireAuthenticatedUser independently checks for an authenticated identity; it does not bind the matched claim to that identity.

The preferred direction is identity-consistent selection plus ordinal values, not an unreviewed global Ordinal replacement. Replacing RequireClaim with RequireRole is not a universal workaround because RoleClaimType and identity role lookup are a different contract.

## Recommended documentation changes

1. Update XML remarks for all RequireClaim overloads in src/Security/Authorization/Core/src/AuthorizationPolicyBuilder.cs and the requirement/class properties in src/Security/Authorization/Core/src/ClaimsAuthorizationRequirement.cs.
2. Coordinate the independent API reference in dotnet/AspNetApiDocs: aspnet-core/xml/Microsoft.AspNetCore.Authorization/AuthorizationPolicyBuilder.xml and aspnet-core/xml/Microsoft.AspNetCore.Authorization.Infrastructure/ClaimsAuthorizationRequirement.xml. Do not assume C# XML edits alone update these files.
3. Update aspnetcore/mvc/security/authorization/claims.md and aspnetcore/security/authorization/claims.md in dotnet/AspNetCore.Docs, next to the examples and case-sensitivity explanation. Distinguish RequireClaim from RequireRole and identity named lookup. Check the [roles article cross-reference](https://github.com/dotnet/AspNetCore.Docs/blob/59f307f054d866e95064e8e9813352289350c23c/aspnetcore/security/authorization/roles.md#L214) for consistency; no role implementation change is proposed.
4. For the version that adopts the fix, document that RequireClaim selects claims via ClaimsPrincipal.FindAll(claimType), whose default implementation delegates to each identity. Allowed values remain ordinal case-sensitive. Explain literal supplied type names, post-mapping claims, custom overrides, and mixed identities.
5. For earlier releases confirmed to retain the old handler, disclose fixed OrdinalIgnoreCase type matching even with CaseSensitiveClaimsIdentity. Use release/patch-qualified notes after affected and fixed versions are verified. Do not imply that the identity change in ASP.NET Core 8.0 also fixed RequireClaim.

Suggested post-fix wording:

> RequireClaim uses the principal's named claim lookup to select claims of the supplied type. The default ClaimsPrincipal implementation delegates that lookup to each identity. A default-configured ClaimsIdentity uses case-insensitive type matching, while CaseSensitiveClaimsIdentity uses ordinal type matching. RequireClaim compares allowed values ordinally, so admin and Admin remain different values. The supplied type is not an alias for RoleClaimType.

Documenting a legacy exception describes older behavior; it does not itself repair the MVC-advertised identity-dependent guarantee. Resolve the intended contract and implementation together.

## Regression coverage

Add focused handler theories to [ClaimsAuthorizationRequirementTests.cs](https://github.com/dotnet/aspnetcore/blob/ccae41d7332af860d5e48d8caf8e3ffa3227f8b1/src/Security/Authorization/test/ClaimsAuthorizationRequirementTests.cs#L8) and a builder-through-authorization regression using the existing pattern in [DefaultAuthorizationServiceTests.cs](https://github.com/dotnet/aspnetcore/blob/ccae41d7332af860d5e48d8caf8e3ffa3227f8b1/src/Security/Authorization/test/DefaultAuthorizationServiceTests.cs#L36). The inspected requirement tests cover formatting; the inspected service negatives do not exercise case-only differences.

The following are expected results for a policy requiring type department, derived from source rather than an executed test suite. Null/empty includes presence-only RequireClaim, null allowedValues on the requirement, and an empty allowed-values collection. Exercise both builder collection overloads for nonempty values.

| Identity and available claim | Allowed values | Proposed result |
| --- | --- | --- |
| Default-configured base: Department=engineering | Null/empty or engineering | Success, preserving ordinary behavior |
| Case-sensitive: Department=engineering | Null/empty or engineering | Not satisfied; current handler satisfies both branches |
| Either: department=engineering | Null/empty or engineering | Success |
| Either: department=Engineering | engineering | Not satisfied; values remain case-sensitive |
| Either: department=Engineering | Null/empty | Success |
| Either: no department claim | Null/empty or engineering | Not satisfied |
| Either: department with empty value | Collection containing only empty string | Success |
| Either: department=engineering | Collection containing only empty string | Not satisfied |

Also cover multiple matching claims, mixed identities, custom identity and principal named-lookup overrides, Claims-only principal overrides, missing claims, null claimType validation, literal non-pattern types, and unchanged unauthenticated claim-only behavior. Keep mapping constant when comparing old and proposed behavior.

A test identity overriding FindAll(string) with ordinal selection can cover the core extension point without a production dependency. Add actual CaseSensitiveClaimsIdentity coverage through an approved test-only dependency or suitable existing authentication test project. The current authorization test project has no explicit IdentityModel reference; the integration-test placement needs to be selected. Do not equate test-double coverage with token-handler integration coverage.

## Impact and verification limits

This is being filed as a non-exploitable authorization-correctness and documentation issue. The mismatch makes claim-based authorization accept a broader set of type names than the MVC guidance advertises for case-sensitive identities. No exploitable authorization bypass, severity, or CVE is asserted. The recommended fix addresses the documented lookup contract and its compatibility implications.

Source and documentation were inspected. A prior file-free .NET 10.0.11 probe confirmed base ClaimsIdentity comparison behavior only; it did not execute this ASP.NET Core handler or IdentityModel. No product tests, proposed fix, or end-to-end JWT/OIDC workflow were executed. Affected-release and first-fixed-version ranges remain unverified.

## Related discussions

* [#58793: Improve performance of ClaimsIdentity](https://github.com/dotnet/aspnetcore/issues/58793) discusses specialized identities, lookup, and compatibility.
* [#56331: AuthorizationPolicyBuilder.RequireClaim overload that take a Func](https://github.com/dotnet/aspnetcore/issues/56331) is a distinct predicate-overload request, not evidence that the current mismatch was fixed.

No exact duplicate was found in the bounded open/closed issue searches performed on 2026-09-08.

Contributor guide

Open the contributing guide

Research direction

Start with ClaimsAuthorizationRequirement.cs and the RequireClaim overloads in AuthorizationPolicyBuilder.cs, then review ClaimsPrincipal.FindAll and the documented behavior in the two claims articles. Confirm the intended compatibility semantics for custom principals, mixed identities, presence-only claims, and ordinal allowed values. Done means the implementation, API XML, and both documentation articles agree, with affected and fixed versions identified.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
authorization, backend-api-design, documentation
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.