From 21884d3227b3d3baec918c786f3e18500dd019fb Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Mon, 13 May 2024 17:22:33 -0700 Subject: [PATCH 1/3] Build in AuthenticationStateProviders from project templates --- .../src/AuthenticationStateData.cs | 27 ++++++ ...tEnvironmentAuthenticationStateProvider.cs | 6 +- .../Authorization/src/PublicAPI.Unshipped.txt | 8 ++ ...orComponentsServiceCollectionExtensions.cs | 3 + .../ServerAuthenticationStateProvider.cs | 2 +- .../Endpoints/src/PublicAPI.Unshipped.txt | 4 + .../src/Rendering/EndpointHtmlRenderer.cs | 16 +++- .../Server/src/Properties/AssemblyInfo.cs | 6 ++ .../Server/src/PublicAPI.Unshipped.txt | 8 ++ ...AuthenticationStateSerializationOptions.cs | 73 +++++++++++++++ .../src/AuthenticationStateSerializer.cs | 52 +++++++++++ .../Server/src/PublicAPI.Unshipped.txt | 7 ++ ...ssemblyRazorComponentsBuilderExtensions.cs | 21 +++++ ...thenticationStateDeserializationOptions.cs | 39 ++++++++ .../src/PublicAPI.Unshipped.txt | 5 ++ ...cs => AccessTokenNotAvailableException.cs} | 0 ...DeserializedAuthenticationStateProvider.cs | 36 ++++++++ ...thenticationServiceCollectionExtensions.cs | 21 +++++ ...t.AspNetCore.Components.WebAssembly.csproj | 1 + ...enticationStateSerializationOptionsTest.cs | 42 +++++++++ .../ServerRenderedAuthenticationStateTest.cs | 88 ++++++++++++++----- .../NoInteractivityTest.cs | 24 ++++- ...CascadingAuthenticationStateConsumer.razor | 48 +--------- ...omponentEndpointsNoInteractivityStartup.cs | 2 + .../RazorComponentEndpointsStartup.cs | 28 ++++-- .../RazorComponents/App.razor | 4 + .../Auth/InteractiveAuthenticationState.razor | 30 ------- ...ServerInteractiveAuthenticationState.razor | 6 ++ .../Auth/StaticAuthenticationState.razor | 10 +-- ...semblyInteractiveAuthenticationState.razor | 5 ++ .../Components.WasmMinimal.csproj | 1 + .../Components.WasmMinimal/Program.cs | 19 ++++ .../CascadingAuthenticationStateReader.razor | 88 +++++++++++++++++++ .../TestContentPackage.csproj | 1 + 34 files changed, 612 insertions(+), 119 deletions(-) create mode 100644 src/Components/Authorization/src/AuthenticationStateData.cs rename src/Components/{Server/src/Circuits => Endpoints/src/DependencyInjection}/ServerAuthenticationStateProvider.cs (94%) create mode 100644 src/Components/Server/src/Properties/AssemblyInfo.cs create mode 100644 src/Components/WebAssembly/Server/src/AuthenticationStateSerializationOptions.cs create mode 100644 src/Components/WebAssembly/Server/src/AuthenticationStateSerializer.cs create mode 100644 src/Components/WebAssembly/WebAssembly.Authentication/src/Options/AuthenticationStateDeserializationOptions.cs rename src/Components/WebAssembly/WebAssembly.Authentication/src/Services/{ExpiredTokenException.cs => AccessTokenNotAvailableException.cs} (100%) create mode 100644 src/Components/WebAssembly/WebAssembly.Authentication/src/Services/DeserializedAuthenticationStateProvider.cs create mode 100644 src/Components/test/E2ETest/ServerRenderingTests/AuthTests/DefaultAuthenticationStateSerializationOptionsTest.cs delete mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Auth/InteractiveAuthenticationState.razor create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Auth/ServerInteractiveAuthenticationState.razor create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Auth/WebAssemblyInteractiveAuthenticationState.razor create mode 100644 src/Components/test/testassets/TestContentPackage/CascadingAuthenticationStateReader.razor diff --git a/src/Components/Authorization/src/AuthenticationStateData.cs b/src/Components/Authorization/src/AuthenticationStateData.cs new file mode 100644 index 000000000000..da4b5da8085d --- /dev/null +++ b/src/Components/Authorization/src/AuthenticationStateData.cs @@ -0,0 +1,27 @@ +// 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; + +namespace Microsoft.AspNetCore.Components.Authorization; + +/// +/// A JSON-serializable type that represents the data that is used to create an . +/// +public class AuthenticationStateData +{ + /// + /// The client-readable claims that describe the . + /// + public IList> Claims { get; set; } = []; + + /// + /// Gets the value that identifies 'Name' claims. This is used when returning the property . + /// + public string NameClaimType { get; set; } = ClaimsIdentity.DefaultNameClaimType; + + /// + /// Gets the value that identifies 'Role' claims. This is used when calling . + /// + public string RoleClaimType { get; set; } = ClaimsIdentity.DefaultRoleClaimType; +} diff --git a/src/Components/Authorization/src/IHostEnvironmentAuthenticationStateProvider.cs b/src/Components/Authorization/src/IHostEnvironmentAuthenticationStateProvider.cs index 46810597846c..fe1dd31c82b8 100644 --- a/src/Components/Authorization/src/IHostEnvironmentAuthenticationStateProvider.cs +++ b/src/Components/Authorization/src/IHostEnvironmentAuthenticationStateProvider.cs @@ -4,8 +4,10 @@ namespace Microsoft.AspNetCore.Components.Authorization; /// -/// An interface implemented by classes that can receive authentication -/// state information from the host environment. +/// An interface implemented by services to receive authentication state information from the host environment. +/// If this is implemented by the host's , it will receive authentication state from the HttpContext. +/// Or if this implemented service that is registered directly as an , +/// it will receive the returned by /// public interface IHostEnvironmentAuthenticationStateProvider { diff --git a/src/Components/Authorization/src/PublicAPI.Unshipped.txt b/src/Components/Authorization/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..74b5773a99d5 100644 --- a/src/Components/Authorization/src/PublicAPI.Unshipped.txt +++ b/src/Components/Authorization/src/PublicAPI.Unshipped.txt @@ -1 +1,9 @@ #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.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 diff --git a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs index f695735195d2..9582ba579942 100644 --- a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs +++ b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection.Metadata; using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Endpoints; using Microsoft.AspNetCore.Components.Endpoints.DependencyInjection; using Microsoft.AspNetCore.Components.Endpoints.Forms; @@ -11,6 +12,7 @@ using Microsoft.AspNetCore.Components.Forms.Mapping; using Microsoft.AspNetCore.Components.Infrastructure; using Microsoft.AspNetCore.Components.Routing; +using Microsoft.AspNetCore.Components.Server; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -66,6 +68,7 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection ServiceDescriptor.Singleton, DefaultRazorComponentsServiceOptionsConfiguration>()); services.TryAddScoped(); services.TryAddScoped(sp => sp.GetRequiredService()); + services.TryAddScoped(); services.AddSupplyValueFromQueryProvider(); services.TryAddCascadingValue(sp => sp.GetRequiredService().HttpContext); diff --git a/src/Components/Server/src/Circuits/ServerAuthenticationStateProvider.cs b/src/Components/Endpoints/src/DependencyInjection/ServerAuthenticationStateProvider.cs similarity index 94% rename from src/Components/Server/src/Circuits/ServerAuthenticationStateProvider.cs rename to src/Components/Endpoints/src/DependencyInjection/ServerAuthenticationStateProvider.cs index 26e3512c177e..1d75b353d056 100644 --- a/src/Components/Server/src/Circuits/ServerAuthenticationStateProvider.cs +++ b/src/Components/Endpoints/src/DependencyInjection/ServerAuthenticationStateProvider.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Components.Server; /// public class ServerAuthenticationStateProvider : AuthenticationStateProvider, IHostEnvironmentAuthenticationStateProvider { - private Task _authenticationStateTask; + private Task? _authenticationStateTask; /// public override Task GetAuthenticationStateAsync() diff --git a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt index ede23967402c..95d31104cc53 100644 --- a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt +++ b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt @@ -1,4 +1,8 @@ #nullable enable Microsoft.AspNetCore.Components.Routing.RazorComponentsEndpointHttpContextExtensions +Microsoft.AspNetCore.Components.Server.ServerAuthenticationStateProvider +Microsoft.AspNetCore.Components.Server.ServerAuthenticationStateProvider.ServerAuthenticationStateProvider() -> void +Microsoft.AspNetCore.Components.Server.ServerAuthenticationStateProvider.SetAuthenticationState(System.Threading.Tasks.Task! authenticationStateTask) -> void +override Microsoft.AspNetCore.Components.Server.ServerAuthenticationStateProvider.GetAuthenticationStateAsync() -> System.Threading.Tasks.Task! static Microsoft.AspNetCore.Components.Endpoints.Infrastructure.ComponentEndpointConventionBuilderHelper.GetEndpointRouteBuilder(Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilder! builder) -> Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! static Microsoft.AspNetCore.Components.Routing.RazorComponentsEndpointHttpContextExtensions.AcceptsInteractiveRouting(this Microsoft.AspNetCore.Http.HttpContext! context) -> bool diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs index c32c602cfa2e..83b1a94e4cf9 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs @@ -78,10 +78,22 @@ internal static async Task InitializeStandardComponentServicesAsync( var navigationManager = (IHostEnvironmentNavigationManager)httpContext.RequestServices.GetRequiredService(); navigationManager?.Initialize(GetContextBaseUri(httpContext.Request), GetFullUri(httpContext.Request)); - if (httpContext.RequestServices.GetService() is IHostEnvironmentAuthenticationStateProvider authenticationStateProvider) + var authenticationStateProvider = httpContext.RequestServices.GetService(); + if (authenticationStateProvider is IHostEnvironmentAuthenticationStateProvider hostEnvironmentAuthenticationStateProvider) { var authenticationState = new AuthenticationState(httpContext.User); - authenticationStateProvider.SetAuthenticationState(Task.FromResult(authenticationState)); + hostEnvironmentAuthenticationStateProvider.SetAuthenticationState(Task.FromResult(authenticationState)); + } + + if (authenticationStateProvider != null) + { + var authStateListeners = httpContext.RequestServices.GetServices(); + Task? authStateTask = null; + foreach (var authStateListener in authStateListeners) + { + authStateTask ??= authenticationStateProvider.GetAuthenticationStateAsync(); + authStateListener.SetAuthenticationState(authStateTask); + } } if (handler != null && form != null) diff --git a/src/Components/Server/src/Properties/AssemblyInfo.cs b/src/Components/Server/src/Properties/AssemblyInfo.cs new file mode 100644 index 000000000000..bec22b01859d --- /dev/null +++ b/src/Components/Server/src/Properties/AssemblyInfo.cs @@ -0,0 +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.Runtime.CompilerServices; + +[assembly: TypeForwardedTo(typeof(Microsoft.AspNetCore.Components.Server.ServerAuthenticationStateProvider))] diff --git a/src/Components/Server/src/PublicAPI.Unshipped.txt b/src/Components/Server/src/PublicAPI.Unshipped.txt index 9d240138aaac..f16893dc154c 100644 --- a/src/Components/Server/src/PublicAPI.Unshipped.txt +++ b/src/Components/Server/src/PublicAPI.Unshipped.txt @@ -1,4 +1,11 @@ #nullable enable +*REMOVED*Microsoft.AspNetCore.Components.Server.ServerAuthenticationStateProvider +*REMOVED*Microsoft.AspNetCore.Components.Server.ServerAuthenticationStateProvider.ServerAuthenticationStateProvider() -> void +*REMOVED*Microsoft.AspNetCore.Components.Server.ServerAuthenticationStateProvider.SetAuthenticationState(System.Threading.Tasks.Task! authenticationStateTask) -> void +*REMOVED*override Microsoft.AspNetCore.Components.Server.ServerAuthenticationStateProvider.GetAuthenticationStateAsync() -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Components.Server.ServerAuthenticationStateProvider (forwarded, contained in Microsoft.AspNetCore.Components.Endpoints) +Microsoft.AspNetCore.Components.Server.ServerAuthenticationStateProvider.ServerAuthenticationStateProvider() -> void (forwarded, contained in Microsoft.AspNetCore.Components.Endpoints) +Microsoft.AspNetCore.Components.Server.ServerAuthenticationStateProvider.SetAuthenticationState(System.Threading.Tasks.Task! authenticationStateTask) -> void (forwarded, contained in Microsoft.AspNetCore.Components.Endpoints) Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ConfigureWebSocketAcceptContext.get -> System.Func? Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ConfigureWebSocketAcceptContext.set -> void @@ -7,4 +14,5 @@ Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ContentSe Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.DisableWebSocketCompression.get -> bool Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.DisableWebSocketCompression.set -> void Microsoft.AspNetCore.Components.Server.ServerComponentsEndpointOptions.ServerComponentsEndpointOptions() -> void +override Microsoft.AspNetCore.Components.Server.ServerAuthenticationStateProvider.GetAuthenticationStateAsync() -> System.Threading.Tasks.Task! (forwarded, contained in Microsoft.AspNetCore.Components.Endpoints) static Microsoft.AspNetCore.Builder.ServerRazorComponentsEndpointConventionBuilderExtensions.AddInteractiveServerRenderMode(this Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilder! builder, System.Action! configure) -> Microsoft.AspNetCore.Builder.RazorComponentsEndpointConventionBuilder! diff --git a/src/Components/WebAssembly/Server/src/AuthenticationStateSerializationOptions.cs b/src/Components/WebAssembly/Server/src/AuthenticationStateSerializationOptions.cs new file mode 100644 index 000000000000..d7c3dc752a47 --- /dev/null +++ b/src/Components/WebAssembly/Server/src/AuthenticationStateSerializationOptions.cs @@ -0,0 +1,73 @@ +// 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.Security.Claims; +using Microsoft.AspNetCore.Components.Authorization; + +namespace Microsoft.AspNetCore.Components.WebAssembly.Server; + +/// +/// Provides options for configuring the JSON serialization of the provided by the server's +/// to the WebAssembly client using . +/// +public class AuthenticationStateSerializationOptions +{ + /// + /// Default constructor for . + /// + public AuthenticationStateSerializationOptions() + { + SerializationCallback = SerializeAuthenticationStateAsync; + } + + /// + /// If , the default will serialize all claims; otherwise, it will only serialize + /// the and claims. + /// + public bool SerializeAllClaims { get; set; } + + /// + /// Default implementation for converting the server's to an object + /// for JSON serialization to the client using ."/> + /// + public Func> SerializationCallback { get; set; } + + private ValueTask SerializeAuthenticationStateAsync(AuthenticationState authenticationState) + { + AuthenticationStateData? data = null; + + if (authenticationState.User.Identity?.IsAuthenticated ?? false) + { + data = new AuthenticationStateData(); + + if (authenticationState.User.Identities.FirstOrDefault() is { } identity) + { + data.NameClaimType = identity.NameClaimType; + data.RoleClaimType = identity.RoleClaimType; + } + + if (SerializeAllClaims) + { + foreach (var claim in authenticationState.User.Claims) + { + data.Claims.Add(new(claim.Type, claim.Value)); + } + } + else + { + if (authenticationState.User.FindFirst(data.NameClaimType) is { } nameClaim) + { + data.Claims.Add(new(nameClaim.Type, nameClaim.Value)); + } + + foreach (var roleClaim in authenticationState.User.FindAll(data.RoleClaimType)) + { + data.Claims.Add(new(roleClaim.Type, roleClaim.Value)); + } + } + } + + return ValueTask.FromResult(data); + } +} diff --git a/src/Components/WebAssembly/Server/src/AuthenticationStateSerializer.cs b/src/Components/WebAssembly/Server/src/AuthenticationStateSerializer.cs new file mode 100644 index 000000000000..4c4a83e23196 --- /dev/null +++ b/src/Components/WebAssembly/Server/src/AuthenticationStateSerializer.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Components.WebAssembly.Server; + +internal sealed class AuthenticationStateSerializer : IHostEnvironmentAuthenticationStateProvider, IDisposable +{ + // Do not change. This must match all versions of the server-side DeserializedAuthenticationStateProvider.PersistenceKey. + internal const string PersistenceKey = $"__internal__{nameof(AuthenticationState)}"; + + private readonly PersistentComponentState _state; + private readonly Func> _serializeCallback; + private readonly PersistingComponentStateSubscription _subscription; + + private Task? _authenticationStateTask; + + public AuthenticationStateSerializer(PersistentComponentState persistentComponentState, IOptions options) + { + _state = persistentComponentState; + _serializeCallback = options.Value.SerializationCallback; + _subscription = persistentComponentState.RegisterOnPersisting(OnPersistingAsync, RenderMode.InteractiveWebAssembly); + } + + private async Task OnPersistingAsync() + { + if (_authenticationStateTask is null) + { + throw new InvalidOperationException($"{nameof(SetAuthenticationState)} must be called before the {nameof(PersistentComponentState)}.{nameof(PersistentComponentState.RegisterOnPersisting)} callback."); + } + + var authenticationStateData = await _serializeCallback(await _authenticationStateTask); + if (authenticationStateData is not null) + { + _state.PersistAsJson(PersistenceKey, authenticationStateData); + } + } + + /// + public void SetAuthenticationState(Task authenticationStateTask) + { + _authenticationStateTask = authenticationStateTask ?? throw new ArgumentNullException(nameof(authenticationStateTask)); + } + + public void Dispose() + { + _subscription.Dispose(); + } +} diff --git a/src/Components/WebAssembly/Server/src/PublicAPI.Unshipped.txt b/src/Components/WebAssembly/Server/src/PublicAPI.Unshipped.txt index deab402bac7a..ce19c26b8ac9 100644 --- a/src/Components/WebAssembly/Server/src/PublicAPI.Unshipped.txt +++ b/src/Components/WebAssembly/Server/src/PublicAPI.Unshipped.txt @@ -1,5 +1,12 @@ #nullable enable +Microsoft.AspNetCore.Components.WebAssembly.Server.AuthenticationStateSerializationOptions +Microsoft.AspNetCore.Components.WebAssembly.Server.AuthenticationStateSerializationOptions.AuthenticationStateSerializationOptions() -> void +Microsoft.AspNetCore.Components.WebAssembly.Server.AuthenticationStateSerializationOptions.SerializationCallback.get -> System.Func>! +Microsoft.AspNetCore.Components.WebAssembly.Server.AuthenticationStateSerializationOptions.SerializationCallback.set -> void +Microsoft.AspNetCore.Components.WebAssembly.Server.AuthenticationStateSerializationOptions.SerializeAllClaims.get -> bool +Microsoft.AspNetCore.Components.WebAssembly.Server.AuthenticationStateSerializationOptions.SerializeAllClaims.set -> void Microsoft.AspNetCore.Components.WebAssembly.Server.WebAssemblyComponentsEndpointOptions.ServeMultithreadingHeaders.get -> bool Microsoft.AspNetCore.Components.WebAssembly.Server.WebAssemblyComponentsEndpointOptions.ServeMultithreadingHeaders.set -> void Microsoft.AspNetCore.Components.WebAssembly.Server.WebAssemblyComponentsEndpointOptions.StaticAssetsManifestPath.get -> string? Microsoft.AspNetCore.Components.WebAssembly.Server.WebAssemblyComponentsEndpointOptions.StaticAssetsManifestPath.set -> void +static Microsoft.Extensions.DependencyInjection.WebAssemblyRazorComponentsBuilderExtensions.AddAuthenticationStateSerialization(this Microsoft.Extensions.DependencyInjection.IRazorComponentsBuilder! builder, System.Action? configure = null) -> Microsoft.Extensions.DependencyInjection.IRazorComponentsBuilder! diff --git a/src/Components/WebAssembly/Server/src/WebAssemblyRazorComponentsBuilderExtensions.cs b/src/Components/WebAssembly/Server/src/WebAssemblyRazorComponentsBuilderExtensions.cs index f390f78cb889..ffe90126c662 100644 --- a/src/Components/WebAssembly/Server/src/WebAssemblyRazorComponentsBuilderExtensions.cs +++ b/src/Components/WebAssembly/Server/src/WebAssemblyRazorComponentsBuilderExtensions.cs @@ -3,8 +3,10 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Endpoints.Infrastructure; using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components.WebAssembly.Server; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -30,6 +32,25 @@ public static IRazorComponentsBuilder AddInteractiveWebAssemblyComponents(this I return builder; } + /// + /// Serializes the returned by the server-side using + /// for use by interactive WebAssembly components via a deserializing client-side which can be added by calling + /// AddAuthenticationStateDeserialization from the Microsoft.AspNetCore.Components.WebAssembly.Authentication package in the client project. + /// + /// The . + /// A callback to customize the serialization of the . + /// An that can be used to further customize the configuration. + public static IRazorComponentsBuilder AddAuthenticationStateSerialization(this IRazorComponentsBuilder builder, Action? configure = null) + { + builder.Services.TryAddEnumerable(ServiceDescriptor.Scoped()); + if (configure is not null) + { + builder.Services.Configure(configure); + } + + return builder; + } + private class WebAssemblyEndpointProvider(IServiceProvider services) : RenderModeEndpointProvider { public override IEnumerable GetEndpointBuilders(IComponentRenderMode renderMode, IApplicationBuilder applicationBuilder) diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/src/Options/AuthenticationStateDeserializationOptions.cs b/src/Components/WebAssembly/WebAssembly.Authentication/src/Options/AuthenticationStateDeserializationOptions.cs new file mode 100644 index 000000000000..58bf7e903f50 --- /dev/null +++ b/src/Components/WebAssembly/WebAssembly.Authentication/src/Options/AuthenticationStateDeserializationOptions.cs @@ -0,0 +1,39 @@ +// 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.Security.Claims; +using Microsoft.AspNetCore.Components.Authorization; + +namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication; + +/// +/// Provides options for configuring the JSON deserialization of the client's from the server using . +/// +public sealed class AuthenticationStateDeserializationOptions +{ + private static readonly Task _defaultUnauthenticatedStateTask = + Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()))); + + /// + /// Default implementation for converting the that was JSON deserialized from the server + /// using to an object to be returned by the WebAssembly + /// client's . + /// + public Func> DeserializationCallback { get; set; } = DeserializeAuthenticationStateAsync; + + private static Task DeserializeAuthenticationStateAsync(AuthenticationStateData? authenticationStateData) + { + if (authenticationStateData is null) + { + return _defaultUnauthenticatedStateTask; + } + + return Task.FromResult( + new AuthenticationState(new ClaimsPrincipal( + new ClaimsIdentity(authenticationStateData.Claims.Select(c => new Claim(c.Key, c.Value)), + authenticationType: nameof(DeserializedAuthenticationStateProvider), + nameType: authenticationStateData.NameClaimType, + roleType: authenticationStateData.RoleClaimType)))); + } +} diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/src/PublicAPI.Unshipped.txt b/src/Components/WebAssembly/WebAssembly.Authentication/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..e6de697cb73c 100644 --- a/src/Components/WebAssembly/WebAssembly.Authentication/src/PublicAPI.Unshipped.txt +++ b/src/Components/WebAssembly/WebAssembly.Authentication/src/PublicAPI.Unshipped.txt @@ -1 +1,6 @@ #nullable enable +Microsoft.AspNetCore.Components.WebAssembly.Authentication.AuthenticationStateDeserializationOptions +Microsoft.AspNetCore.Components.WebAssembly.Authentication.AuthenticationStateDeserializationOptions.AuthenticationStateDeserializationOptions() -> void +Microsoft.AspNetCore.Components.WebAssembly.Authentication.AuthenticationStateDeserializationOptions.DeserializationCallback.get -> System.Func!>! +Microsoft.AspNetCore.Components.WebAssembly.Authentication.AuthenticationStateDeserializationOptions.DeserializationCallback.set -> void +static Microsoft.Extensions.DependencyInjection.WebAssemblyAuthenticationServiceCollectionExtensions.AddAuthenticationStateDeserialization(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action? configure = null) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/ExpiredTokenException.cs b/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/AccessTokenNotAvailableException.cs similarity index 100% rename from src/Components/WebAssembly/WebAssembly.Authentication/src/Services/ExpiredTokenException.cs rename to src/Components/WebAssembly/WebAssembly.Authentication/src/Services/AccessTokenNotAvailableException.cs diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/DeserializedAuthenticationStateProvider.cs b/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/DeserializedAuthenticationStateProvider.cs new file mode 100644 index 000000000000..065fffdb3951 --- /dev/null +++ b/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/DeserializedAuthenticationStateProvider.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Security.Claims; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication; + +internal sealed class DeserializedAuthenticationStateProvider : AuthenticationStateProvider +{ + // Do not change. This must match all versions of the server-side AuthenticationStateSerializer.PersistenceKey. + private const string PersistenceKey = $"__internal__{nameof(AuthenticationState)}"; + + private static readonly Task _defaultUnauthenticatedTask = + Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()))); + + private readonly Task _authenticationStateTask = _defaultUnauthenticatedTask; + + [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.")] + public DeserializedAuthenticationStateProvider(PersistentComponentState state, IOptions options) + { + if (!state.TryTakeFromJson(PersistenceKey, out var authenticationStateData) || authenticationStateData is null) + { + return; + } + + _authenticationStateTask = options.Value.DeserializationCallback(authenticationStateData); + } + + public override Task GetAuthenticationStateAsync() => _authenticationStateTask; +} diff --git a/src/Components/WebAssembly/WebAssembly.Authentication/src/WebAssemblyAuthenticationServiceCollectionExtensions.cs b/src/Components/WebAssembly/WebAssembly.Authentication/src/WebAssemblyAuthenticationServiceCollectionExtensions.cs index 4c40da411b76..20cf1e0867f7 100644 --- a/src/Components/WebAssembly/WebAssembly.Authentication/src/WebAssemblyAuthenticationServiceCollectionExtensions.cs +++ b/src/Components/WebAssembly/WebAssembly.Authentication/src/WebAssemblyAuthenticationServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.WebAssembly.Authentication; using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal; @@ -17,6 +18,26 @@ namespace Microsoft.Extensions.DependencyInjection; /// public static class WebAssemblyAuthenticationServiceCollectionExtensions { + /// + /// Adds an where the is deserialized from the server + /// using and . There should be a corresponding call to + /// AddAuthenticationStateSerialization from the Microsoft.AspNetCore.Components.WebAssembly.Server package in the server project. + /// + /// The to add the services to. + /// An action that will configure the . + /// + public static IServiceCollection AddAuthenticationStateDeserialization(this IServiceCollection services, Action? configure = null) + { + services.AddOptions(); + services.TryAddScoped(); + if (configure != null) + { + services.Configure(configure); + } + + return services; + } + /// /// Adds support for authentication for SPA applications using the given and /// . diff --git a/src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.csproj b/src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.csproj index 896817768457..83377b00e498 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.csproj +++ b/src/Components/WebAssembly/WebAssembly/src/Microsoft.AspNetCore.Components.WebAssembly.csproj @@ -49,6 +49,7 @@ + diff --git a/src/Components/test/E2ETest/ServerRenderingTests/AuthTests/DefaultAuthenticationStateSerializationOptionsTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/AuthTests/DefaultAuthenticationStateSerializationOptionsTest.cs new file mode 100644 index 000000000000..273f48f82b12 --- /dev/null +++ b/src/Components/test/E2ETest/ServerRenderingTests/AuthTests/DefaultAuthenticationStateSerializationOptionsTest.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Components.TestServer.RazorComponents; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.AspNetCore.E2ETesting; +using OpenQA.Selenium; +using TestServer; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Components.E2ETests.ServerRenderingTests.AuthTests; + +public class DefaultAuthenticationStateSerializationOptionsTest + : ServerTestBase>> +{ + public DefaultAuthenticationStateSerializationOptionsTest( + BrowserFixture browserFixture, + BasicTestAppServerSiteFixture> serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) + { + } + + [Fact] + public void DoesNotSerializeAllClaimsByDefault() + { + Navigate($"{ServerPathBase}/auth/webassembly-interactive-authentication-state?roleClaimType=role&nameClaimType=name"); + + Browser.Click(By.LinkText("Log in")); + + Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive")).Text); + Browser.Equal("WebAssembly", () => Browser.FindElement(By.Id("platform")).Text); + Browser.Equal("True", () => Browser.FindElement(By.Id("identity-authenticated")).Text); + Browser.Equal("YourUsername", () => Browser.FindElement(By.Id("identity-name")).Text); + Browser.Equal("True", () => Browser.FindElement(By.Id("is-in-test-role-1")).Text); + Browser.Equal("True", () => Browser.FindElement(By.Id("is-in-test-role-2")).Text); + + // While the name and role claims are serialized by default, the test claim is not. + Browser.Equal("(none)", () => Browser.FindElement(By.Id("test-claim")).Text); + } +} diff --git a/src/Components/test/E2ETest/ServerRenderingTests/AuthTests/ServerRenderedAuthenticationStateTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/AuthTests/ServerRenderedAuthenticationStateTest.cs index 378b146dd787..931b2368c241 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/AuthTests/ServerRenderedAuthenticationStateTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/AuthTests/ServerRenderedAuthenticationStateTest.cs @@ -2,12 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using Components.TestServer.RazorComponents; -using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; -using TestServer; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; using Microsoft.AspNetCore.E2ETesting; -using Xunit.Abstractions; using OpenQA.Selenium; +using TestServer; +using Xunit.Abstractions; namespace Microsoft.AspNetCore.Components.E2ETests.ServerRenderingTests.AuthTests; @@ -20,46 +20,90 @@ public ServerRenderedAuthenticationStateTest( ITestOutputHelper output) : base(browserFixture, serverFixture, output) { + // Test with AuthenticationStateSerializationOptions.SerializeAllClaims = true since that keeps the Server and WebAssembly + // behavior as similar as possible. The default behavior is tested by DefaultAuthenticationStateSerializationOptionsTest. + serverFixture.AdditionalArguments.Add("SerializeAllClaims=true"); + } + + [Theory] + [InlineData("Static")] + [InlineData("Server")] + [InlineData("WebAssembly")] + public void CanUseServerAuthenticationState(string platform) + { + var pageName = $"{platform}{(platform != "Static" ? "-interactive" : "")}-authentication-state"; + Navigate($"{ServerPathBase}/auth/{pageName}"); + + VerifyLoggedOut(platform); + + Browser.Click(By.LinkText("Log in")); + + VerifyLoggedIn(platform); + + Browser.Click(By.LinkText("Log out")); + + VerifyLoggedOut(platform); } [Fact] - public void CanUseServerAuthenticationState_Static() + public void CanUseCustomNameAndRoleTypeOnWebAssembly() { - Navigate($"{ServerPathBase}/auth/static-authentication-state"); + Navigate($"{ServerPathBase}/auth/webassembly-interactive-authentication-state?roleClaimType=role&nameClaimType=name"); - Browser.Equal("False", () => Browser.FindElement(By.Id("identity-authenticated")).Text); - Browser.Equal("", () => Browser.FindElement(By.Id("identity-name")).Text); - Browser.Equal("(none)", () => Browser.FindElement(By.Id("test-claim")).Text); + VerifyLoggedOut("WebAssembly"); Browser.Click(By.LinkText("Log in")); - Browser.Equal("True", () => Browser.FindElement(By.Id("identity-authenticated")).Text); - Browser.Equal("YourUsername", () => Browser.FindElement(By.Id("identity-name")).Text); - Browser.Equal("Test claim value", () => Browser.FindElement(By.Id("test-claim")).Text); + VerifyLoggedIn("WebAssembly"); + Browser.Equal("(none)", () => Browser.FindElement(By.Id("additional-claim")).Text); Browser.Click(By.LinkText("Log out")); - Browser.Equal("False", () => Browser.FindElement(By.Id("identity-authenticated")).Text); + + VerifyLoggedOut("WebAssembly"); } [Fact] - public void CanUseServerAuthenticationState_Interactive() + public void CanCustomizeAuthenticationStateDeserialization() { - Navigate($"{ServerPathBase}/auth/interactive-authentication-state"); + Navigate($"{ServerPathBase}/auth/webassembly-interactive-authentication-state?additionalClaim=Custom%20claim%20value"); + + VerifyLoggedOut("WebAssembly"); + Browser.Equal("(none)", () => Browser.FindElement(By.Id("additional-claim")).Text); + + Browser.Click(By.LinkText("Log in")); + + VerifyLoggedIn("WebAssembly"); + Browser.Equal("Custom claim value", () => Browser.FindElement(By.Id("additional-claim")).Text); - Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive")).Text); + Browser.Click(By.LinkText("Log out")); + + VerifyLoggedOut("WebAssembly"); + Browser.Equal("(none)", () => Browser.FindElement(By.Id("additional-claim")).Text); + } + + private void VerifyPlatform(string platform) + { + Browser.Equal((platform != "Static").ToString(), () => Browser.FindElement(By.Id("is-interactive")).Text); + Browser.Equal(platform, () => Browser.FindElement(By.Id("platform")).Text); + } + + private void VerifyLoggedOut(string platform) + { + VerifyPlatform(platform); Browser.Equal("False", () => Browser.FindElement(By.Id("identity-authenticated")).Text); Browser.Equal("", () => Browser.FindElement(By.Id("identity-name")).Text); Browser.Equal("(none)", () => Browser.FindElement(By.Id("test-claim")).Text); + Browser.Equal("False", () => Browser.FindElement(By.Id("is-in-test-role-1")).Text); + Browser.Equal("False", () => Browser.FindElement(By.Id("is-in-test-role-2")).Text); + } - Browser.Click(By.LinkText("Log in")); - - Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive")).Text); + private void VerifyLoggedIn(string platform) + { + VerifyPlatform(platform); Browser.Equal("True", () => Browser.FindElement(By.Id("identity-authenticated")).Text); Browser.Equal("YourUsername", () => Browser.FindElement(By.Id("identity-name")).Text); Browser.Equal("Test claim value", () => Browser.FindElement(By.Id("test-claim")).Text); - - Browser.Click(By.LinkText("Log out")); - Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive")).Text); - Browser.Equal("False", () => Browser.FindElement(By.Id("identity-authenticated")).Text); + Browser.Equal("True", () => Browser.FindElement(By.Id("is-in-test-role-1")).Text); + Browser.Equal("True", () => Browser.FindElement(By.Id("is-in-test-role-2")).Text); } } diff --git a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs index 2a10978617cd..b0f887da31df 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs @@ -5,7 +5,6 @@ using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; using Microsoft.AspNetCore.E2ETesting; -using Microsoft.AspNetCore.InternalTesting; using OpenQA.Selenium; using TestServer; using Xunit.Abstractions; @@ -41,4 +40,27 @@ public void NavigationManagerCanRefreshSSRPageWhenInteractivityNotPresent() Browser.NotEqual(guid, () => Browser.Exists(By.Id("guid")).Text); Browser.Equal("GET", () => Browser.Exists(By.Id("method")).Text); } + + [Fact] + public void CanUseServerAuthenticationStateByDefault() + { + Navigate($"{ServerPathBase}/auth/static-authentication-state"); + + Browser.Equal("False", () => Browser.FindElement(By.Id("is-interactive")).Text); + Browser.Equal("Static", () => Browser.FindElement(By.Id("platform")).Text); + + Browser.Equal("False", () => Browser.FindElement(By.Id("identity-authenticated")).Text); + Browser.Equal("", () => Browser.FindElement(By.Id("identity-name")).Text); + Browser.Equal("(none)", () => Browser.FindElement(By.Id("test-claim")).Text); + Browser.Equal("False", () => Browser.FindElement(By.Id("is-in-test-role-1")).Text); + Browser.Equal("False", () => Browser.FindElement(By.Id("is-in-test-role-2")).Text); + + Browser.Click(By.LinkText("Log in")); + + Browser.Equal("True", () => Browser.FindElement(By.Id("identity-authenticated")).Text); + Browser.Equal("YourUsername", () => Browser.FindElement(By.Id("identity-name")).Text); + Browser.Equal("Test claim value", () => Browser.FindElement(By.Id("test-claim")).Text); + Browser.Equal("True", () => Browser.FindElement(By.Id("is-in-test-role-1")).Text); + Browser.Equal("True", () => Browser.FindElement(By.Id("is-in-test-role-2")).Text); + } } diff --git a/src/Components/test/testassets/BasicTestApp/AuthTest/CascadingAuthenticationStateConsumer.razor b/src/Components/test/testassets/BasicTestApp/AuthTest/CascadingAuthenticationStateConsumer.razor index 48852bf72908..1cc0035b7f4f 100644 --- a/src/Components/test/testassets/BasicTestApp/AuthTest/CascadingAuthenticationStateConsumer.razor +++ b/src/Components/test/testassets/BasicTestApp/AuthTest/CascadingAuthenticationStateConsumer.razor @@ -1,48 +1,4 @@ @page "/CascadingAuthenticationStateConsumer" -@using System.Security.Claims -@using Microsoft.AspNetCore.Components.Authorization +@using TestContentPackage -

