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
{

0 commit comments

Comments
 (0)