Skip to content

Commit a8f5c7e

Browse files
authored
Add ClaimData for AuthenticationStateData and fix overtrimming (#56878)
1 parent bef4dae commit a8f5c7e

14 files changed

+146
-90
lines changed

src/Components/Authorization/src/AuthenticationStateData.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public class AuthenticationStateData
1313
/// <summary>
1414
/// The client-readable claims that describe the <see cref="AuthenticationState.User"/>.
1515
/// </summary>
16-
public IList<KeyValuePair<string, string>> Claims { get; set; } = [];
16+
public IList<ClaimData> Claims { get; set; } = [];
1717

1818
/// <summary>
1919
/// Gets the value that identifies 'Name' claims. This is used when returning the property <see cref="ClaimsIdentity.Name"/>.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Security.Claims;
5+
using System.Text.Json.Serialization;
6+
7+
namespace Microsoft.AspNetCore.Components.Authorization;
8+
9+
/// <summary>
10+
/// This is a serializable representation of a <see cref="Claim"/> object that only consists of the type and value.
11+
/// </summary>
12+
public readonly struct ClaimData
13+
{
14+
/// <summary>
15+
/// Constructs a new instance of <see cref="ClaimData"/> from a type and value.
16+
/// </summary>
17+
/// <param name="type">The claim type.</param>
18+
/// <param name="value">The claim value</param>
19+
[JsonConstructor]
20+
public ClaimData(string type, string value)
21+
{
22+
Type = type;
23+
Value = value;
24+
}
25+
26+
/// <summary>
27+
/// Constructs a new instance of <see cref="ClaimData"/> from a <see cref="Claim"/> copying only the
28+
/// <see cref="Claim.Type"/> and <see cref="Claim.Value"/> into their corresponding properties.
29+
/// </summary>
30+
/// <param name="claim">The <see cref="Claim"/> to copy from.</param>
31+
public ClaimData(Claim claim)
32+
: this(claim.Type, claim.Value)
33+
{
34+
}
35+
36+
/// <summary>
37+
/// Gets the claim type of the claim. <seealso cref="ClaimTypes"/>.
38+
/// </summary>
39+
public string Type { get; }
40+
41+
/// <summary>
42+
/// Gets the value of the claim.
43+
/// </summary>
44+
public string Value { get; }
45+
}
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
#nullable enable
22
Microsoft.AspNetCore.Components.Authorization.AuthenticationStateData
33
Microsoft.AspNetCore.Components.Authorization.AuthenticationStateData.AuthenticationStateData() -> void
4-
Microsoft.AspNetCore.Components.Authorization.AuthenticationStateData.Claims.get -> System.Collections.Generic.IList<System.Collections.Generic.KeyValuePair<string!, string!>>!
4+
Microsoft.AspNetCore.Components.Authorization.AuthenticationStateData.Claims.get -> System.Collections.Generic.IList<Microsoft.AspNetCore.Components.Authorization.ClaimData>!
55
Microsoft.AspNetCore.Components.Authorization.AuthenticationStateData.Claims.set -> void
66
Microsoft.AspNetCore.Components.Authorization.AuthenticationStateData.NameClaimType.get -> string!
77
Microsoft.AspNetCore.Components.Authorization.AuthenticationStateData.NameClaimType.set -> void
88
Microsoft.AspNetCore.Components.Authorization.AuthenticationStateData.RoleClaimType.get -> string!
99
Microsoft.AspNetCore.Components.Authorization.AuthenticationStateData.RoleClaimType.set -> void
10+
Microsoft.AspNetCore.Components.Authorization.ClaimData
11+
Microsoft.AspNetCore.Components.Authorization.ClaimData.ClaimData() -> void
12+
Microsoft.AspNetCore.Components.Authorization.ClaimData.ClaimData(string! type, string! value) -> void
13+
Microsoft.AspNetCore.Components.Authorization.ClaimData.ClaimData(System.Security.Claims.Claim! claim) -> void
14+
Microsoft.AspNetCore.Components.Authorization.ClaimData.Type.get -> string!
15+
Microsoft.AspNetCore.Components.Authorization.ClaimData.Value.get -> string!

src/Components/WebAssembly/Server/src/AuthenticationStateSerializationOptions.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,19 +51,19 @@ public AuthenticationStateSerializationOptions()
5151
{
5252
foreach (var claim in authenticationState.User.Claims)
5353
{
54-
data.Claims.Add(new(claim.Type, claim.Value));
54+
data.Claims.Add(new(claim));
5555
}
5656
}
5757
else
5858
{
5959
if (authenticationState.User.FindFirst(data.NameClaimType) is { } nameClaim)
6060
{
61-
data.Claims.Add(new(nameClaim.Type, nameClaim.Value));
61+
data.Claims.Add(new(nameClaim));
6262
}
6363

6464
foreach (var roleClaim in authenticationState.User.FindAll(data.RoleClaimType))
6565
{
66-
data.Claims.Add(new(roleClaim.Type, roleClaim.Value));
66+
data.Claims.Add(new(roleClaim));
6767
}
6868
}
6969
}

