Skip to content

Add ClaimData for AuthenticationStateData and fix overtrimming #56878

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class AuthenticationStateData
/// <summary>
/// The client-readable claims that describe the <see cref="AuthenticationState.User"/>.
/// </summary>
public IList<KeyValuePair<string, string>> Claims { get; set; } = [];
public IList<ClaimData> Claims { get; set; } = [];

/// <summary>
/// Gets the value that identifies 'Name' claims. This is used when returning the property <see cref="ClaimsIdentity.Name"/>.
Expand Down
45 changes: 45 additions & 0 deletions src/Components/Authorization/src/ClaimData.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// This is a serializable representation of a <see cref="Claim"/> object that only consists of the type and value.
/// </summary>
public readonly struct ClaimData
{
/// <summary>
/// Constructs a new instance of <see cref="ClaimData"/> from a type and value.
/// </summary>
/// <param name="type">The claim type.</param>
/// <param name="value">The claim value</param>
[JsonConstructor]
public ClaimData(string type, string value)
{
Type = type;
Value = value;
}

/// <summary>
/// Constructs a new instance of <see cref="ClaimData"/> from a <see cref="Claim"/> copying only the
/// <see cref="Claim.Type"/> and <see cref="Claim.Value"/> into their corresponding properties.
/// </summary>
/// <param name="claim">The <see cref="Claim"/> to copy from.</param>
public ClaimData(Claim claim)
: this(claim.Type, claim.Value)
{
}

/// <summary>
/// Gets the claim type of the claim. <seealso cref="ClaimTypes"/>.
/// </summary>
public string Type { get; }

/// <summary>
/// Gets the value of the claim.
/// </summary>
public string Value { get; }
}
8 changes: 7 additions & 1 deletion src/Components/Authorization/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -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<System.Collections.Generic.KeyValuePair<string!, string!>>!
Microsoft.AspNetCore.Components.Authorization.AuthenticationStateData.Claims.get -> System.Collections.Generic.IList<Microsoft.AspNetCore.Components.Authorization.ClaimData>!
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!
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ private static Task<AuthenticationState> 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))));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<ClaimData>))]
[DynamicDependency(JsonSerialized, typeof(ClaimData))]
public DeserializedAuthenticationStateProvider(PersistentComponentState state, IOptions<AuthenticationStateDeserializationOptions> options)
{
if (!state.TryTakeFromJson<AuthenticationStateData?>(PersistenceKey, out var authenticationStateData) || authenticationStateData is null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public class BasicTestAppServerSiteFixture<TStartup> : AspNetSiteServerFixture w
{
public BasicTestAppServerSiteFixture()
{
ApplicationAssembly = typeof(TStartup).Assembly;
BuildWebHostMethod = TestServer.Program.BuildWebHost<TStartup>;
}
}
Original file line number Diff line number Diff line change
@@ -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<TStartup> : BasicTestAppServerSiteFixture<TStartup> where TStartup : class
{
public readonly bool TestTrimmedApps = typeof(ToggleExecutionModeServerFixture<>).Assembly
.GetCustomAttributes<AssemblyMetadataAttribute>()
.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<TStartup>();
// 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
namespace Microsoft.AspNetCore.Components.E2ETests.ServerRenderingTests.AuthTests;

public class DefaultAuthenticationStateSerializationOptionsTest
: ServerTestBase<BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<App>>>
: ServerTestBase<TrimmingServerFixture<RazorComponentEndpointsStartup<App>>>
{
public DefaultAuthenticationStateSerializationOptionsTest(
BrowserFixture browserFixture,
BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<App>> serverFixture,
TrimmingServerFixture<RazorComponentEndpointsStartup<App>> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
namespace Microsoft.AspNetCore.Components.E2ETests.ServerRenderingTests.AuthTests;

public class ServerRenderedAuthenticationStateTest
: ServerTestBase<BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<App>>>
: ServerTestBase<TrimmingServerFixture<RazorComponentEndpointsStartup<App>>>
{
public ServerRenderedAuthenticationStateTest(
BrowserFixture browserFixture,
BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<App>> serverFixture,
TrimmingServerFixture<RazorComponentEndpointsStartup<App>> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
Expand Down
49 changes: 2 additions & 47 deletions src/Components/test/E2ETest/Tests/RemoteAuthenticationTest.cs
Original file line number Diff line number Diff line change
@@ -1,42 +1,24 @@
// 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;

namespace Microsoft.AspNetCore.Components.E2ETest.Tests;

public class RemoteAuthenticationTest :
ServerTestBase<BasicTestAppServerSiteFixture<RemoteAuthenticationStartup>>
ServerTestBase<TrimmingServerFixture<RemoteAuthenticationStartup>>
{
public readonly bool TestTrimmedApps = typeof(ToggleExecutionModeServerFixture<>).Assembly
.GetCustomAttributes<AssemblyMetadataAttribute>()
.First(m => m.Key == "Microsoft.AspNetCore.E2ETesting.TestTrimmedOrMultithreadingApps")
.Value == "true";

public RemoteAuthenticationTest(
BrowserFixture browserFixture,
BasicTestAppServerSiteFixture<RemoteAuthenticationStartup> serverFixture,
TrimmingServerFixture<RemoteAuthenticationStartup> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
serverFixture.ApplicationAssembly = typeof(RemoteAuthenticationStartup).Assembly;

if (TestTrimmedApps)
{
serverFixture.BuildWebHostMethod = BuildPublishedWebHost;
serverFixture.GetContentRootMethod = GetPublishedContentRoot;
}
}

[Fact]
Expand All @@ -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<RemoteAuthenticationStartup>();
// 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;
}
}
28 changes: 2 additions & 26 deletions src/Components/test/E2ETest/Tests/WebAssemblyPrerenderedTest.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -10,26 +9,15 @@

namespace Microsoft.AspNetCore.Components.E2ETest.Tests;

public class WebAssemblyPrerenderedTest : ServerTestBase<AspNetSiteServerFixture>
public class WebAssemblyPrerenderedTest : ServerTestBase<TrimmingServerFixture<Wasm.Prerendered.Server.Startup>>
{
public WebAssemblyPrerenderedTest(
BrowserFixture browserFixture,
AspNetSiteServerFixture serverFixture,
TrimmingServerFixture<Wasm.Prerendered.Server.Startup> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
serverFixture.BuildWebHostMethod = Wasm.Prerendered.Server.Program.BuildWebHost;
serverFixture.Environment = AspNetEnvironment.Development;

var testTrimmedApps = typeof(ToggleExecutionModeServerFixture<>).Assembly
.GetCustomAttributes<AssemblyMetadataAttribute>()
.First(m => m.Key == "Microsoft.AspNetCore.E2ETesting.TestTrimmedOrMultithreadingApps")
.Value == "true";

if (testTrimmedApps)
{
serverFixture.GetContentRootMethod = GetPublishedContentRoot;
}
}

[Fact]
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<TRootComponent>()
.AddAdditionalAssemblies(Assembly.Load("Components.WasmMinimal"))
.AddInteractiveServerRenderMode(options =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<RemoteAuthenticationApp>()
.AddAdditionalAssemblies(Assembly.Load("Components.WasmRemoteAuthentication"))
.AddInteractiveWebAssemblyRenderMode(options => options.PathPrefix = "/WasmRemoteAuthentication");
Expand Down
Loading