diff --git a/src/Components/Authorization/src/AuthenticationStateData.cs b/src/Components/Authorization/src/AuthenticationStateData.cs index da4b5da8085d..bc83beac830c 100644 --- a/src/Components/Authorization/src/AuthenticationStateData.cs +++ b/src/Components/Authorization/src/AuthenticationStateData.cs @@ -13,7 +13,7 @@ public class AuthenticationStateData /// /// The client-readable claims that describe the . /// - public IList> Claims { get; set; } = []; + public IList Claims { get; set; } = []; /// /// Gets the value that identifies 'Name' claims. This is used when returning the property . diff --git a/src/Components/Authorization/src/ClaimData.cs b/src/Components/Authorization/src/ClaimData.cs new file mode 100644 index 000000000000..b0ae8c4e2638 --- /dev/null +++ b/src/Components/Authorization/src/ClaimData.cs @@ -0,0 +1,45 @@ +// 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 System.Text.Json.Serialization; + +namespace Microsoft.AspNetCore.Components.Authorization; + +/// +/// This is a serializable representation of a object that only consists of the type and value. +/// +public readonly struct ClaimData +{ + /// + /// Constructs a new instance of from a type and value. + /// + /// The claim type. + /// The claim value + [JsonConstructor] + public ClaimData(string type, string value) + { + Type = type; + Value = value; + } + + /// + /// Constructs a new instance of from a copying only the + /// and into their corresponding properties. + /// + /// The to copy from. + public ClaimData(Claim claim) + : this(claim.Type, claim.Value) + { + } + + /// + /// Gets the claim type of the claim. . + /// + public string Type { get; } + + /// + /// Gets the value of the claim. + /// + public string Value { get; } +} diff --git a/src/Components/Authorization/src/PublicAPI.Unshipped.txt b/src/Components/Authorization/src/PublicAPI.Unshipped.txt index 74b5773a99d5..e02769f17caf 100644 --- a/src/Components/Authorization/src/PublicAPI.Unshipped.txt +++ b/src/Components/Authorization/src/PublicAPI.Unshipped.txt @@ -1,9 +1,15 @@ #nullable enable Microsoft.AspNetCore.Components.Authorization.AuthenticationStateData Microsoft.AspNetCore.Components.Authorization.AuthenticationStateData.AuthenticationStateData() -> void -Microsoft.AspNetCore.Components.Authorization.AuthenticationStateData.Claims.get -> System.Collections.Generic.IList>! +Microsoft.AspNetCore.Components.Authorization.AuthenticationStateData.Claims.get -> System.Collections.Generic.IList! Microsoft.AspNetCore.Components.Authorization.AuthenticationStateData.Claims.set -> void Microsoft.AspNetCore.Components.Authorization.AuthenticationStateData.NameClaimType.get -> string! Microsoft.AspNetCore.Components.Authorization.AuthenticationStateData.NameClaimType.set -> void Microsoft.AspNetCore.Components.Authorization.AuthenticationStateData.RoleClaimType.get -> string! Microsoft.AspNetCore.Components.Authorization.AuthenticationStateData.RoleClaimType.set -> void +Microsoft.AspNetCore.Components.Authorization.ClaimData +Microsoft.AspNetCore.Components.Authorization.ClaimData.ClaimData() -> void +Microsoft.AspNetCore.Components.Authorization.ClaimData.ClaimData(string! type, string! value) -> void +Microsoft.AspNetCore.Components.Authorization.ClaimData.ClaimData(System.Security.Claims.Claim! claim) -> void +Microsoft.AspNetCore.Components.Authorization.ClaimData.Type.get -> string! +Microsoft.AspNetCore.Components.Authorization.ClaimData.Value.get -> string! diff --git a/src/Components/WebAssembly/Server/src/AuthenticationStateSerializationOptions.cs b/src/Components/WebAssembly/Server/src/AuthenticationStateSerializationOptions.cs index d7c3dc752a47..077bf134822a 100644 --- a/src/Components/WebAssembly/Server/src/AuthenticationStateSerializationOptions.cs +++ b/src/Components/WebAssembly/Server/src/AuthenticationStateSerializationOptions.cs @@ -51,19 +51,19 @@ public AuthenticationStateSerializationOptions() { foreach (var claim in authenticationState.User.Claims) { - data.Claims.Add(new(claim.Type, claim.Value)); + data.Claims.Add(new(claim)); } } else { if (authenticationState.User.FindFirst(data.NameClaimType) is { } nameClaim) { - data.Claims.Add(new(nameClaim.Type, nameClaim.Value)); + data.Claims.Add(new(nameClaim)); } foreach (var roleClaim in authenticationState.User.FindAll(data.RoleClaimType)) { - data.Claims.Add(new(roleClaim.Type, roleClaim.Value)); + data.Claims.Add(new(roleClaim)); } } } diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/src/Options/AuthenticationStateDeserializationOptions.cs b/src/Components/WebAssembly/WebAssembly.Authentication/src/Options/AuthenticationStateDeserializationOptions.cs index 58bf7e903f50..d2e4207eee19 100644 --- a/src/Components/WebAssembly/WebAssembly.Authentication/src/Options/AuthenticationStateDeserializationOptions.cs +++ b/src/Components/WebAssembly/WebAssembly.Authentication/src/Options/AuthenticationStateDeserializationOptions.cs @@ -31,7 +31,7 @@ private static Task DeserializeAuthenticationStateAsync(Aut return Task.FromResult( new AuthenticationState(new ClaimsPrincipal( - new ClaimsIdentity(authenticationStateData.Claims.Select(c => new Claim(c.Key, c.Value)), + new ClaimsIdentity(authenticationStateData.Claims.Select(c => new Claim(c.Type, c.Value)), authenticationType: nameof(DeserializedAuthenticationStateProvider), nameType: authenticationStateData.NameClaimType, roleType: authenticationStateData.RoleClaimType)))); diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/DeserializedAuthenticationStateProvider.cs b/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/DeserializedAuthenticationStateProvider.cs index 065fffdb3951..bff25cd51fbe 100644 --- a/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/DeserializedAuthenticationStateProvider.cs +++ b/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/DeserializedAuthenticationStateProvider.cs @@ -5,6 +5,7 @@ using System.Security.Claims; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.Extensions.Options; +using static Microsoft.AspNetCore.Internal.LinkerFlags; namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication; @@ -21,7 +22,10 @@ internal sealed class DeserializedAuthenticationStateProvider : AuthenticationSt [UnconditionalSuppressMessage( "Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", - Justification = $"{nameof(DeserializedAuthenticationStateProvider)} uses the {nameof(PersistentComponentState)} APIs to deserialize the token, which are already annotated.")] + Justification = $"{nameof(DeserializedAuthenticationStateProvider)} uses the {nameof(DynamicDependencyAttribute)} to preserve the necessary members.")] + [DynamicDependency(JsonSerialized, typeof(AuthenticationStateData))] + [DynamicDependency(JsonSerialized, typeof(IList))] + [DynamicDependency(JsonSerialized, typeof(ClaimData))] public DeserializedAuthenticationStateProvider(PersistentComponentState state, IOptions options) { if (!state.TryTakeFromJson(PersistenceKey, out var authenticationStateData) || authenticationStateData is null) diff --git a/src/Components/test/E2ETest/Infrastructure/ServerFixtures/BasicTestAppServerSiteFixture.cs b/src/Components/test/E2ETest/Infrastructure/ServerFixtures/BasicTestAppServerSiteFixture.cs index e828bf69dfe6..20bb845423b8 100644 --- a/src/Components/test/E2ETest/Infrastructure/ServerFixtures/BasicTestAppServerSiteFixture.cs +++ b/src/Components/test/E2ETest/Infrastructure/ServerFixtures/BasicTestAppServerSiteFixture.cs @@ -7,6 +7,7 @@ public class BasicTestAppServerSiteFixture : AspNetSiteServerFixture w { public BasicTestAppServerSiteFixture() { + ApplicationAssembly = typeof(TStartup).Assembly; BuildWebHostMethod = TestServer.Program.BuildWebHost; } } diff --git a/src/Components/test/E2ETest/Infrastructure/ServerFixtures/TrimmingServerFixture.cs b/src/Components/test/E2ETest/Infrastructure/ServerFixtures/TrimmingServerFixture.cs new file mode 100644 index 000000000000..a0f0698cf450 --- /dev/null +++ b/src/Components/test/E2ETest/Infrastructure/ServerFixtures/TrimmingServerFixture.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; + +namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; + +public class TrimmingServerFixture : BasicTestAppServerSiteFixture where TStartup : class +{ + public readonly bool TestTrimmedApps = typeof(ToggleExecutionModeServerFixture<>).Assembly + .GetCustomAttributes() + .First(m => m.Key == "Microsoft.AspNetCore.E2ETesting.TestTrimmedOrMultithreadingApps") + .Value == "true"; + + public TrimmingServerFixture() + { + if (TestTrimmedApps) + { + BuildWebHostMethod = BuildPublishedWebHost; + GetContentRootMethod = GetPublishedContentRoot; + } + } + + private static IHost BuildPublishedWebHost(string[] args) => + Extensions.Hosting.Host.CreateDefaultBuilder(args) + .ConfigureLogging((ctx, lb) => + { + var sink = new TestSink(); + lb.AddProvider(new TestLoggerProvider(sink)); + lb.Services.AddSingleton(sink); + }) + .ConfigureWebHostDefaults(webHostBuilder => + { + webHostBuilder.UseStartup(); + // Avoid UseStaticAssets or we won't use the trimmed published output. + }) + .Build(); + + private static string GetPublishedContentRoot(Assembly assembly) + { + var contentRoot = Path.Combine(AppContext.BaseDirectory, "trimmed-or-threading", assembly.GetName().Name); + + if (!Directory.Exists(contentRoot)) + { + throw new DirectoryNotFoundException($"Test is configured to use trimmed outputs, but trimmed outputs were not found in {contentRoot}."); + } + + return contentRoot; + } +} diff --git a/src/Components/test/E2ETest/ServerRenderingTests/AuthTests/DefaultAuthenticationStateSerializationOptionsTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/AuthTests/DefaultAuthenticationStateSerializationOptionsTest.cs index 273f48f82b12..ebf67eb8ccf7 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/AuthTests/DefaultAuthenticationStateSerializationOptionsTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/AuthTests/DefaultAuthenticationStateSerializationOptionsTest.cs @@ -12,11 +12,11 @@ namespace Microsoft.AspNetCore.Components.E2ETests.ServerRenderingTests.AuthTests; public class DefaultAuthenticationStateSerializationOptionsTest - : ServerTestBase>> + : ServerTestBase>> { public DefaultAuthenticationStateSerializationOptionsTest( BrowserFixture browserFixture, - BasicTestAppServerSiteFixture> serverFixture, + TrimmingServerFixture> serverFixture, ITestOutputHelper output) : base(browserFixture, serverFixture, output) { diff --git a/src/Components/test/E2ETest/ServerRenderingTests/AuthTests/ServerRenderedAuthenticationStateTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/AuthTests/ServerRenderedAuthenticationStateTest.cs index 931b2368c241..29fd3c4a9d2a 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/AuthTests/ServerRenderedAuthenticationStateTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/AuthTests/ServerRenderedAuthenticationStateTest.cs @@ -12,11 +12,11 @@ namespace Microsoft.AspNetCore.Components.E2ETests.ServerRenderingTests.AuthTests; public class ServerRenderedAuthenticationStateTest - : ServerTestBase>> + : ServerTestBase>> { public ServerRenderedAuthenticationStateTest( BrowserFixture browserFixture, - BasicTestAppServerSiteFixture> serverFixture, + TrimmingServerFixture> serverFixture, ITestOutputHelper output) : base(browserFixture, serverFixture, output) { diff --git a/src/Components/test/E2ETest/Tests/RemoteAuthenticationTest.cs b/src/Components/test/E2ETest/Tests/RemoteAuthenticationTest.cs index 2ab1622379b8..95227c75759d 100644 --- a/src/Components/test/E2ETest/Tests/RemoteAuthenticationTest.cs +++ b/src/Components/test/E2ETest/Tests/RemoteAuthenticationTest.cs @@ -1,15 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Reflection; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; using Microsoft.AspNetCore.E2ETesting; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Testing; using OpenQA.Selenium; using TestServer; using Xunit.Abstractions; @@ -17,26 +11,14 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests; public class RemoteAuthenticationTest : - ServerTestBase> + ServerTestBase> { - public readonly bool TestTrimmedApps = typeof(ToggleExecutionModeServerFixture<>).Assembly - .GetCustomAttributes() - .First(m => m.Key == "Microsoft.AspNetCore.E2ETesting.TestTrimmedOrMultithreadingApps") - .Value == "true"; - public RemoteAuthenticationTest( BrowserFixture browserFixture, - BasicTestAppServerSiteFixture serverFixture, + TrimmingServerFixture serverFixture, ITestOutputHelper output) : base(browserFixture, serverFixture, output) { - serverFixture.ApplicationAssembly = typeof(RemoteAuthenticationStartup).Assembly; - - if (TestTrimmedApps) - { - serverFixture.BuildWebHostMethod = BuildPublishedWebHost; - serverFixture.GetContentRootMethod = GetPublishedContentRoot; - } } [Fact] @@ -49,31 +31,4 @@ public void NavigateToLogin_PreservesExtraQueryParams() var heading = Browser.Exists(By.TagName("h1")); Browser.Equal("Hello, Jane Doe!", () => heading.Text); } - - private static IHost BuildPublishedWebHost(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureLogging((ctx, lb) => - { - var sink = new TestSink(); - lb.AddProvider(new TestLoggerProvider(sink)); - lb.Services.AddSingleton(sink); - }) - .ConfigureWebHostDefaults(webHostBuilder => - { - webHostBuilder.UseStartup(); - // Avoid UseStaticAssets or we won't use the trimmed published output. - }) - .Build(); - - private static string GetPublishedContentRoot(Assembly assembly) - { - var contentRoot = Path.Combine(AppContext.BaseDirectory, "trimmed-or-threading", assembly.GetName().Name); - - if (!Directory.Exists(contentRoot)) - { - throw new DirectoryNotFoundException($"Test is configured to use trimmed outputs, but trimmed outputs were not found in {contentRoot}."); - } - - return contentRoot; - } } diff --git a/src/Components/test/E2ETest/Tests/WebAssemblyPrerenderedTest.cs b/src/Components/test/E2ETest/Tests/WebAssemblyPrerenderedTest.cs index e0c0dd0c8a74..18869083cf0f 100644 --- a/src/Components/test/E2ETest/Tests/WebAssemblyPrerenderedTest.cs +++ b/src/Components/test/E2ETest/Tests/WebAssemblyPrerenderedTest.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Reflection; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; using Microsoft.AspNetCore.E2ETesting; @@ -10,26 +9,15 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests; -public class WebAssemblyPrerenderedTest : ServerTestBase +public class WebAssemblyPrerenderedTest : ServerTestBase> { public WebAssemblyPrerenderedTest( BrowserFixture browserFixture, - AspNetSiteServerFixture serverFixture, + TrimmingServerFixture serverFixture, ITestOutputHelper output) : base(browserFixture, serverFixture, output) { - serverFixture.BuildWebHostMethod = Wasm.Prerendered.Server.Program.BuildWebHost; serverFixture.Environment = AspNetEnvironment.Development; - - var testTrimmedApps = typeof(ToggleExecutionModeServerFixture<>).Assembly - .GetCustomAttributes() - .First(m => m.Key == "Microsoft.AspNetCore.E2ETesting.TestTrimmedOrMultithreadingApps") - .Value == "true"; - - if (testTrimmedApps) - { - serverFixture.GetContentRootMethod = GetPublishedContentRoot; - } } [Fact] @@ -53,16 +41,4 @@ private void WaitUntilLoaded() var jsExecutor = (IJavaScriptExecutor)Browser; Browser.True(() => jsExecutor.ExecuteScript("return window['__aspnetcore__testing__blazor_wasm__started__'];") is not null); } - - private static string GetPublishedContentRoot(Assembly assembly) - { - var contentRoot = Path.Combine(AppContext.BaseDirectory, "trimmed-or-threading", assembly.GetName().Name); - - if (!Directory.Exists(contentRoot)) - { - throw new DirectoryNotFoundException($"Test is configured to use trimmed outputs, but trimmed outputs were not found in {contentRoot}."); - } - - return contentRoot; - } } diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs index 7a9463136a36..943ee32d635c 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs @@ -86,7 +86,16 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) _ = app.UseEndpoints(endpoints => { - endpoints.MapStaticAssets(); + var contentRootStaticAssetsPath = Path.Combine(env.ContentRootPath, "Components.TestServer.staticwebassets.endpoints.json"); + if (File.Exists(contentRootStaticAssetsPath)) + { + endpoints.MapStaticAssets(contentRootStaticAssetsPath); + } + else + { + endpoints.MapStaticAssets(); + } + _ = endpoints.MapRazorComponents() .AddAdditionalAssemblies(Assembly.Load("Components.WasmMinimal")) .AddInteractiveServerRenderMode(options => diff --git a/src/Components/test/testassets/Components.TestServer/RemoteAuthenticationStartup.cs b/src/Components/test/testassets/Components.TestServer/RemoteAuthenticationStartup.cs index 9c9641242521..e0ea4303ef8e 100644 --- a/src/Components/test/testassets/Components.TestServer/RemoteAuthenticationStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RemoteAuthenticationStartup.cs @@ -28,11 +28,16 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseAntiforgery(); app.UseEndpoints(endpoints => { -#if !DEBUG - endpoints.MapStaticAssets(Path.Combine("trimmed-or-threading", "Components.TestServer", "Components.TestServer.staticwebassets.endpoints.json")); -#else - endpoints.MapStaticAssets("Components.TestServer.staticwebassets.endpoints.json"); -#endif + var contentRootStaticAssetsPath = Path.Combine(env.ContentRootPath, "Components.TestServer.staticwebassets.endpoints.json"); + if (File.Exists(contentRootStaticAssetsPath)) + { + endpoints.MapStaticAssets(contentRootStaticAssetsPath); + } + else + { + endpoints.MapStaticAssets(); + } + endpoints.MapRazorComponents() .AddAdditionalAssemblies(Assembly.Load("Components.WasmRemoteAuthentication")) .AddInteractiveWebAssemblyRenderMode(options => options.PathPrefix = "/WasmRemoteAuthentication");