From 182acdd7895accbf32b404366344e65a5981f1ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Aug 2026 04:12:29 +0000 Subject: [PATCH] Backport authenticated principal helper Co-authored-by: Youssef1313 <31348972+Youssef1313@users.noreply.github.com> --- ...datingServerAuthenticationStateProvider.cs | 3 +- ...rosoft.AspNetCore.Components.Server.csproj | 1 + .../Microsoft.AspNetCore.OutputCaching.csproj | 1 + .../src/Policies/DefaultPolicy.cs | 5 +- .../test/OutputCachePolicyProviderTests.cs | 58 +++++++++++++++++++ .../DenyAnonymousAuthorizationRequirement.cs | 8 +-- .../Microsoft.AspNetCore.Authorization.csproj | 1 + src/Shared/SecurityHelper/SecurityHelper.cs | 11 ++++ .../test/Shared.Tests/SecurityHelperTests.cs | 37 ++++++++++++ 9 files changed, 116 insertions(+), 9 deletions(-) diff --git a/src/Components/Server/src/Circuits/RevalidatingServerAuthenticationStateProvider.cs b/src/Components/Server/src/Circuits/RevalidatingServerAuthenticationStateProvider.cs index ee664eaf49eb..d455fb8a1f29 100644 --- a/src/Components/Server/src/Circuits/RevalidatingServerAuthenticationStateProvider.cs +++ b/src/Components/Server/src/Circuits/RevalidatingServerAuthenticationStateProvider.cs @@ -3,6 +3,7 @@ using System.Security.Claims; using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Components.Server; @@ -61,7 +62,7 @@ private async Task RevalidationLoop(Task authenticationStat try { var authenticationState = await authenticationStateTask; - if (authenticationState.User.Identity?.IsAuthenticated == true) + if (SecurityHelper.IsAuthenticated(authenticationState.User)) { while (!cancellationToken.IsCancellationRequested) { diff --git a/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj b/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj index 00079c99d6be..516061c4ac1b 100644 --- a/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj +++ b/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj @@ -30,6 +30,7 @@ + diff --git a/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj b/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj index 55cc936fec7b..18e1b0fe4a05 100644 --- a/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj +++ b/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj @@ -23,6 +23,7 @@ + diff --git a/src/Middleware/OutputCaching/src/Policies/DefaultPolicy.cs b/src/Middleware/OutputCaching/src/Policies/DefaultPolicy.cs index 1f633dbaff83..ecfef9a7052e 100644 --- a/src/Middleware/OutputCaching/src/Policies/DefaultPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/DefaultPolicy.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Internal; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.OutputCaching; @@ -50,7 +51,7 @@ ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, Canc return ValueTask.CompletedTask; } - if (context.HttpContext.User?.Identity?.IsAuthenticated == true) + if (SecurityHelper.IsAuthenticated(context.HttpContext.User)) { context.AllowCacheStorage = false; return ValueTask.CompletedTask; @@ -79,7 +80,7 @@ private static bool AttemptOutputCaching(OutputCacheContext context) } // Verify existence of authorization headers - if (!StringValues.IsNullOrEmpty(request.Headers.Authorization) || request.HttpContext.User?.Identity?.IsAuthenticated == true) + if (!StringValues.IsNullOrEmpty(request.Headers.Authorization) || SecurityHelper.IsAuthenticated(request.HttpContext.User)) { return false; } diff --git a/src/Middleware/OutputCaching/test/OutputCachePolicyProviderTests.cs b/src/Middleware/OutputCaching/test/OutputCachePolicyProviderTests.cs index a91138df1422..f6078d09d4ec 100644 --- a/src/Middleware/OutputCaching/test/OutputCachePolicyProviderTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachePolicyProviderTests.cs @@ -1,6 +1,7 @@ // 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.Http; using Microsoft.Extensions.Logging.Testing; using Microsoft.Net.Http.Headers; @@ -89,6 +90,63 @@ public async Task AttemptResponseCaching_AuthorizationHeaders_NotAllowed() Assert.False(context.AllowCacheLookup); } + [Fact] + public async Task AttemptOutputCaching_AuthenticatedUser_NotAllowed() + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(testSink: sink); + context.HttpContext.Request.Method = HttpMethods.Get; + context.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(authenticationType: "custom")); + + var policy = new OutputCachePolicyBuilder().Build(); + + await policy.CacheRequestAsync(context, default); + + Assert.False(context.AllowCacheStorage); + Assert.False(context.AllowCacheLookup); + } + + [Fact] + public async Task AttemptOutputCaching_AuthenticatedByNonPrimaryIdentity_NotAllowed() + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(testSink: sink); + context.HttpContext.Request.Method = HttpMethods.Get; + context.HttpContext.User = new ClaimsPrincipal(new[] + { + new ClaimsIdentity(), + new ClaimsIdentity(authenticationType: "custom"), + }); + + Assert.False(context.HttpContext.User.Identity?.IsAuthenticated); + + var policy = new OutputCachePolicyBuilder().Build(); + + await policy.CacheRequestAsync(context, default); + + Assert.False(context.AllowCacheStorage); + Assert.False(context.AllowCacheLookup); + } + + [Fact] + public async Task IsResponseCacheable_AuthenticatedByNonPrimaryIdentity_NotAllowed() + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(testSink: sink); + context.HttpContext.User = new ClaimsPrincipal(new[] + { + new ClaimsIdentity(), + new ClaimsIdentity(authenticationType: "custom"), + }); + + Assert.False(context.HttpContext.User.Identity?.IsAuthenticated); + + var policy = new OutputCachePolicyBuilder().Build(); + await policy.ServeResponseAsync(context, default); + + Assert.False(context.AllowCacheStorage); + } + [Fact] public async Task AllowCacheStorage_NoStore_IsAllowed() { diff --git a/src/Security/Authorization/Core/src/DenyAnonymousAuthorizationRequirement.cs b/src/Security/Authorization/Core/src/DenyAnonymousAuthorizationRequirement.cs index 69f5d308175a..35e62e3216c9 100644 --- a/src/Security/Authorization/Core/src/DenyAnonymousAuthorizationRequirement.cs +++ b/src/Security/Authorization/Core/src/DenyAnonymousAuthorizationRequirement.cs @@ -1,8 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Linq; using System.Threading.Tasks; +using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Authorization.Infrastructure; @@ -21,11 +21,7 @@ public class DenyAnonymousAuthorizationRequirement : AuthorizationHandlerThe requirement to evaluate. protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DenyAnonymousAuthorizationRequirement requirement) { - var user = context.User; - var userIsAnonymous = - user?.Identity == null || - !user.Identities.Any(i => i.IsAuthenticated); - if (!userIsAnonymous) + if (SecurityHelper.IsAuthenticated(context.User)) { context.Succeed(requirement); } diff --git a/src/Security/Authorization/Core/src/Microsoft.AspNetCore.Authorization.csproj b/src/Security/Authorization/Core/src/Microsoft.AspNetCore.Authorization.csproj index 44df5654138a..7990232dccaf 100644 --- a/src/Security/Authorization/Core/src/Microsoft.AspNetCore.Authorization.csproj +++ b/src/Security/Authorization/Core/src/Microsoft.AspNetCore.Authorization.csproj @@ -29,6 +29,7 @@ Microsoft.AspNetCore.Authorization.AuthorizeAttribute + diff --git a/src/Shared/SecurityHelper/SecurityHelper.cs b/src/Shared/SecurityHelper/SecurityHelper.cs index 7871714db7af..7d31f4a99371 100644 --- a/src/Shared/SecurityHelper/SecurityHelper.cs +++ b/src/Shared/SecurityHelper/SecurityHelper.cs @@ -43,4 +43,15 @@ public static ClaimsPrincipal MergeUserPrincipal(ClaimsPrincipal? existingPrinci } return newPrincipal; } + + /// + /// Determines whether the is authenticated. + /// Uses the same aggregate semantics as authorization's + /// DenyAnonymousAuthorizationRequirement: the principal is authenticated when it has an + /// identity and at least one of its identities is authenticated (not only the primary identity). + /// + /// The to inspect. + /// if the principal is authenticated; otherwise . + public static bool IsAuthenticated(ClaimsPrincipal? user) + => user?.Identity is not null && user.Identities.Any(static identity => identity.IsAuthenticated); } diff --git a/src/Shared/test/Shared.Tests/SecurityHelperTests.cs b/src/Shared/test/Shared.Tests/SecurityHelperTests.cs index 47a7f6cdaafb..b5324680fc9f 100644 --- a/src/Shared/test/Shared.Tests/SecurityHelperTests.cs +++ b/src/Shared/test/Shared.Tests/SecurityHelperTests.cs @@ -89,4 +89,41 @@ public void AddingPreservesNewIdentitiesAndDropsEmpty() Assert.Equal(identityNoAuthTypeWithClaim, user.Identities.Skip(2).First()); Assert.Equal(identityEmptyWithAuthType, user.Identities.Skip(3).First()); } + + [Fact] + public void IsAuthenticated_NullUser_ReturnsFalse() + { + Assert.False(SecurityHelper.IsAuthenticated(null)); + } + + [Fact] + public void IsAuthenticated_NoIdentities_ReturnsFalse() + { + Assert.False(SecurityHelper.IsAuthenticated(new ClaimsPrincipal())); + } + + [Fact] + public void IsAuthenticated_UnauthenticatedIdentity_ReturnsFalse() + { + Assert.False(SecurityHelper.IsAuthenticated(new ClaimsPrincipal(new ClaimsIdentity()))); + } + + [Fact] + public void IsAuthenticated_AuthenticatedIdentity_ReturnsTrue() + { + Assert.True(SecurityHelper.IsAuthenticated(new ClaimsPrincipal(new ClaimsIdentity(authenticationType: "custom")))); + } + + [Fact] + public void IsAuthenticated_AuthenticatedByNonPrimaryIdentity_ReturnsTrue() + { + var user = new ClaimsPrincipal(new[] + { + new ClaimsIdentity(), + new ClaimsIdentity(authenticationType: "custom"), + }); + + Assert.False(user.Identity.IsAuthenticated); + Assert.True(SecurityHelper.IsAuthenticated(user)); + } }