src/Components/WebAssembly/WebAssembly.Authentication/src/Options/AuthenticationStateDeserializationOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ private static Task<AuthenticationState> DeserializeAuthenticationStateAsync(Aut
3131

3232
return Task.FromResult(
3333
new AuthenticationState(new ClaimsPrincipal(
34-
new ClaimsIdentity(authenticationStateData.Claims.Select(c => new Claim(c.Key, c.Value)),
34+
new ClaimsIdentity(authenticationStateData.Claims.Select(c => new Claim(c.Type, c.Value)),
3535
authenticationType: nameof(DeserializedAuthenticationStateProvider),
3636
nameType: authenticationStateData.NameClaimType,
3737
roleType: authenticationStateData.RoleClaimType))));

src/Components/WebAssembly/WebAssembly.Authentication/src/Services/DeserializedAuthenticationStateProvider.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Security.Claims;
66
using Microsoft.AspNetCore.Components.Authorization;
77
using Microsoft.Extensions.Options;
8+
using static Microsoft.AspNetCore.Internal.LinkerFlags;
89

910
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication;
1011

@@ -21,7 +22,10 @@ internal sealed class DeserializedAuthenticationStateProvider : AuthenticationSt
2122
[UnconditionalSuppressMessage(
2223
"Trimming",
2324
"IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
24-
Justification = $"{nameof(DeserializedAuthenticationStateProvider)} uses the {nameof(PersistentComponentState)} APIs to deserialize the token, which are already annotated.")]
25+
Justification = $"{nameof(DeserializedAuthenticationStateProvider)} uses the {nameof(DynamicDependencyAttribute)} to preserve the necessary members.")]
26+
[DynamicDependency(JsonSerialized, typeof(AuthenticationStateData))]
27+
[DynamicDependency(JsonSerialized, typeof(IList<ClaimData>))]
28+
[DynamicDependency(JsonSerialized, typeof(ClaimData))]
2529
public DeserializedAuthenticationStateProvider(PersistentComponentState state, IOptions<AuthenticationStateDeserializationOptions> options)
2630
{
2731
if (!state.TryTakeFromJson<AuthenticationStateData?>(PersistenceKey, out var authenticationStateData) || authenticationStateData is null)

src/Components/test/E2ETest/Infrastructure/ServerFixtures/BasicTestAppServerSiteFixture.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ public class BasicTestAppServerSiteFixture<TStartup> : AspNetSiteServerFixture w
77
{
88
public BasicTestAppServerSiteFixture()
99
{
10+
ApplicationAssembly = typeof(TStartup).Assembly;
1011
BuildWebHostMethod = TestServer.Program.BuildWebHost<TStartup>;
1112
}
1213
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Reflection;
5+
using Microsoft.AspNetCore.Hosting;
6+
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.Extensions.Hosting;
8+
using Microsoft.Extensions.Logging;
9+
using Microsoft.Extensions.Logging.Testing;
10+
11+
namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
12+
13+
public class TrimmingServerFixture<TStartup> : BasicTestAppServerSiteFixture<TStartup> where TStartup : class
14+
{
15+
public readonly bool TestTrimmedApps = typeof(ToggleExecutionModeServerFixture<>).Assembly
16+
.GetCustomAttributes<AssemblyMetadataAttribute>()
17+
.First(m => m.Key == "Microsoft.AspNetCore.E2ETesting.TestTrimmedOrMultithreadingApps")
18+
.Value == "true";
19+
20+
public TrimmingServerFixture()
21+
{
22+
if (TestTrimmedApps)
23+
{
24+
BuildWebHostMethod = BuildPublishedWebHost;
25+
GetContentRootMethod = GetPublishedContentRoot;
26+
}
27+
}
28+
29+
private static IHost BuildPublishedWebHost(string[] args) =>
30+
Extensions.Hosting.Host.CreateDefaultBuilder(args)
31+
.ConfigureLogging((ctx, lb) =>
32+
{
33+
var sink = new TestSink();
34+
lb.AddProvider(new TestLoggerProvider(sink));
35+
lb.Services.AddSingleton(sink);
36+
})
37+
.ConfigureWebHostDefaults(webHostBuilder =>
38+
{
39+
webHostBuilder.UseStartup<TStartup>();
40+
// Avoid UseStaticAssets or we won't use the trimmed published output.
41+
})
42+
.Build();
43+
44+
private static string GetPublishedContentRoot(Assembly assembly)
45+
{
46+
var contentRoot = Path.Combine(AppContext.BaseDirectory, "trimmed-or-threading", assembly.GetName().Name);
47+
48+
if (!Directory.Exists(contentRoot))
49+
{
50+
throw new DirectoryNotFoundException($"Test is configured to use trimmed outputs, but trimmed outputs were not found in {contentRoot}.");
51+
}
52+
53+
return contentRoot;
54+
}
55+
}

src/Components/test/E2ETest/ServerRenderingTests/AuthTests/DefaultAuthenticationStateSerializationOptionsTest.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
namespace Microsoft.AspNetCore.Components.E2ETests.ServerRenderingTests.AuthTests;
1313

1414
public class DefaultAuthenticationStateSerializationOptionsTest
15-
: ServerTestBase<BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<App>>>
15+
: ServerTestBase<TrimmingServerFixture<RazorComponentEndpointsStartup<App>>>
1616
{
1717
public DefaultAuthenticationStateSerializationOptionsTest(
1818
BrowserFixture browserFixture,
19-
BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<App>> serverFixture,
19+
TrimmingServerFixture<RazorComponentEndpointsStartup<App>> serverFixture,
2020
ITestOutputHelper output)
2121
: base(browserFixture, serverFixture, output)
2222
{

src/Components/test/E2ETest/ServerRenderingTests/AuthTests/ServerRenderedAuthenticationStateTest.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
namespace Microsoft.AspNetCore.Components.E2ETests.ServerRenderingTests.AuthTests;
1313

1414
public class ServerRenderedAuthenticationStateTest
15-
: ServerTestBase<BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<App>>>
15+
: ServerTestBase<TrimmingServerFixture<RazorComponentEndpointsStartup<App>>>
1616
{
1717
public ServerRenderedAuthenticationStateTest(
1818
BrowserFixture browserFixture,
19-
BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<App>> serverFixture,
19+
TrimmingServerFixture<RazorComponentEndpointsStartup<App>> serverFixture,
2020
ITestOutputHelper output)
2121
: base(browserFixture, serverFixture, output)
2222
{
Lines changed: 2 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,24 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Reflection;
54
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
65
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
76
using Microsoft.AspNetCore.E2ETesting;
8-
using Microsoft.AspNetCore.Hosting;
9-
using Microsoft.Extensions.DependencyInjection;
10-
using Microsoft.Extensions.Hosting;
11-
using Microsoft.Extensions.Logging;
12-
using Microsoft.Extensions.Logging.Testing;
137
using OpenQA.Selenium;
148
using TestServer;
159
using Xunit.Abstractions;
1610

1711
namespace Microsoft.AspNetCore.Components.E2ETest.Tests;
1812

1913
public class RemoteAuthenticationTest :
20-
ServerTestBase<BasicTestAppServerSiteFixture<RemoteAuthenticationStartup>>
14+
ServerTestBase<TrimmingServerFixture<RemoteAuthenticationStartup>>
2115
{
22-
public readonly bool TestTrimmedApps = typeof(ToggleExecutionModeServerFixture<>).Assembly
23-
.GetCustomAttributes<AssemblyMetadataAttribute>()
24-
.First(m => m.Key == "Microsoft.AspNetCore.E2ETesting.TestTrimmedOrMultithreadingApps")
25-
.Value == "true";
26-
2716
public RemoteAuthenticationTest(
2817
BrowserFixture browserFixture,
29-
BasicTestAppServerSiteFixture<RemoteAuthenticationStartup> serverFixture,
18+
TrimmingServerFixture<RemoteAuthenticationStartup> serverFixture,
3019
ITestOutputHelper output)
3120
: base(browserFixture, serverFixture, output)
3221
{
33-
serverFixture.ApplicationAssembly = typeof(RemoteAuthenticationStartup).Assembly;
34-
35-
if (TestTrimmedApps)
36-
{
37-
serverFixture.BuildWebHostMethod = BuildPublishedWebHost;
38-
serverFixture.GetContentRootMethod = GetPublishedContentRoot;
39-
}
4022
}
4123

4224
[Fact]
@@ -49,31 +31,4 @@ public void NavigateToLogin_PreservesExtraQueryParams()
4931
var heading = Browser.Exists(By.TagName("h1"));
5032
Browser.Equal("Hello, Jane Doe!", () => heading.Text);
5133
}
52-
53-
private static IHost BuildPublishedWebHost(string[] args) =>
54-
Host.CreateDefaultBuilder(args)
55-
.ConfigureLogging((ctx, lb) =>
56-
{
57-
var sink = new TestSink();
58-
lb.AddProvider(new TestLoggerProvider(sink));
59-
lb.Services.AddSingleton(sink);
60-
})
61-
.ConfigureWebHostDefaults(webHostBuilder =>
62-
{
63-
webHostBuilder.UseStartup<RemoteAuthenticationStartup>();
64-
// Avoid UseStaticAssets or we won't use the trimmed published output.
65-
})
66-
.Build();
67-
68-
private static string GetPublishedContentRoot(Assembly assembly)
69-
{
70-
var contentRoot = Path.Combine(AppContext.BaseDirectory, "trimmed-or-threading", assembly.GetName().Name);
71-
72-
if (!Directory.Exists(contentRoot))
73-
{
74-
throw new DirectoryNotFoundException($"Test is configured to use trimmed outputs, but trimmed outputs were not found in {contentRoot}.");
75-
}
76-
77-
return contentRoot;
78-
}
7934
}
Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Reflection;
54
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
65
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
76
using Microsoft.AspNetCore.E2ETesting;
@@ -10,26 +9,15 @@
109

1110
namespace Microsoft.AspNetCore.Components.E2ETest.Tests;
1211

13-
public class WebAssemblyPrerenderedTest : ServerTestBase<AspNetSiteServerFixture>
12+
public class WebAssemblyPrerenderedTest : ServerTestBase<TrimmingServerFixture<Wasm.Prerendered.Server.Startup>>
1413
{
1514
public WebAssemblyPrerenderedTest(
1615
BrowserFixture browserFixture,
17-
AspNetSiteServerFixture serverFixture,
16+
TrimmingServerFixture<Wasm.Prerendered.Server.Startup> serverFixture,
1817
ITestOutputHelper output)
1918
: base(browserFixture, serverFixture, output)
2019
{
21-
serverFixture.BuildWebHostMethod = Wasm.Prerendered.Server.Program.BuildWebHost;
2220
serverFixture.Environment = AspNetEnvironment.Development;
23-
24-
var testTrimmedApps = typeof(ToggleExecutionModeServerFixture<>).Assembly
25-
.GetCustomAttributes<AssemblyMetadataAttribute>()
26-
.First(m => m.Key == "Microsoft.AspNetCore.E2ETesting.TestTrimmedOrMultithreadingApps")
27-
.Value == "true";
28-
29-
if (testTrimmedApps)
30-
{
31-
serverFixture.GetContentRootMethod = GetPublishedContentRoot;
32-
}
3321
}
3422

3523
[Fact]
@@ -53,16 +41,4 @@ private void WaitUntilLoaded()
5341
var jsExecutor = (IJavaScriptExecutor)Browser;
5442
Browser.True(() => jsExecutor.ExecuteScript("return window['__aspnetcore__testing__blazor_wasm__started__'];") is not null);
5543
}
56-
57-
private static string GetPublishedContentRoot(Assembly assembly)
58-
{
59-
var contentRoot = Path.Combine(AppContext.BaseDirectory, "trimmed-or-threading", assembly.GetName().Name);
60-
61-
if (!Directory.Exists(contentRoot))
62-
{
63-
throw new DirectoryNotFoundException($"Test is configured to use trimmed outputs, but trimmed outputs were not found in {contentRoot}.");
64-
}
65-
66-
return contentRoot;
67-
}
6844
}

src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,16 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
8686

8787
_ = app.UseEndpoints(endpoints =>
8888
{
89-
endpoints.MapStaticAssets();
89+
var contentRootStaticAssetsPath = Path.Combine(env.ContentRootPath, "Components.TestServer.staticwebassets.endpoints.json");
90+
if (File.Exists(contentRootStaticAssetsPath))
91+
{
92+
endpoints.MapStaticAssets(contentRootStaticAssetsPath);
93+
}
94+
else
95+
{
96+
endpoints.MapStaticAssets();
97+
}
98+
9099
_ = endpoints.MapRazorComponents<TRootComponent>()
91100
.AddAdditionalAssemblies(Assembly.Load("Components.WasmMinimal"))
92101
.AddInteractiveServerRenderMode(options =>

src/Components/test/testassets/Components.TestServer/RemoteAuthenticationStartup.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,16 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
2828
app.UseAntiforgery();
2929
app.UseEndpoints(endpoints =>
3030
{
31-
#if !DEBUG
32-
endpoints.MapStaticAssets(Path.Combine("trimmed-or-threading", "Components.TestServer", "Components.TestServer.staticwebassets.endpoints.json"));
33-
#else
34-
endpoints.MapStaticAssets("Components.TestServer.staticwebassets.endpoints.json");
35-
#endif
31+
var contentRootStaticAssetsPath = Path.Combine(env.ContentRootPath, "Components.TestServer.staticwebassets.endpoints.json");
32+
if (File.Exists(contentRootStaticAssetsPath))
33+
{
34+
endpoints.MapStaticAssets(contentRootStaticAssetsPath);
35+
}
36+
else
37+
{
38+
endpoints.MapStaticAssets();
39+
}
40+
3641
endpoints.MapRazorComponents<RemoteAuthenticationApp>()
3742
.AddAdditionalAssemblies(Assembly.Load("Components.WasmRemoteAuthentication"))
3843
.AddInteractiveWebAssemblyRenderMode(options => options.PathPrefix = "/WasmRemoteAuthentication");

0 commit comments

Comments
 (0)