Conversation
javiercn
requested review from
a team,
Youssef1313,
cincuranet,
davpetr and
rokonec
as code owners
September 15, 2026 11:55
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Critical anonymous/authenticated cache-key collisions and a moderate legacy-key collision remain unresolved.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Unifies authenticated-user identity derivation across Antiforgery, Cache Tag Helpers, and CacheView while preserving Antiforgery compatibility.
Changes:
- Centralizes claim identifier extraction and hashing in
SecurityHelper. - Updates cache keys with identifier markers and shared identity behavior.
- Adds documentation and focused compatibility and cache-key tests.
Unresolved findings:
CacheViewKeyResolver.cs: critical (1 vote)—authenticated principals without identifiers can share keys with anonymous users.CacheTagKey.cs: critical (1 vote)—the same anonymous/authenticated collision exists.CacheTagKey.cs: moderate (1 vote)—legacy distributed-cache keys may collide with the new format.
File summaries
| File | Summary |
|---|---|
src/Shared/test/Shared.Tests/SecurityHelperTests.cs |
Tests shared identifier extraction and hashing. |
src/Shared/SecurityHelper/SecurityHelper.cs |
Provides the shared user-identifier algorithm. |
src/Mvc/Mvc.TagHelpers/test/CacheTagKeyTest.cs |
Tests Cache Tag Helper key behavior. |
src/Mvc/Mvc.TagHelpers/src/Microsoft.AspNetCore.Mvc.TagHelpers.csproj |
Includes shared helper sources. |
src/Mvc/Mvc.TagHelpers/src/CacheTagHelperBase.cs |
Documents user-variation behavior. |
src/Mvc/Mvc.TagHelpers/src/Cache/CacheTagKey.cs |
Generates claim-based cache keys; contains unresolved collision findings. |
src/Components/Endpoints/test/CacheViewKeyResolverTest.cs |
Tests CacheView key resolution. |
src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj |
Includes shared helper sources. |
src/Components/Endpoints/src/CacheView/CacheViewKeyResolver.cs |
Generates CacheView user keys; contains an unresolved collision finding. |
src/Components/Endpoints/src/CacheView/CacheView.cs |
Documents CacheView user variation. |
src/Antiforgery/test/DefaultClaimUidExtractorTest.cs |
Verifies Antiforgery compatibility. |
src/Antiforgery/src/Microsoft.AspNetCore.Antiforgery.csproj |
Includes shared helper sources. |
src/Antiforgery/src/Internal/DefaultClaimUidExtractor.cs |
Delegates identifier extraction to shared logic. |
Review details
Suppressed comments (1)
src/Mvc/Mvc.TagHelpers/src/Cache/CacheTagKey.cs:156
- This changes the user-varying suffix without changing the legacy
VaryByUsertoken, so old distributed-cache entries are not guaranteed to become misses. For example, an old entry for an authenticated user whoseIdentity.NameisNoIdentifierhas exactly the new anonymous key, allowing personalized content to be reused for a no-identifier request; an old name ofIdentifier||<digest>can similarly collide with the new identifier form. Version or otherwise namespace the user-varying key format (and update both helper variants' expectations) so no legacy key can match.
.Append(VaryByUserName)
.Append(CacheKeyTokenSeparator)
.Append(_userIdentifier is null ? NoUserIdentifierName : UserIdentifierName);
- Files reviewed: 13/13 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
This branch centralizes the existing Antiforgery claim-UID algorithm as an internal shared-source
ClaimsPrincipalidentifier, then adopts it in Antiforgery, MVC Cache Tag Helper (including distributed caching), and ComponentsCacheView. The hard constraint is byte-for-byte Antiforgery compatibility: identity/claim traversal, UTF-8 framing, sorting, and SHA-256 output remain unchanged so deployed tokens stay valid. No public API signatures change; Output Caching and Response Caching behavior remain unchanged.The complete change comprises ten commits (
5002ac85f14ethrough423f6a47ed) and 16 files. CacheView user-varied keys intentionally turn over. Cache Tag Helper now unconditionally versions its internal key format, so all existing memory/distributed entries become misses and expire normally rather than risking overlap with the former unescapedIdentity.Namesuffix.Design
The shared primitive is internal source, compiled into each consumer assembly:
The identifier contract, documented on the existing Cache Tag Helper and CacheView
VaryByUserproperties, is:sub,ClaimTypes.NameIdentifier, thenClaimTypes.Upn; hash the selected claim's ordinal(type, value, issuer)tuple.(type, value, issuer)tuple. Equal-type fallback order intentionally retains the previous runtime sort behavior.falsefor a null principal or when authenticated identities contribute no claims.Authentication type and
Identity.Nameare not part of the shared identifier. Cache consumers separately domain claimless authenticated principals from anonymous principals because content can depend onIsAuthenticated. Antiforgery's laterIdentity.Namefallback remains Antiforgery-specific.Cache Tag Helper keeps the existing
VaryByUserproperty andvary-by-userattribute. Versioning is an unconditional private cache-key token, not a new parameter: keys begin<CacheTagHelper-or-DistributedCacheTagHelper>||v1||<key>, and user variance still usesVaryByUser. The publicCacheView,CacheTagHelper, andDistributedCacheTagHelpertype documentation now states consistently that generated keys are framework implementation details, may change between ASP.NET Core product versions, and can cause older entries to become misses and expire normally; it does not expose a stable key format or promise migration.Output Caching continues to reject authenticated/Authorization requests under its default policy, with custom
VaryByValueas explicit opt-in; Response Caching continues to follow HTTP shared-cache rules. Neither subsystem gains principal-based variance or a new API.Implementation
The common path scans standard list-backed principals and identities as spans and returns as soon as it finds the first recognized claim. Only unusual identity enumerables, the all-claims fallback, or serialized inputs over 256 bytes use pooled buffers:
Exact
ClaimsIdentityinstances use the direct scan; derived identities preserve virtualFindFirst(Predicate<Claim>)dispatch because changing an override's result could invalidate existing Antiforgery tokens. Hash input preservesBinaryWriter.Write(string)framing: UTF-8 bytes preceded by a 7-bit encoded byte length. For example, byte length 200 is encoded as0xC8, 0x01before the payload:Antiforgery retains its service/interface boundary and delegates only extraction:
CacheView appends one of three disjoint domains inside its enclosing SHA-256 key:
MVC Cache Tag Helper uses the same three states in both equality and generated text. Its format version is unconditional and the existing user token is unchanged:
The version token prevents a legacy entry for the same logical key—
...||VaryByUser||<unescaped Identity.Name>—from matching the new format. Without a versioned domain, an old nameNoIdentifierexactly matched the initial new anonymous suffix, andIdentifier||<digest>could match an identified suffix. The prerequisite is narrow (a surviving distributed entry and matching old name), but the consequence is cross-user cached-content reuse.TagHelpers' shared helper copy uses
Microsoft.AspNetCore.Mvc.TagHelpers.Internal, selected byMVC_TAGHELPERS, avoiding a collision with referenced MVC assemblies without suppressingCS0436. Antiforgery and Components Endpoints compile it underMicrosoft.Extensions.Internal.Outcome
subvalues produced equal keys under the old NameIdentifier-first algorithm.subprecedence passes, with issuer, fallback, AuthenticationType, and marker coverage.409406c6c7, targeted TagHelpers failed 3/3 and Components failed 1/1 because anonymous and authenticated-without-claims keys were equal.NoIdentifierand `Identifierdotnet test .\src\Shared\test\Shared.Tests\Microsoft.AspNetCore.Shared.Tests.csproj --filter "FullyQualifiedName~SecurityHelperTests" --no-restore -v:q -p:UseIisNativeAssets=falsedotnet test .\src\Antiforgery\test\Microsoft.AspNetCore.Antiforgery.Test.csproj --no-restore -v:q -p:UseIisNativeAssets=falsedotnet test .\src\Mvc\Mvc.TagHelpers\test\Microsoft.AspNetCore.Mvc.TagHelpers.Test.csproj --no-restore -v:q -p:UseIisNativeAssets=falsedotnet test .\src\Components\Endpoints\test\Microsoft.AspNetCore.Components.Endpoints.Tests.csproj --no-restore -v:q -p:UseIisNativeAssets=falseUseIisNativeAssets=falsegit diff --check, public API inspectionAntiforgery compatibility is pinned at both layers, including known digest
yhXE+2v4zSXHtRHmzm4cmrhZca2J0g7yTUwtUerdeF4=and long multibyte UTF-8/multi-byte-length digestn68b/ma1cEcRL2AyEiy3YE4zE+qGpgMJda+FFN+oqAk=. Extractor-level tests retain the original scenarios with fixed digest outputs.The subject result was noisy and shows a nominal 3% time regression while removing 120 B; other measured scenarios improved and all standard warmed paths reached 0 B/op. These are extraction microbenchmarks, not end-to-end request measurements. Custom enumerable/derived identity implementations can allocate internally, and an
ArrayPoolmiss can allocate.