Improve performance of ClaimsIdentity
- Dominant language
- C#
- Stars
- 38.4k
- Forks
- 10.9k
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 290
Description
## Background and Motivation
.NET 4.5 introduced [`ClaimsIdentity`](https://learn.microsoft.com/en-us/dotnet/api/system.security.claims.claimsidentity?view=net-8.0), [`ClaimsPrincipal`](https://learn.microsoft.com/en-us/dotnet/api/system.security.claims.claimsprincipal?view=net-8.0), and [`Claim`](https://learn.microsoft.com/en-us/dotnet/api/system.security.claims.claim?view=net-8.0) as a standard way to represent the claims in a [`SecurityToken`](https://learn.microsoft.com/en-us/dotnet/api/system.identitymodel.tokens.securitytoken?view=net-8.0). Since then there are performance and consistency improvements that can be made.
ASP.NET Core uses Microsoft.IdentityModel.* 7.x packages. In this version, when IdentityModel reads an incoming token, it parses it into a [`JsonWebToken`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.identitymodel.jsonwebtokens.jsonwebtoken?view=msal-web-dotnet-latest) instance and the claims are stored in a dictionary. The [`JsonWebToken.Claims`](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/96b6b365dc468b4068d798181b0c514a1aa15f3f/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs#L604) collection is lazily created by [iterating through that dictionary](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/96b6b365dc468b4068d798181b0c514a1aa15f3f/src/Microsoft.IdentityModel.JsonWebTokens/Json/JsonClaimSet.cs#L39) and creating and adding a new `Claim` instance for each claim value. When `ClaimsIdentity` is requested and IdentityModel creates one, [it iterates through a collection of JsonWebToken.Claims](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/96b6b365dc468b4068d798181b0c514a1aa15f3f/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs#L280), creates a new `Claim`, and adds it to an instance of `ClaimsIdentity` to be returned. When `ClaimsIdentity` methods like `HasClaim` and `FindFirst` are called, they operate on the [`IEnumerable`](https://learn.microsoft.com/en-us/dotnet/api/system.security.claims.claimsidentity.claims?view=net-8.0#system-security-claims-claimsidentity-claims) in that `ClaimsIdentity` instance. The full `Claim` collection is created even if only one claim is needed. Methods like `HasClaim` have to iterate through the whole `Claim` collection.
## Proposed API
IdentityModel introduces a new type, `SecurityTokenClaimsIdentity`, which will derive from `ClaimsIdentity`. When ASP.NET uses IdentityModel an instance of this new type will be returned. `SecurityTokenClaimsIdentity` holds a backing instance of `JsonWebToken` (it's of more general type `SecurityToken`, but currently only `JsonWebToken` supported). Operations like `HasClaim` and `FindFirst` will now look in the claims dictionary. `Claim` collection will be generated only when all claims are requested and only once.
[This PR](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/pull/2858) shows initial in-progress proposal. The [initial performance results](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/pull/2858#issuecomment-2410334031) show ~65% reduction in latency and ~40% reduction in allocations for `FindFirst` and `HasClaim`.
This change does mean that the `ClaimsIdentity` is now case-sensitive. IdentityModel 8 already [introduced `CaseSensitiveClaimsIdentity`](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/releases/tag/8.0.0) and uses it by default. The goal is for the behavior of `SecurityTokenClaimsIdentity` operations to match the behavior of `ClaimsIdentity` (except the case-sensitivity).
Once `SecurityTokenClaimsIdentity` is generally available, ASP.NET can upgrade the IdentityModel package and use that type internally. IdentityModel will provide a flag to enable or disable the use of this new type by default.
ASP.NET can also expose a flag, like` UseSecurityTokenClaimsIdentity` in an options class like `JwtBearerOptions`, `OpenIdConnectOptions`.
## Usage Examples
ASP.NET can still use `ClaimsIdentity` as a return type in methods, only the implementation and case sensitive behavior will change as stated above. The users can use the new ASP.NET version without any changes in their code.
## Alternative Designs
ASP.NET can make this new `ClaimsIdentity` behavior opt-in and expose an options flag to enable it.
## Risks
- The new `ClaimsIdentity` type will be case-sensitive, which is a breaking change.
- `JsonWebToken` doesn't support adding and removing claims; `ClaimsIdentity` does. Also for these operations, `ClaimsIdentity` does a comparison by `Claim` reference while `JsonWebToken` compares by claim type and claim value. Work will need to be done to make sure `SecurityTokenClaimsIdentity` behavior is on par with `ClaimsIdentity` in these cases.
Contributor guide
Assessment
This issue has not been assessed yet.