Cascading authentication state

- -@if (user == null) -{ - Requesting authentication state... -} -else -{ -

- Authenticated: - @user.Identity.IsAuthenticated -

- -

- Name: - @user.Identity.Name -

- -

- Test claim: - @if (user.HasClaim(TestClaimPredicate) == true) - { - @user.Claims.Single(c => TestClaimPredicate(c)).Value - } - else - { - (none) - } -

-} - -@code -{ - static Predicate TestClaimPredicate = c => c.Type == "test-claim"; - - ClaimsPrincipal user; - - [CascadingParameter] Task AuthenticationStateTask { get; set; } - - protected override async Task OnParametersSetAsync() - { - user = (await AuthenticationStateTask).User; - } -} + diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsNoInteractivityStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsNoInteractivityStartup.cs index 00c9fe461b31..c4af233c40fe 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsNoInteractivityStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsNoInteractivityStartup.cs @@ -31,6 +31,7 @@ public void ConfigureServices(IServiceCollection services) options.MaxFormMappingCollectionSize = 100; }); services.AddHttpContextAccessor(); + services.AddCascadingAuthenticationState(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -54,6 +55,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseStaticFiles(); app.UseRouting(); + RazorComponentEndpointsStartup.UseFakeAuthState(app); app.UseAntiforgery(); app.UseEndpoints(endpoints => { diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs index 04d13dbf835f..7a9463136a36 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs @@ -33,7 +33,13 @@ public void ConfigureServices(IServiceCollection services) options.MaxFormMappingCollectionSize = 100; }) .AddInteractiveWebAssemblyComponents() - .AddInteractiveServerComponents(); + .AddInteractiveServerComponents() + .AddAuthenticationStateSerialization(options => + { + bool.TryParse(Configuration["SerializeAllClaims"], out var serializeAllClaims); + options.SerializeAllClaims = serializeAllClaims; + }); + services.AddHttpContextAccessor(); services.AddSingleton(); services.AddCascadingAuthenticationState(); @@ -103,18 +109,21 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) }); } - private static void UseFakeAuthState(IApplicationBuilder app) + internal static void UseFakeAuthState(IApplicationBuilder app) { app.Use((HttpContext context, Func next) => { // Completely insecure fake auth system with no password for tests. Do not do anything like this in real apps. // It accepts a query parameter 'username' and then sets or deletes a cookie to hold that, and supplies a principal // using this username (taken either from the cookie or query param). + string GetQueryOrDefault(string queryKey, string defaultValue) => + context.Request.Query.TryGetValue(queryKey, out var value) ? value : defaultValue; + const string cookieKey = "fake_username"; - context.Request.Cookies.TryGetValue(cookieKey, out var username); - if (context.Request.Query.TryGetValue("username", out var usernameFromQuery)) + var username = GetQueryOrDefault("username", context.Request.Cookies[cookieKey]); + + if (context.Request.Query.ContainsKey("username")) { - username = usernameFromQuery; if (string.IsNullOrEmpty(username)) { context.Response.Cookies.Delete(cookieKey); @@ -126,15 +135,20 @@ private static void UseFakeAuthState(IApplicationBuilder app) } } + var nameClaimType = GetQueryOrDefault("nameClaimType", ClaimTypes.Name); + var roleClaimType = GetQueryOrDefault("roleClaimType", ClaimTypes.Role); + if (!string.IsNullOrEmpty(username)) { var claims = new List { - new Claim(ClaimTypes.Name, username), + new Claim(nameClaimType, username), + new Claim(roleClaimType, "test-role-1"), + new Claim(roleClaimType, "test-role-2"), new Claim("test-claim", "Test claim value"), }; - context.User = new ClaimsPrincipal(new ClaimsIdentity(claims, "FakeAuthenticationType")); + context.User = new ClaimsPrincipal(new ClaimsIdentity(claims, "FakeAuthenticationType", nameClaimType, roleClaimType)); } return next(); diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor index 094bb249394f..d0344cb014c3 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor @@ -18,6 +18,10 @@