-
Notifications
You must be signed in to change notification settings - Fork 10k
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
Build in AuthenticationStateProviders from Blazor Web templates #52769
Comments
Thanks for contacting us. We're moving this issue to the |
Is there any guides on how to have a workaround untill this is finished? Wanted to suggest the newly released Blazor United as a base for an upcoming project, but since I can't figure out how the authentication and authorization against B2C with custom user flows would work, that a nonstarter, sadly. I assume in might work OK with server-side rendering, but the whole idea is to have them mixed. And since there is no template for Microsoft Identity (only individual accounts), then I assume that is not guaranteed to work, right? |
You can take a look at https://learn.microsoft.com/en-us/aspnet/core/blazor/security/blazor-web-app-with-oidc. The sample include PersistingServerAuthenticationStateProvider.cs and PersistentAuthenticationStateProvider.cs that are similar to the ones in the individual auth template. |
Background and MotivationIn .NET 9, we should automatically register both server and client This should make it a lot easier to add auth to a Blazor project that is created without the "Individual Auth" template. As part of this, we will want to figure out which claims are safe to transmit to the clients, and potentially add a callback to configure custom claims. Proposed API// Microsoft.AspNetCore.Components.Endpoints.dll
namespace Microsoft.AspNetCore.Components.Endpoints;
/// <summary>
/// Provides options for configuring server-side rendering of Razor Components.
/// </summary>
public sealed class RazorComponentsServiceOptions
{
+ public bool SerializeAuthenticationStateToClient { get; set; } = false;
+
+ public Func<AuthenticationState, Task<IEnumerable<KeyValuePair<string, string>>>> SerializeAuthenticationState { get; set; } = SerializeClaimsDefault;
private static async Task<IEnumerable<KeyValuePair<string, string>>> SerializeClaimsDefault(AuthenticationState authenticationState)
{
foreach (var claim in authenticationState.User.Claims)
{
yield return new KeyValuePair<string, string>(claim.Type, claim.Value);
}
}
} // namespace Microsoft.AspNetCore.Components.WebAssembly.dll
namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting;
+ public sealed class AuthenticationStateDeserializationOptions
+ {
+ public Func<IEnumerable<KeyValuePair<string, string>>, Task<AuthenticationState>> DeserializeAuthenticationState { get; set; } = DeserializeAuthenticationState;
+
+ private static Task<AuthenticationState> DeserializeClaimsDefault(IEnumerable<KeyValuePair<string, string>> claims)
+ {
+ return Task.FromResult(
+ new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims.Select(c => new(c.Type, c.Value),
+ authenticationType: "DeserializedAuthenticationState"))));
+ }
+ }
+
namespace Microsoft.Extensions.DependencyInjection;
+ public static class DeserializeAuthentciationStateServiceCollectionExtensions
+ {
+ public static void ConfigureAuthenticationStateDeserialization(this IServiceCollection services, Action<AuthenticationStateDeserializationOptions> configureOptions);
+ } Usage ExamplesThe simplest way to serialize/deserialize all claims is just to set // Program.cs for server project
builder.Services.AddRazorComponents(o => o.SerializeAuthenticationStateToClient = true)
.AddInteractiveWebAssemblyComponents() Alternatively, you could include only specific claims: // Program.cs for server project
async Task<IEnumerable<KeyValuePair<string, string>>> MySerializeClaims(AuthenticationState authenticationState)
{
foreach (var claim in authenticationState.User.Claims)
{
if (myAllowedClaims.Contains(claim.Type))
{
yield return new KeyValuePair<string, string>(claim.Type, claim.Value);
}
}
}
builder.Services.AddRazorComponents(o =>
{
o.SerializeAuthenticationStateToClient = true;
o.SerializeAuthenticationState = MySerializeClaims
})
.AddInteractiveWebAssemblyComponents()
.AddInteractiveServerComponents(); Alternative DesignsWe could change all references to "AuthenticationState" to "ClaimsPrincipal". We could come up with better naming for "serialize" and "deserialize"? Maybe something involving "transmit" or "initialize". Instead of exposing RisksWe must always register a default |
|
Updated API Proposal// Microsoft.AspNetCore.Components.Server.dll
+ [assembly: TypeForwardedTo(typeof(Microsoft.AspNetCore.Components.Server.ServerAuthenticationStateProvider))]
// Microsoft.AspNetCore.Components.Endpoints.dll
+ namespace Microsoft.AspNetCore.Components.Server;
+ public class ServerAuthenticationStateProvider : AuthenticationStateProvider, IHostEnvironmentAuthenticationStateProvider // Microsoft.AspNetCore.Components.Authorization.dll
namespace Microsoft.AspNetCore.Components.Authorization;
+ public class AuthenticationStateData
+ {
+ public IList<KeyValuePair<string, string>> Claims { get; set; } = [];
+ public string NameClaimType { get; set; } = ClaimsIdentity.DefaultNameClaimType;
+ public string RoleClaimType { get; set; } = ClaimsIdentity.DefaultRoleClaimType;
+ } // Microsoft.AspNetCore.Components.WebAssembly.Server.dll
namespace Microsoft.AspNetCore.Components.WebAssembly.Server;
+ public class AuthenticationStateSerializationOptions
+ {
+ public bool SerializeAllClaims { get; set; }
+ public Func<AuthenticationState, ValueTask<AuthenticationStateData?>> SerializationCallback { get; set; } = SerializeAuthenticationStateAsync;
+ }
namespace Microsoft.Extensions.DependencyInjection;
public static class WebAssemblyRazorComponentsBuilderExtensions
{
public static IRazorComponentsBuilder AddInteractiveWebAssemblyComponents(this IRazorComponentsBuilder builder);
+ public static IRazorComponentsBuilder AddAuthenticationStateSerialization(this IRazorComponentsBuilder builder, Action<AuthenticationStateSerializationOptions>? configure = null)
} // Microsoft.AspNetCore.Components.WebAssembly.Authentication.dll
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication;
+ public sealed class AuthenticationStateDeserializationOptions
+ {
+ public Func<AuthenticationStateData?, Task<AuthenticationState>> DeserializationCallback { get; set; } = DeserializeAuthenticationStateAsync;
+ }
namespace Microsoft.Extensions.DependencyInjection;
public static class WebAssemblyAuthenticationServiceCollectionExtensions
{
// public static IRemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount> AddRemoteAuthentication<...
+ public static IServiceCollection AddAuthenticationStateDeserialization(this IServiceCollection services, Action<AuthenticationStateDeserializationOptions>? configure = null);
} Usage ExamplesThe simplest way to serialize/deserialize all claims is to call This will only serialize only the name and role claims: // Server Program.cs
builder.Services.AddRazorComponents()
.AddInteractiveWebAssemblyComponents()
.AddAuthenticationStateSerialization(); // Client Program.cs
builder.Services.AddAuthorizationCore();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddAuthenticationStateDeserialization(); Alternatively, you could include all claims: // Server Program.cs
builder.Services.AddRazorComponents()
.AddInteractiveWebAssemblyComponents()
.AddAuthenticationStateSerialization(options => options.SerializeAllClaims = true);
// Server Program.cs
builder.Services.AddRazorComponents()
.AddInteractiveWebAssemblyComponents()
.AddAuthenticationStateSerialization(options =>
{
options.SerializationCallback = authenticationState =>
{
AuthenticationStateData authenticationStateData = null;
var identity = authenticationState.User.Identities.First();
if (identity.IsAuthenticated)
{
authenticationStateData = new AuthenticationStateData();
authenticationStateData.NameClaimType = identity.NameClaimType;
authenticationStateData.RoleClaimType = identity.RoleClaimType;
foreach (var claim in identity.Claims)
{
if (_allowedClaims.Contains(claim.Type))
{
authenticationStateData.Claims.Add(new(claim.Type, claim.Value));
}
}
}
return ValueTask.FromResult(authenticationStateData);
};
}); // Client Program.cs
// ...
builder.Services.AddAuthorizationCore();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddAuthenticationStateDeserialization(options =>
{
options.DeserializationCallback = authenticationStateData =>
{
if (authenticationStateData is null)
{
return Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())));
}
var claims = new List<Claim>();
foreach (var kvp in authenticationStateData.Claims)
{
claims.Add(new(kvp.Key, kvp.Value));
}
claims.Add(new("client_only_claim", "custom"));
return Task.FromResult(
new AuthenticationState(new ClaimsPrincipal(
new ClaimsIdentity(claims,
authenticationType: "DeserializationCallback",
nameType: authenticationStateData.NameClaimType,
roleType: authenticationStateData.RoleClaimType))));
};
});
// ... Alternative DesignsWe could come up with better naming for "Serialization" and "Deserialization"? Maybe something involving "transmit" or "initialize". This feature relies on Initially, I wanted to make it possible to turn on this feature with only one line of code on the server. It would have been possible to achieve this by making the call to We could change all references to "AuthenticationState" to "ClaimsPrincipal", but I think referencing AuthenticationState makes it clear this feature is specific to Blazor, and we might be helpful reference the entire AuthenticationState if it gets more properties in the future. Instead of exposing In the previous API review, the possibility of using I did decide to use I did address comment about using an extension method instead of adding a settable boolean to enable the feature. Calling RisksPeople might be surprised that People might also be surprised that if they override the |
The PR has been merged in preview5, but I'm reopening this to finish the API review discussion. We can still make changes in preview6. |
[API Review]
|
// Microsoft.AspNetCore.Components.Server.dll
+ [assembly: TypeForwardedTo(typeof(Microsoft.AspNetCore.Components.Server.ServerAuthenticationStateProvider))]
// Microsoft.AspNetCore.Components.Endpoints.dll
+ namespace Microsoft.AspNetCore.Components.Server;
public sealed class ServerAuthenticationStateProvider : AuthenticationStateProvider, IHostEnvironmentAuthenticationStateProvider // Microsoft.AspNetCore.Components.Authorization.dll
namespace Microsoft.AspNetCore.Components.Authorization;
+ public sealed class AuthenticationStateData
+ {
+ public IList<ClaimData> Claims { get; set; } = [];
+ public string NameClaimType { get; set; } = ClaimsIdentity.DefaultNameClaimType;
+ public string RoleClaimType { get; set; } = ClaimsIdentity.DefaultRoleClaimType;
+ }
+
+ public readonly struct ClaimData(string type, string value)
+ {
+ public ClaimData(Claim claim) { }
+ public string Type => type;
+ public string Value => value;
+ } // Microsoft.AspNetCore.Components.WebAssembly.Server.dll
namespace Microsoft.AspNetCore.Components.WebAssembly.Server;
+ public sealed class AuthenticationStateSerializationOptions
+ {
+ public bool SerializeAllClaims { get; set; }
+ public Func<AuthenticationState, ValueTask<AuthenticationStateData?>> SerializationCallback { get; set; } = SerializeAuthenticationStateAsync;
+ }
namespace Microsoft.Extensions.DependencyInjection;
public static class WebAssemblyRazorComponentsBuilderExtensions
{
public static IRazorComponentsBuilder AddInteractiveWebAssemblyComponents(this IRazorComponentsBuilder builder);
+ public static IRazorComponentsBuilder AddAuthenticationStateSerialization(this IRazorComponentsBuilder builder, Action<AuthenticationStateSerializationOptions>? configure = null)
} // Microsoft.AspNetCore.Components.WebAssembly.Authentication.dll
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication;
+ public sealed class AuthenticationStateDeserializationOptions
+ {
+ public Func<AuthenticationStateData?, Task<AuthenticationState>> DeserializationCallback { get; set; } = DeserializeAuthenticationStateAsync;
+ }
namespace Microsoft.Extensions.DependencyInjection;
public static class WebAssemblyAuthenticationServiceCollectionExtensions
{
// public static IRemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount> AddRemoteAuthentication<...
+ public static IServiceCollection AddAuthenticationStateDeserialization(this IServiceCollection services, Action<AuthenticationStateDeserializationOptions>? configure = null);
} We would really like to drop API approved |
- Addresses API review feedback from #52769
@halter73 What happened with this?
|
The idea was to make If @blowdart thinks someone calling |
I think being specific is better here. And I wish I'd read the design before because I disagree that some of the information in the claims class is superfluous 😕 |
In .NET 9, we should automatically register both server and client
AuthenticationStateProvider
's similar to the ones added by the "Individual Auth" option in the Blazor Web templates.This should make it a lot easier to add auth to a Blazor project that is created without the "Individual Auth" template. As part of this, we will want to figure out which claims are safe to transmit to the clients, and potentially add a callback to configure custom claims.
The text was updated successfully, but these errors were encountered: