diff --git a/.openpublishing.redirection.json b/.openpublishing.redirection.json index 9cfe8ad57d4a..5f4493f0457f 100644 --- a/.openpublishing.redirection.json +++ b/.openpublishing.redirection.json @@ -960,11 +960,6 @@ "redirect_url": "/aspnet/core/tutorials/web-api-javascript", "redirect_document_id": false }, - { - "source_path": "aspnetcore/migration/index.md", - "redirect_url": "/aspnet/core/migration/fx-to-core/", - "redirect_document_id": false - }, { "source_path": "aspnetcore/migration/proper-to-2x/index.md", "redirect_url": "/aspnet/core/migration/fx-to-core/", diff --git a/aspnetcore/blazor/forms/validation.md b/aspnetcore/blazor/forms/validation.md index 978e280e1800..2dfd86d3c7ff 100644 --- a/aspnetcore/blazor/forms/validation.md +++ b/aspnetcore/blazor/forms/validation.md @@ -5,7 +5,7 @@ description: Learn how to use validation in Blazor forms. monikerRange: '>= aspnetcore-3.1' ms.author: wpickett ms.custom: mvc -ms.date: 11/12/2024 +ms.date: 09/08/2025 uid: blazor/forms/validation --- # ASP.NET Core Blazor forms validation @@ -134,6 +134,12 @@ The compon * [`DataAnnotationsValidator`](https://github.com/dotnet/AspNetCore/blob/main/src/Components/Forms/src/DataAnnotationsValidator.cs) * [`EnableDataAnnotationsValidation`](https://github.com/dotnet/AspNetCore/blob/main/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs) +:::moniker range=">= aspnetcore-10.0" + +For details on validation behavior, see the [`DataAnnotationsValidator` validation behavior](#dataannotationsvalidator-validation-behavior) section. + +:::moniker-end + If you need to enable data annotations validation support for an in code, call with an injected (`@inject IServiceProvider ServiceProvider`) on the . For an advanced example, see the [`NotifyPropertyChangedValidationComponent` component in the ASP.NET Core Blazor framework's `BasicTestApp` (`dotnet/aspnetcore` GitHub repository)](https://github.com/dotnet/aspnetcore/blob/main/src/Components/test/testassets/BasicTestApp/FormsTest/NotifyPropertyChangedValidationComponent.razor). In a production version of the example, replace the `new TestServiceProvider()` argument for the service provider with an injected . [!INCLUDE[](~/includes/aspnetcore-repo-ref-source-links.md)] @@ -1542,18 +1548,25 @@ Using `CustomFieldClassProvider3`: [Class-level validation with `IValidatableObject`](xref:mvc/models/validation#ivalidatableobject) ([API documentation](xref:System.ComponentModel.DataAnnotations.IValidatableObject)) is supported for Blazor form models. validation only executes when the form is submitted and only if all other validation succeeds. +:::moniker range="< aspnetcore-10.0" + ## Blazor data annotations validation package -The [`Microsoft.AspNetCore.Components.DataAnnotations.Validation`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation) is a package that fills validation experience gaps using the component. The package is currently *experimental*. +> [!NOTE] +> The [`Microsoft.AspNetCore.Components.DataAnnotations.Validation` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation) is no longer recommended for apps that target .NET 10 or later. For more information, see the [Nested objects, collection types, and complex types](#nested-objects-collection-types-and-complex-types) section. + +The [`Microsoft.AspNetCore.Components.DataAnnotations.Validation` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation) fills validation experience gaps using the component. The package is currently *experimental*. > [!WARNING] -> The [`Microsoft.AspNetCore.Components.DataAnnotations.Validation`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation) package has a latest version of *release candidate* at [NuGet.org](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation). Continue to use the *experimental* release candidate package at this time. Experimental features are provided for the purpose of exploring feature viability and may not ship in a stable version. Watch the [Announcements GitHub repository](https://github.com/aspnet/Announcements), the [`dotnet/aspnetcore` GitHub repository](https://github.com/dotnet/aspnetcore), or this topic section for further updates. +> The [`Microsoft.AspNetCore.Components.DataAnnotations.Validation` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation) has a latest version of *release candidate* at [NuGet.org](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation). Continue to use the *experimental* release candidate package at this time. Experimental features are provided for the purpose of exploring feature viability and may not ship in a stable version. Watch the [Announcements GitHub repository](https://github.com/aspnet/Announcements), the [`dotnet/aspnetcore` GitHub repository](https://github.com/dotnet/aspnetcore), or this topic section for further updates. + +:::moniker-end :::moniker range="< aspnetcore-6.0" ## `[CompareProperty]` attribute -The doesn't work well with the component because the doesn't associate the validation result with a specific member. This can result in inconsistent behavior between field-level validation and when the entire model is validated on a submit. The [`Microsoft.AspNetCore.Components.DataAnnotations.Validation`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation) *experimental* package introduces an additional validation attribute, `ComparePropertyAttribute`, that works around these limitations. In a Blazor app, `[CompareProperty]` is a direct replacement for the [`[Compare]` attribute](xref:System.ComponentModel.DataAnnotations.CompareAttribute). +The doesn't work well with the component because the doesn't associate the validation result with a specific member. This can result in inconsistent behavior between field-level validation and when the entire model is validated on a submit. The [`Microsoft.AspNetCore.Components.DataAnnotations.Validation` *experimental* package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation) introduces an additional validation attribute, `ComparePropertyAttribute`, that works around these limitations. In a Blazor app, `[CompareProperty]` is a direct replacement for the [`[Compare]` attribute](xref:System.ComponentModel.DataAnnotations.CompareAttribute). :::moniker-end @@ -1638,26 +1651,18 @@ In the following `OrderPage` component, the . However, the only validates top-level properties of the model bound to the form that aren't complex-type properties. - -To validate the bound model's entire object graph, including complex-type properties, use the `ObjectGraphDataAnnotationsValidator` provided by the *experimental* [`Microsoft.AspNetCore.Components.DataAnnotations.Validation`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation) package. - -> [!NOTE] -> The `ObjectGraphDataAnnotationsValidator` isn't compatible with [nested objects and collection types validation](#nested-objects-and-collection-types), but it's capable of validating nested objects and collection types on its own. - :::moniker-end :::moniker range="< aspnetcore-10.0" ## Nested objects, collection types, and complex types -Blazor provides support for validating form input using data annotations with the built-in . However, the only validates top-level properties of the model bound to the form that aren't collection- or complex-type properties. +> [!NOTE] +> For apps targeting .NET 10 or later, we no longer recommend using the [`Microsoft.AspNetCore.Components.DataAnnotations.Validation` *experimental* package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation) and approach described in this section. We recommend using the built-in validation features of the component. -To validate the bound model's entire object graph, including collection- and complex-type properties, use the `ObjectGraphDataAnnotationsValidator` provided by the *experimental* [`Microsoft.AspNetCore.Components.DataAnnotations.Validation`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation) package: +Blazor provides support for validating form input using data annotations with the built-in . However, the in .NET 9 or earlier only validates top-level properties of the model bound to the form that aren't collection- or complex-type properties. -:::moniker-end +To validate the bound model's entire object graph, including collection- and complex-type properties, use the `ObjectGraphDataAnnotationsValidator` provided by the *experimental* [`Microsoft.AspNetCore.Components.DataAnnotations.Validation` package](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.DataAnnotations.Validation) in .NET 9 or earlier: ```razor @@ -1703,6 +1708,8 @@ public class ShipDescription } ``` +:::moniker-end + ## Enable the submit button based on form validation To enable and disable the submit button based on form validation, the following example: @@ -1829,3 +1836,17 @@ A side effect of the preceding approach is that a validation summary ( component has the same validation order and short-circuiting behavior as . The following rules are applied when validating an instance of type `T`: + +1. Member properties of `T` are validated, including recursively validating nested objects. +1. Type-level attributes of `T` are validated. +1. The method is executed, if `T` implements it. + +If one of the preceding steps produces a validation error, the remaining steps are skipped. + +:::moniker-end diff --git a/aspnetcore/blazor/fundamentals/routing.md b/aspnetcore/blazor/fundamentals/routing.md index cee45dab7dba..977b545e104e 100644 --- a/aspnetcore/blazor/fundamentals/routing.md +++ b/aspnetcore/blazor/fundamentals/routing.md @@ -5,7 +5,7 @@ description: Learn how to manage Blazor app request routing and how to use the N monikerRange: '>= aspnetcore-3.1' ms.author: wpickett ms.custom: mvc -ms.date: 11/12/2024 +ms.date: 09/08/2025 uid: blazor/fundamentals/routing --- # ASP.NET Core Blazor routing and navigation @@ -704,6 +704,31 @@ The following component: For more information on component disposal, see . +:::moniker range=">= aspnetcore-9.0" + +## Navigation Manager redirect behavior during static server-side rendering (static SSR) + +For a redirect during static server-side rendering (static SSR), relies on throwing a that gets captured by the framework, which converts the error into a redirect. Code that exists after the call to isn't called. When using Visual Studio, the debugger breaks on the exception, requiring you to deselect the checkbox for **Break when this exception type is user-handled** in the Visual Studio UI to avoid the debugger stopping for future redirects. + +:::moniker-end + +:::moniker range=">= aspnetcore-10.0" + +You can use the `` MSBuild property set to `true` in the app's project file to opt-in to no longer throwing a . Also, code after the call to executes when it wouldn't have run before. This behavior is enabled by default in the .NET 10 or later Blazor Web App project template: + +```xml +true +``` + +:::moniker-end + +:::moniker range=">= aspnetcore-9.0 < aspnetcore-10.0" + +> [!NOTE] +> In .NET 10 or later, you can opt-in to not throwing a by setting the `` MSBuild property to `true` in the app's project file. To take advantage of the new MSBuild property and behavior, upgrade the app to .NET 10 or later. + +:::moniker-end + :::moniker range=">= aspnetcore-10.0" ## Not Found responses diff --git a/aspnetcore/blazor/host-and-deploy/configure-trimmer.md b/aspnetcore/blazor/host-and-deploy/configure-trimmer.md index e369c66d8a72..f535c74a69fd 100644 --- a/aspnetcore/blazor/host-and-deploy/configure-trimmer.md +++ b/aspnetcore/blazor/host-and-deploy/configure-trimmer.md @@ -5,7 +5,7 @@ description: Learn how to control the Intermediate Language (IL) Trimmer when bu monikerRange: '>= aspnetcore-5.0' ms.author: wpickett ms.custom: mvc -ms.date: 11/12/2024 +ms.date: 09/08/2025 uid: blazor/host-and-deploy/configure-trimmer --- # Configure the Trimmer for ASP.NET Core Blazor @@ -42,58 +42,107 @@ For more information, see [Trimming options (.NET documentation)](/dotnet/core/d ## Failure to preserve types used by a published app -Trimming may have detrimental effects for a published app leading to runtime errors. In apps that use [reflection](/dotnet/csharp/advanced-topics/reflection-and-attributes/), the IL Trimmer often can't determine the required types for runtime reflection and trims them away or trims away parameter names from methods. This can happen with complex framework types used for JS interop, JSON serialization/deserialization, and other operations. +Trimming may have detrimental effects for a published app leading to runtime errors, even in spite of setting the [`` property](#configuration) to `false` in the project file. In apps that use [reflection](/dotnet/csharp/advanced-topics/reflection-and-attributes/), the IL Trimmer often can't determine the required types for runtime reflection and trims them away or trims away parameter names from methods. This can happen with complex framework types used for JS interop, JSON serialization/deserialization, and other operations. The IL Trimmer is also unable to react to an app's dynamic behavior at runtime. To ensure the trimmed app works correctly once deployed, test published output frequently while developing. -Consider the following client-side component in a Blazor Web App (.NET 8 or later) that deserializes a collection (`List>`): +Consider the following example that performs JSON deserialization into a collection (`List>`). + +`TrimExample.razor`: ```razor -@rendermode @(new InteractiveWebAssemblyRenderMode(false)) +@page "/trim-example" @using System.Diagnostics.CodeAnalysis @using System.Text.Json -
+

Trim Example

+ +
    @foreach (var item in @items) { -
    @item.Key
    -
    @item.Value
    +
  • @item.Item1, @item.Item2
  • } -
+ @code { - private List> items = []; + private List> items = []; [StringSyntax(StringSyntaxAttribute.Json)] private const string data = - """[{"key":"key 1","value":"value 1"},{"key":"key 2","value":"value 2"}]"""; + """[{"item1":"1:T1","item2":"1:T2"},{"item1":"2:T1","item2":"2:T2"}]"""; protected override void OnInitialized() { JsonSerializerOptions options = new() { PropertyNameCaseInsensitive = true }; items = JsonSerializer - .Deserialize>>(data, options)!; + .Deserialize>>(data, options)!; } } ``` -The preceding component executes normally when the app is run locally and produces the following rendered definition list (`
`): +The preceding component executes normally when the app is run locally and produces the following rendered list: + +> • 1:T1, 1:T2 +> • 2:T2, 2:T2 + +When the app is published, is trimmed from the app, even in spite of setting the `` property to `false` in the project file. Accessing the component throws the following exception: + +> :::no-loc text="crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]"::: +> :::no-loc text="Unhandled exception rendering component: ConstructorContainsNullParameterNames, System.Tuple`2[System.String,System.String]"::: +> :::no-loc text="System.NotSupportedException: ConstructorContainsNullParameterNames, System.Tuple`2[System.String,System.String]"::: + +To address lost types, consider adopting one of the following approaches. + +### Custom types + +Custom types aren't trimmed by Blazor when an app is published, so we recommend using custom types for JS interop, JSON serialization/deserialization, and other operations that rely on reflection. + +The following modifications create a `StringTuple` type for use by the component. + +`StringTuple.cs`: + +```csharp +[method: SetsRequiredMembers] +public sealed class StringTuple(string item1, string item2) +{ + public required string Item1 { get; init; } = item1; + public required string Item2 { get; init; } = item2; +} +``` + +The component is modified to use the `StringTuple` type: + +```diff +- private List> items = []; ++ private List items = []; +``` + +```diff +- items = JsonSerializer.Deserialize>>(data, options)!; ++ items = JsonSerializer.Deserialize>(data, options)!; +``` + +Because custom types are never trimmed by Blazor when an app is published, the component works as designed after the app is published. + +:::moniker range=">= aspnetcore-10.0" -> **:::no-loc text="key 1":::** -> :::no-loc text="value 1"::: -> **:::no-loc text="key 2":::** -> :::no-loc text="value 2"::: +If you prefer to use framework types in spite of our recommendation, use either of the following approaches: -When the app is published, is trimmed from the app, even in spite of setting the [`` property](#configuration) to `false` in the project file. Accessing the component throws the following exception: +* [Preserve the type as a dynamic dependency](#preserve-the-type-as-a-dynamic-dependency) +* [Use a Root Descriptor](#use-a-root-descriptor) -> :::no-loc text="Unhandled exception rendering component: ConstructorContainsNullParameterNames, System.Collections.Generic.KeyValuePair`2[System.String,System.String]"::: +:::moniker-end -To address lost types, consider the following approaches. +:::moniker range="< aspnetcore-10.0" + +If you prefer to use framework types in spite of our recommendation, [preserve the type as a dynamic dependency](#preserve-the-type-as-a-dynamic-dependency). + +:::moniker-end ### Preserve the type as a dynamic dependency -We recommend creating a dynamic dependency to preserve the type with the [`[DynamicDependency]` attribute](xref:System.Diagnostics.CodeAnalysis.DynamicDependencyAttribute). +Create a dynamic dependency to preserve the type with the [`[DynamicDependency]` attribute](xref:System.Diagnostics.CodeAnalysis.DynamicDependencyAttribute). If not already present, add an `@using` directive for : @@ -101,14 +150,15 @@ If not already present, add an `@using` directive for : +Add a [`[DynamicDependency]` attribute](xref:System.Diagnostics.CodeAnalysis.DynamicDependencyAttribute) to preserve the : ```diff -+ [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors, typeof(KeyValuePair))] -private List> items = []; ++ [DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors, ++ typeof(Tuple))] +private List> items = []; ``` - - -### Custom types - - - -The following modifications create a `StringKeyValuePair` type for use by the component. - -`StringKeyValuePair.cs`: +:::moniker-end -```csharp -[method: SetsRequiredMembers] -public sealed class StringKeyValuePair(string key, string value) -{ - public required string Key { get; init; } = key; - public required string Value { get; init; } = value; -} -``` +:::moniker range="= aspnetcore-8.0" -The component is modified to use the `StringKeyValuePair` type: +### Workaround in .NET 8 -```diff -- private List> items = []; -+ private List items = []; -``` +As a workaround in .NET 8, you can add the `_ExtraTrimmerArgs` MSBuild property set to `--keep-metadata parametername` in the app's project file to preserve parameter names during trimming: -```diff -- items = JsonSerializer.Deserialize>>(data, options)!; -+ items = JsonSerializer.Deserialize>(data, options)!; +```xml + + <_ExtraTrimmerArgs>--keep-metadata parametername + ``` -Because custom types are never trimmed by Blazor when an app is published, the component works as designed after the app is published. +:::moniker-end ## Additional resources diff --git a/aspnetcore/blazor/hybrid/security/maui-blazor-web-identity.md b/aspnetcore/blazor/hybrid/security/maui-blazor-web-identity.md index 6cde47ffd258..f4731f0ece28 100644 --- a/aspnetcore/blazor/hybrid/security/maui-blazor-web-identity.md +++ b/aspnetcore/blazor/hybrid/security/maui-blazor-web-identity.md @@ -117,7 +117,7 @@ app.MapAdditionalIdentityEndpoints(); ``` > [!IMPORTANT] -> ASP.NET Core Identity pages and the implementation of the framework class to manage logins and users is generated automatically when you create a a project from the Blazor Web App project template with **Individual Accounts**. +> ASP.NET Core Identity pages and the implementation of the framework class to manage logins and users is generated automatically when you create a project from the Blazor Web App project template with **Individual Accounts**. > > This article focuses on using the provided sample app; but when creating a new project from the Blazor Web App template, you must remove the generated call to on . The call isn't necessary when implementing API such as `MapAdditionalIdentityEndpoints` in the sample app and results in an error if left in the app. diff --git a/aspnetcore/blazor/javascript-interoperability/index.md b/aspnetcore/blazor/javascript-interoperability/index.md index 297b16dd8aed..30f0f22f5fbf 100644 --- a/aspnetcore/blazor/javascript-interoperability/index.md +++ b/aspnetcore/blazor/javascript-interoperability/index.md @@ -202,7 +202,7 @@ Blazor uses for serialization w * Global default serialization isn't customizable to avoid breaking existing component libraries, impacts on performance and security, and reductions in reliability. * Serializing .NET member names results in lowercase JSON key names. * JSON is deserialized as C# instances, which permit mixed casing. Internal casting for assignment to C# model properties works as expected in spite of any case differences between JSON key names and C# property names. -* Complex framework types, such as , might be [trimmed away by the IL Trimmer on publish](xref:blazor/host-and-deploy/configure-trimmer#failure-to-preserve-types-used-by-a-published-app) and not present for JS interop or JSON serialization/deserialization. We recommend creating custom types for types that the IL Trimmer trims away. +* Complex framework types might be [trimmed away by the IL Trimmer on publish](xref:blazor/host-and-deploy/configure-trimmer#failure-to-preserve-types-used-by-a-published-app) and not present for JS interop or JSON serialization/deserialization. We recommend creating custom types for types that the IL Trimmer trims away. * Blazor always relies on [reflection for JSON serialization](/dotnet/standard/serialization/system-text-json/reflection-vs-source-generation), including when using C# [source generation](/dotnet/csharp/roslyn-sdk/source-generators-overview). Setting `JsonSerializerIsReflectionEnabledByDefault` to `false` in the app's project file results in an error when serialization is attempted. API is available for custom serialization. Properties can be annotated with a [`[JsonConverter]` attribute](xref:System.Text.Json.Serialization.JsonConverterAttribute) to override default serialization for an existing data type. diff --git a/aspnetcore/blazor/progressive-web-app/push-notifications.md b/aspnetcore/blazor/progressive-web-app/push-notifications.md index a767e8f0f97f..20dee812a328 100644 --- a/aspnetcore/blazor/progressive-web-app/push-notifications.md +++ b/aspnetcore/blazor/progressive-web-app/push-notifications.md @@ -291,7 +291,7 @@ private static async Task SendNotificationAsync(Order order, The preceding example enables the server to send notifications, but the browser doesn't react to notifications without additional logic. Displaying notifications is covered in the [Display notifications](#display-notifications) section. -The browser's developer tools console indicates the arrival of notifications ten seconds after orders are placed in the Blazing Pizza app. On the **Application** tab, open the **Push Messaging** section. Select the the circle to **Start recording**: +The browser's developer tools console indicates the arrival of notifications ten seconds after orders are placed in the Blazing Pizza app. On the **Application** tab, open the **Push Messaging** section. Select the circle to **Start recording**: ![Browser developer tools console on the "Application" tab with "Push Messaging" open showing three push notifications to the app with their timestamps.](~/blazor/progressive-web-app/push-notifications/_static/developer-tools-console-notifications.png) diff --git a/aspnetcore/blazor/state-management/prerendered-state-persistence.md b/aspnetcore/blazor/state-management/prerendered-state-persistence.md index 6114ad55b287..14228127d395 100644 --- a/aspnetcore/blazor/state-management/prerendered-state-persistence.md +++ b/aspnetcore/blazor/state-management/prerendered-state-persistence.md @@ -5,7 +5,7 @@ description: Learn how to persist user data (state) in Blazor apps using Blazor' monikerRange: '>= aspnetcore-8.0' ms.author: wpickett ms.custom: mvc -ms.date: 08/05/2025 +ms.date: 09/08/2025 uid: blazor/state-management/prerendered-state-persistence --- # ASP.NET Core Blazor prerendered state persistence @@ -312,13 +312,47 @@ For components embedded into a page or view of a Razor Pages or MVC app, you mus ## Interactive routing and prerendering + + When the `Routes` component doesn't define a render mode, the app is using per-page/component interactivity and navigation. Using per-page/component navigation, internal navigation is handled by [enhanced routing](xref:blazor/fundamentals/routing#enhanced-navigation-and-form-handling) after the app becomes interactive. "Internal navigation" in this context means that the URL destination of the navigation event is a Blazor endpoint inside the app. - +:::moniker range=">= aspnetcore-10.0" + +Blazor supports handling persistent component state during [enhanced navigation](xref:blazor/fundamentals/routing#enhanced-navigation-and-form-handling). State persisted during enhanced navigation can be read by interactive components on the page. + +By default, persistent component state is only loaded by interactive components when they're initially loaded on the page. This prevents important state, such as data in an edited webform, from being overwritten if additional enhanced navigation events to the same page occur after the component is loaded. + +If the data is read-only and doesn't change frequently, opt-in to allow updates during enhanced navigation by setting `AllowUpdates = true` on the [`[PersistentState]` attribute](xref:Microsoft.AspNetCore.Components.PersistentStateAttribute). This is useful for scenarios such as displaying cached data that's expensive to fetch but doesn't change often. The following example demonstrates the use of `AllowUpdates` for weather forecast data: + +```csharp +[PersistentState(AllowUpdates = true)] +public WeatherForecast[]? Forecasts { get; set; } + +protected override async Task OnInitializedAsync() +{ +    Forecasts ??= await ForecastService.GetForecastAsync(); +} +``` + +To skip restoring state during prerendering, set `RestoreBehavior` to `SkipInitialValue`: + +```csharp +[PersistentState(RestoreBehavior = RestoreBehavior.SkipInitialValue)] +public string NoPrerenderedData { get; set; } +``` + +To skip restoring state during reconnection, set `RestoreBehavior` to `SkipLastSnapshot`. This can be useful to ensure fresh data after reconnection: + +```csharp +[PersistentState(RestoreBehavior = RestoreBehavior.SkipLastSnapshot)] +public int CounterNotRestoredOnReconnect { get; set; } +``` + +Call `PersistentComponentState.RegisterOnRestoring` to register a callback for imperatively controlling how state is restored, similar to how provides full control of how state is persisted. + +:::moniker-end + +:::moniker range="< aspnetcore-10.0" The service only works on the initial page load and not across internal enhanced page navigation events. @@ -326,4 +360,6 @@ If the app performs a full (non-enhanced) navigation to a page utilizing persist If an interactive circuit has already been established and an enhanced navigation is performed to a page utilizing persistent component state, the state *isn't made available in the existing circuit for the component to use*. There's no prerendering for the internal page request, and the service isn't aware that an enhanced navigation has occurred. There's no mechanism to deliver state updates to components that are already running on an existing circuit. The reason for this is that Blazor only supports passing state from the server to the client at the time the runtime initializes, not after the runtime has started. -Disabling enhanced navigation, which reduces performance but also avoids the problem of loading state with for internal page requests, is covered in . +Disabling enhanced navigation, which reduces performance but also avoids the problem of loading state with for internal page requests, is covered in . Alternatively, update the app to .NET 10 or later, where Blazor supports handling persistent component state when during enhanced navigation. + +:::moniker-end diff --git a/aspnetcore/fundamentals/minimal-apis.md b/aspnetcore/fundamentals/minimal-apis.md index 9c2a86ff3291..c2089186bdab 100644 --- a/aspnetcore/fundamentals/minimal-apis.md +++ b/aspnetcore/fundamentals/minimal-apis.md @@ -1,11 +1,11 @@ --- title: Minimal APIs quick reference author: wadepickett -description: Provides an overview of minimal APIs in ASP.NET Core +description: Provides an overview of Minimal APIs in ASP.NET Core ms.author: wpickett content_well_notification: AI-contribution monikerRange: '>= aspnetcore-6.0' -ms.date: 08/22/2025 +ms.date: 09/08/2025 uid: fundamentals/minimal-apis ai-usage: ai-assisted --- @@ -20,19 +20,19 @@ ai-usage: ai-assisted This document: -* Provides a quick reference for minimal APIs. +* Provides a quick reference for Minimal APIs. * Is intended for experienced developers. For an introduction, see . -The minimal APIs consist of: +The Minimal APIs consist of: -* [WebApplication and WebApplicationBuilder](xref:fundamentals/minimal-apis/webapplication) +* [`WebApplication` and `WebApplicationBuilder`](xref:fundamentals/minimal-apis/webapplication) * [Route Handlers](xref:fundamentals/minimal-apis/route-handlers) [!INCLUDE[](~/fundamentals/minimal-apis/includes/webapplication10.md)] ## ASP.NET Core Middleware -The following table lists some of the middleware frequently used with minimal APIs. +The following table lists some of the middleware frequently used with Minimal APIs. | Middleware | Description | API | |--|--|--| @@ -72,7 +72,7 @@ The arguments passed to these methods are called "route h ## Validation support in Minimal APIs -Support for validation in Minimal APIs is now available. This feature allows you to request validation of data sent to your API endpoints. Enabling validation allows the ASP.NET Core runtime to perform any validations defined on the: +Enabling validation allows the ASP.NET Core runtime to perform validations defined on the: * Query * Header @@ -89,20 +89,20 @@ public record Product( ``` Developers customize the behavior of the validation system by: -* Creating custom [`[Validation]`](xref:System.ComponentModel.DataAnnotations.ValidationAttribute) attribute implementations. +* Creating custom [`[Validation]` attribute](xref:System.ComponentModel.DataAnnotations.ValidationAttribute) implementations. * Implementing the [`IValidatableObject`](xref:System.ComponentModel.DataAnnotations.IValidatableObject) interface for complex validation logic. -If validation fails, the runtime returns a 400 Bad Request response with details of the validation errors. +If validation fails, the runtime returns a *400 - Bad Request* response with details of the validation errors. -### Enable built-in validation support for minimal APIs +### Enable built-in validation support for Minimal APIs -Enable the built-in validation support for minimal APIs by calling the `AddValidation` extension method to register the required services in the service container for your application: +Enable the built-in validation support for Minimal APIs by calling the `AddValidation` extension method to register the required services in the service container for your application: ```csharp builder.Services.AddValidation(); ``` -The implementation automatically discovers types that are defined in minimal API handlers or as base types of types defined in minimal API handlers. An endpoint filter performs validation on these types and is added for each endpoint. +The implementation automatically discovers types that are defined in Minimal API handlers or as base types of types defined in Minimal API handlers. An endpoint filter performs validation on these types and is added for each endpoint. Validation can be disabled for specific endpoints by using the `DisableValidation` extension method, as in the following example: @@ -114,7 +114,7 @@ app.MapPost("/products", ``` ### Customize validation error responses using IProblemDetailsService -Customize error responses from minimal API validation logic with an implementation. Register this service in your application's service collection to enable more consistent and user-specific error responses. Support for minimal API validation was introduced in ASP.NET Core in .NET 10. +Customize error responses from Minimal API validation logic with an implementation. Register this service in your application's service collection to enable more consistent and user-specific error responses. Support for Minimal API validation was introduced in ASP.NET Core in .NET 10. To implement custom validation error responses: @@ -253,7 +253,7 @@ We recommend adding an extension method to interface can represent values returned from minimal APIs that don't utilize the implicit support for JSON serializing the returned object to the HTTP response. The static [Results](/dotnet/api/microsoft.aspnetcore.http.results) class is used to create varying `IResult` objects that represent different types of responses. For example, setting the response status code or redirecting to another URL. +The interface can represent values returned from Minimal APIs that don't utilize the implicit support for JSON serializing the returned object to the HTTP response. The static [Results](/dotnet/api/microsoft.aspnetcore.http.results) class is used to create varying `IResult` objects that represent different types of responses. For example, setting the response status code or redirecting to another URL. The types implementing `IResult` are public, allowing for type assertions when testing. For example: @@ -333,7 +333,7 @@ The following code disables `ValidateScopes` and `ValidateOnBuild` in `Developme * [Short-circuit routing](https://andrewlock.net/exploring-the-dotnet-8-preview-short-circuit-routing/) * [Identity API endpoints](https://andrewlock.net/exploring-the-dotnet-8-preview-introducing-the-identity-api-endpoints/) * [Keyed service dependency injection container support](https://andrewlock.net/exploring-the-dotnet-8-preview-keyed-services-dependency-injection-support/) -* [A look behind the scenes of minimal API endpoints](https://andrewlock.net/behind-the-scenes-of-minimal-apis-1-a-first-look-behind-the-scenes-of-minimal-api-endpoints/) +* [A look behind the scenes of Minimal API endpoints](https://andrewlock.net/behind-the-scenes-of-minimal-apis-1-a-first-look-behind-the-scenes-of-minimal-api-endpoints/) * [Organizing ASP.NET Core Minimal APIs](https://www.tessferrandez.com/blog/2023/10/31/organizing-minimal-apis.html) * [Fluent validation discussion on GitHub](https://github.com/dotnet/aspnetcore/issues/51834#issuecomment-1837180853) diff --git a/aspnetcore/migration/80-90.md b/aspnetcore/migration/80-90.md index 4f65cd6ead46..1f9a1fbb6dcc 100644 --- a/aspnetcore/migration/80-90.md +++ b/aspnetcore/migration/80-90.md @@ -1,16 +1,15 @@ --- title: Migrate from ASP.NET Core in .NET 8 to ASP.NET Core in .NET 9 author: wadepickett -description: Learn how to migrate an ASP.NET Core in .NET 8 to ASP.NET Core in .NET 9 +description: Learn how to migrate an ASP.NET Core in .NET 8 to ASP.NET Core in .NET 9. ms.author: wpickett ms.date: 2/11/2024 uid: migration/80-to-90 --- - - - # Migrate from ASP.NET Core in .NET 8 to ASP.NET Core in .NET 9 + + This article explains how to update an ASP.NET Core in .NET 8 to ASP.NET Core in .NET 9. ## Prerequisites diff --git a/aspnetcore/migration/90-to-100.md b/aspnetcore/migration/90-to-100.md new file mode 100644 index 000000000000..5e47dad2d25b --- /dev/null +++ b/aspnetcore/migration/90-to-100.md @@ -0,0 +1,78 @@ +--- +title: Migrate from ASP.NET Core in .NET 9 to ASP.NET Core in .NET 10 +author: wadepickett +description: Learn how to migrate an ASP.NET Core in .NET 9 to ASP.NET Core in .NET 10. +ms.author: wpickett +ms.date: 8/14/2025 +uid: migration/90-to-100 +--- +# Migrate from ASP.NET Core in .NET 9 to ASP.NET Core in .NET 10 + + + +This article explains how to update an ASP.NET Core in .NET 9 to ASP.NET Core in .NET 10. + +## Prerequisites + +# [Visual Studio](#tab/visual-studio) + +[!INCLUDE[](~/includes/net-prereqs-vs-10-latest.md)] + +# [Visual Studio Code](#tab/visual-studio-code) + +[!INCLUDE[](~/includes/net-prereqs-vsc-10.0.md)] + +--- + +## Update the .NET SDK version in `global.json` + +If you rely on a [`global.json`](/dotnet/core/tools/global-json) file to target a specific .NET SDK version, update the `version` property to the .NET 10 SDK version that's installed. For example: + +```diff +{ + "sdk": { +- "version": "9.0.304" ++ "version": "10.0.100" + } +} +``` + +## Update the target framework + +Update the project file's [Target Framework Moniker (TFM)](/dotnet/standard/frameworks) to `net10.0`: + +```diff + + + +- net9.0 ++ net10.0 + + + +``` + +## Update package references + +In the project file, update each [`Microsoft.AspNetCore.*`](https://www.nuget.org/packages?q=Microsoft.AspNetCore.*), [`Microsoft.EntityFrameworkCore.*`](https://www.nuget.org/packages?q=Microsoft.EntityFrameworkCore.*), [`Microsoft.Extensions.*`](https://www.nuget.org/packages?q=Microsoft.Extensions.*), and [`System.Net.Http.Json`](https://www.nuget.org/packages/System.Net.Http.Json) package reference's `Version` attribute to 10.0.0 or later. For example: + +```diff + +- +- +- +- ++ ++ ++ ++ + +``` + +## Blazor + +[!INCLUDE[](~/migration/90-to-100/includes/blazor.md)] + +## Breaking changes + +Use the articles in [Breaking changes in .NET](/dotnet/core/compatibility/breaking-changes) to find breaking changes that might apply when upgrading an app to a newer version of .NET. diff --git a/aspnetcore/migration/90-to-100/includes/blazor.md b/aspnetcore/migration/90-to-100/includes/blazor.md new file mode 100644 index 000000000000..bcea105f933a --- /dev/null +++ b/aspnetcore/migration/90-to-100/includes/blazor.md @@ -0,0 +1,26 @@ +Complete migration coverage for Blazor apps is scheduled for September and October of 2025. + +### Adopt passkey user authentication in an existing Blazor Web App + +For guidance, see . + +### When navigation errors are disabled in a Blazor Web App with Individual Accounts + +*This section applies to Blazor Web Apps that set the `` MSBuild property to `true` in order to avoid throwing an navigation exception during static server-side rendering (static SSR).* + +The `IdentityRedirectManager` threw an in the `RedirectTo` method to ensure the method wasn't called from an interactive render mode and all the redirection methods were marked with the [`[DoesNotReturn]` attribute](xref:System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute). The .NET 10 or later Blazor Web App project template sets the `` MSBuild property to `true` in the app's project file in order to avoid throwing the exception during static SSR. If an app based on the project template from a prior release of .NET is updated to .NET 10 or later and includes the `` MSBuild property set to `true`, make the following changes. For more information, see . + +In `Components/Account/IdentityRedirectManager.cs`: + +* Remove the from the `RedirectTo` method: + + ```diff + - throw new InvalidOperationException( + - $"{nameof(IdentityRedirectManager)} can only be used during static rendering."); + ``` + +* Remove five instances of the `[DoesNotReturn]` attribute from the file: + + ```diff + - [DoesNotReturn] + ``` diff --git a/aspnetcore/migration/index.md b/aspnetcore/migration/index.md new file mode 100644 index 000000000000..023107574b65 --- /dev/null +++ b/aspnetcore/migration/index.md @@ -0,0 +1,11 @@ +--- +title: Migrate an ASP.NET Core app +author: wadepickett +description: Learn how to migrate an ASP.NET Core app. +ms.author: wpickett +ms.date: 8/19/2025 +uid: migration/index +--- +# Migrate an ASP.NET Core app + +Use the guidance in this node to migrate an ASP.NET Core app. diff --git a/aspnetcore/mvc/models/validation.md b/aspnetcore/mvc/models/validation.md index a29e3fb61e16..c03c7eac3e0b 100644 --- a/aspnetcore/mvc/models/validation.md +++ b/aspnetcore/mvc/models/validation.md @@ -44,21 +44,7 @@ Validation attributes let you specify validation rules for model properties. The ## Built-in attributes -Here are some of the built-in validation attributes: - -* [[ValidateNever]](xref:Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidateNeverAttribute): Indicates that a property or parameter should be excluded from validation. -* [[CreditCard]](xref:System.ComponentModel.DataAnnotations.CreditCardAttribute): Validates that the property has a credit card format. Requires [jQuery Validation Additional Methods](https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.1/additional-methods.js). -* [[Compare]](xref:System.ComponentModel.DataAnnotations.CompareAttribute): Validates that two properties in a model match. -* [[EmailAddress]](xref:System.ComponentModel.DataAnnotations.EmailAddressAttribute): Validates that the property has an email format. -* [[Phone]](xref:System.ComponentModel.DataAnnotations.PhoneAttribute): Validates that the property has a telephone number format. -* [[Range]](xref:System.ComponentModel.DataAnnotations.RangeAttribute): Validates that the property value falls within a specified range. -* [[RegularExpression]](xref:System.ComponentModel.DataAnnotations.RegularExpressionAttribute): Validates that the property value matches a specified regular expression. -* [[Required]](xref:System.ComponentModel.DataAnnotations.RequiredAttribute): Validates that the field isn't null. See [`[Required]` attribute](#non-nullable-reference-types-and-required-attribute) for details about this attribute's behavior. -* [[StringLength]](xref:System.ComponentModel.DataAnnotations.StringLengthAttribute): Validates that a string property value doesn't exceed a specified length limit. -* [[Url]](xref:System.ComponentModel.DataAnnotations.UrlAttribute): Validates that the property has a URL format. -* [[Remote]](xref:Microsoft.AspNetCore.Mvc.RemoteAttribute): Validates input on the client by calling an action method on the server. See [`[Remote]` attribute](#remote-attribute) for details about this attribute's behavior. - -A complete list of validation attributes can be found in the namespace. +For a complete list of validation attributes, see the namespace. ### Error messages diff --git a/aspnetcore/release-notes/aspnetcore-10.0.md b/aspnetcore/release-notes/aspnetcore-10.0.md index c9f98af78c6d..b5d6d1eecf20 100644 --- a/aspnetcore/release-notes/aspnetcore-10.0.md +++ b/aspnetcore/release-notes/aspnetcore-10.0.md @@ -4,7 +4,7 @@ author: wadepickett description: Learn about the new features in ASP.NET Core in .NET 10. ms.author: wpickett ms.custom: mvc -ms.date: 08/14/2025 +ms.date: 8/14/2025 uid: aspnetcore-10 --- # What's new in ASP.NET Core in .NET 10 @@ -90,6 +90,8 @@ The following image shows an example of the Authenticated request duration metri For more information, see . +[!INCLUDE[](~/release-notes/aspnetcore-10/includes/identity-metrics.md)] + [!INCLUDE[](~/release-notes/aspnetcore-10/includes/avoid-cookie-login-redirects.md)] ## Miscellaneous diff --git a/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md b/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md index e984b4222ce4..57d917eb7d4d 100644 --- a/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md +++ b/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md @@ -372,21 +372,23 @@ In Blazor Web Apps, framework static assets are automatically preloaded using [` For more information, see . -### `NavigationManager.NavigateTo` no longer throws a `NavigationException` +### Opt-in to avoiding a `NavigationException` during static server-side rendering with `NavigationManager.NavigateTo` -Previously, calling during static server-side rendering (SSR) would throw a , interrupting execution before being converted to a redirection response. This caused confusion during debugging and was inconsistent with interactive rendering, where code after continues to execute normally. +Calling during static server-side rendering (static SSR) throws a , interrupting execution before being converted to a redirection response. This can cause confusion during debugging and is inconsistent with interactive rendering behavior, where code after continues to execute normally. -Calling during static SSR no longer throws a . Instead, it behaves consistently with interactive rendering by performing the navigation without throwing an exception. +In .NET 10, you can set the `` MSBuild property to `true` in the app's project file in order to avoid throwing the exception during static SSR: -Code that relied on being thrown should be updated. For example, in the default Blazor Identity UI, the `IdentityRedirectManager` previously threw an after calling `RedirectTo` to ensure it wasn't invoked during interactive rendering. This exception and the [`[DoesNotReturn]` attributes](xref:System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute) should now be removed. +```xml + + true + +``` -To revert to the previous behavior of throwing a , set the following switch: +With the MSBuild property set, calling during static SSR no longer throws a . Instead, it behaves consistently with interactive rendering by performing the navigation without throwing an exception. Code after executes before the redirection occurs. -```csharp -AppContext.SetSwitch( - "Microsoft.AspNetCore.Components.Endpoints.NavigationManager.DisableThrowNavigationException", - isEnabled: false); -``` +The .NET 10 Blazor Web App project template sets the MSBuild property to `true` by default. We recommend that apps updating to .NET 10 use the new MSBuild property and avoid the prior behavior. + +If the MSBuild property is used, code that relied on being thrown should be updated. In the default Blazor Identity UI of the Blazor Web App project template before the release of .NET 10, the `IdentityRedirectManager` throws an after calling `RedirectTo` to ensure that the method wasn't invoked during interactive rendering. This exception and the [`[DoesNotReturn]` attributes](xref:System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute) should now be removed when the MSBuild property is used. For more information, see . ### Blazor router has a `NotFoundPage` parameter @@ -587,6 +589,21 @@ In the following `OrderPage` component, the implementation. + * The `[SkipValidation]` attribute can exclude properties or types from validation. +* Validation now uses a source generator-based implementation instead of reflection-based implementation for improved performance and compatibility with ahead-of-time (AOT) compilation. + +The component now has the same validation order and short-circuiting behavior as . The following rules are applied when validating an instance of type `T`: + +1. Member properties of `T` are validated, including recursively validating nested objects. +1. Type-level attributes of `T` are validated. +1. The method is executed, if `T` implements it. + +If one of the preceding steps produces a validation error, the remaining steps are skipped. + ### Custom Blazor cache and `BlazorCacheBootResources` MSBuild property removed Now that all Blazor client-side files are fingerprinted and cached by the browser, Blazor's custom caching mechanism and the `BlazorCacheBootResources` MSBuild property have been removed from the framework. If the client-side project's project file contains the MSBuild property, remove the property, as it no longer has any effect: @@ -601,13 +618,12 @@ For more information, see +* ### Circuit state persistence @@ -709,3 +725,37 @@ In the following example, a hidden input field is created for the form's `Parame private void Submit() => submitted = true; } ``` + +### Persistent component state support for enhanced navigation + +Blazor now supports handling persistent component state during [enhanced navigation](xref:blazor/fundamentals/routing#enhanced-navigation-and-form-handling). State persisted during enhanced navigation can be read by interactive components on the page. + +By default, persistent component state is only loaded by interactive components when they're initially loaded on the page. This prevents important state, such as data in an edited webform, from being overwritten if additional enhanced navigation events to the same page occur after the component is loaded. + +If the data is read-only and doesn't change frequently, opt-in to allow updates during enhanced navigation by setting `AllowUpdates = true` on the [`[PersistentState]` attribute](xref:Microsoft.AspNetCore.Components.PersistentStateAttribute). This is useful for scenarios such as displaying cached data that's expensive to fetch but doesn't change often. The following example demonstrates the use of `AllowUpdates` for weather forecast data: + +```csharp +[PersistentState(AllowUpdates = true)] +public WeatherForecast[]? Forecasts { get; set; } + +protected override async Task OnInitializedAsync() +{ +    Forecasts ??= await ForecastService.GetForecastAsync(); +} +``` + +To skip restoring state during prerendering, set `RestoreBehavior` to `SkipInitialValue`: + +```csharp +[PersistentState(RestoreBehavior = RestoreBehavior.SkipInitialValue)] +public string NoPrerenderedData { get; set; } +``` + +To skip restoring state during reconnection, set `RestoreBehavior` to `SkipLastSnapshot`. This can be useful to ensure fresh data after reconnection: + +```csharp +[PersistentState(RestoreBehavior = RestoreBehavior.SkipLastSnapshot)] +public int CounterNotRestoredOnReconnect { get; set; } +``` + +Call `PersistentComponentState.RegisterOnRestoring` to register a callback for imperatively controlling how state is restored, similar to how provides full control of how state is persisted. diff --git a/aspnetcore/release-notes/aspnetcore-10/includes/identity-metrics.md b/aspnetcore/release-notes/aspnetcore-10/includes/identity-metrics.md new file mode 100644 index 000000000000..b32afa1b5652 --- /dev/null +++ b/aspnetcore/release-notes/aspnetcore-10/includes/identity-metrics.md @@ -0,0 +1,25 @@ +### ASP.NET Core Identity metrics + +[ASP.NET Core Identity](xref:security/authentication/identity) observability has been improved in .NET 10 with metrics. Metrics are counters, histograms, and gauges that provide time-series measurements of system or application behavior. + +For example, use the new ASP.NET Core Identity metrics to observe: + +* **User management**: New user creations, password changes, and role assignments. +* **Login/session handling**: Login attempts, sign ins, sign outs, and users using two factor authentication. + +The new metrics are in the `Microsoft.AspNetCore.Identity` meter: + +* `aspnetcore.identity.user.create.duration` +* `aspnetcore.identity.user.update.duration` +* `aspnetcore.identity.user.delete.duration` +* `aspnetcore.identity.user.check_password_attempts` +* `aspnetcore.identity.user.generated_tokens` +* `aspnetcore.identity.user.verify_token_attempts` +* `aspnetcore.identity.sign_in.authenticate.duration` +* `aspnetcore.identity.sign_in.check_password_attempts` +* `aspnetcore.identity.sign_in.sign_ins` +* `aspnetcore.identity.sign_in.sign_outs` +* `aspnetcore.identity.sign_in.two_factor_clients_remembered` +* `aspnetcore.identity.sign_in.two_factor_clients_forgotten` + +For more information about using metrics in ASP.NET Core, see . diff --git a/aspnetcore/security/authentication/passkeys/blazor.md b/aspnetcore/security/authentication/passkeys/blazor.md new file mode 100644 index 000000000000..32a0f164d842 --- /dev/null +++ b/aspnetcore/security/authentication/passkeys/blazor.md @@ -0,0 +1,285 @@ +--- +title: Implement passkeys in ASP.NET Core Blazor Web Apps +author: guardrex +description: Learn how to implement passkeys authentication in ASP.NET Core Blazor Web Apps. +ms.author: wpickett +ms.custom: mvc +ms.date: 09/08/2025 +uid: security/authentication/passkeys/blazor +zone_pivot_groups: implementation +--- +# Implement passkeys in ASP.NET Core Blazor Web Apps + +This guide explains how to implement [passkey support](xref:security/authentication/passkeys/index) for a new or existing Blazor Web App with ASP.NET Core Identity. + +For an overview of passkeys and general configuration guidance, see . + +:::zone pivot="new-development" + +## Prerequisites + + + +[.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) + +## Create a Blazor Web App + +Use the following guidance to create a new Blazor Web App with ASP.NET Core Identity, which includes passkeys support. + +# [Visual Studio](#tab/visual-studio) + +> [!NOTE] +> Visual Studio 2022 or later and .NET 10 or later SDK are required. + +In Visual Studio: + +* Select **Create a new project** from the **Start Window** or select **File** > **New** > **Project** from the menu bar. +* In the **Create a new project** dialog, select **Blazor Web App** from the list of project templates. Select the **Next** button. +* In the **Configure your new project** dialog, name the project `BlazorWebAppPasskeys` in the **Project name** field, including matching the capitalization. Using this exact project name is important to ensure that the namespaces match for code that you copy from the article into the app that you're building. +* Confirm that the **Location** for the app is suitable. Leave the **Place solution and project in the same directory** checkbox selected. Select the **Next** button. +* In the **Additional information** dialog, set the **Authentication type** to **Individual Accounts**. Use the following settings for the other options: + * **Framework**: Latest framework release (.NET 10 or later) + * **Configure for HTTPS**: Selected + * **Interactive render mode**: **Server** + * **Interactivity location**: **Global** + * **Include sample pages**: Selected + * **Do not use top-level statements**: Not selected + * **Use the .dev.localhost TLD in the application URL**: Not selected + * Select **Create**. + +# [Visual Studio Code](#tab/visual-studio-code) + +This guidance assumes that you have familiarity with VS Code. If you're new to VS Code, see the [VS Code documentation](https://code.visualstudio.com/docs). The videos listed by the [Introductory Videos page](https://code.visualstudio.com/docs/getstarted/introvideos) are designed to give you an overview of VS Code's features. + +In VS Code: + +* Go to the **Explorer** view and select the **Create .NET Project** button. Alternatively, you can bring up the **Command Palette** using Ctrl+Shift+P, and then type "`.NET`" and find and select the **.NET: New Project** command. + +* Select the **Blazor Web App** project template from the list. + +* In the **Project Location** dialog, create or select a folder for the project. + +* In the **Command Palette**, name the project `BlazorWebAppPasskeys`, including matching the capitalization. Using this exact project name is important to ensure that the namespaces match for code that you copy from the article into the app that you're building. + +* Select **Create project** from the **Command Palette**. + +# [.NET CLI](#tab/net-cli/) + +In a command shell: + +Change to the directory using the `cd` command to where you want to create the project folder (for example, `cd c:/users/Bernie_Kopell/Documents`). + +Use the [`dotnet new` command](/dotnet/core/tools/dotnet-new) with the [`blazor` project template](/dotnet/core/tools/dotnet-new-sdk-templates#blazor) to create a new Blazor Web App project. The [`-o|--output` option](/dotnet/core/tools/dotnet-new#options) passed to the command creates the project in a new folder named `BlazorWebAppPasskeys` at the current directory location. + +> [!IMPORTANT] +> Name the project `BlazorWebAppPasskeys`, including matching the capitalization, so the namespaces match for code that you copy from the article to the app. + +```dotnetcli +dotnet new blazor -au Individual -o BlazorWebAppPasskeys +``` + +--- + +The preceding instructions create a Blazor Web App with: + +* ASP.NET Core Identity configured for user authentication using the [`-au|--authentication` option](/dotnet/core/tools/dotnet-new-sdk-templates#blazor). +* Entity Framework Core with SQLite for data storage. +* Passkey registration and authentication endpoints. +* UI components for managing passkeys. + +> [!NOTE] +> Currently, only the Blazor Web App project template includes built-in passkey support. + +## Run the application + +# [Visual Studio](#tab/visual-studio) + +Press F5 to run the app with debugging or Ctrl+F5 to run the app without debugging. + +# [Visual Studio Code](#tab/visual-studio-code) + +Press F5 to run the app with debugging or Ctrl+F5 to run the app without debugging. + +# [.NET CLI](#tab/net-cli/) + +In a command shell opened to the root folder of the server `BlazorWebAppPasskeys` project, execute the following command: + +```dotnetcli +dotnet watch +``` + +:::zone-end + +:::zone pivot="existing-app" + +The following guidance relies upon an app that was created with **Individual Accounts** for the app's **Authentication type** or [scaffolding Identity into an existing app](xref:security/authentication/scaffold-identity#scaffold-identity-into-a-blazor-project). + +## Prerequisites + + + +* An existing Blazor Web App (.NET 10 or later) with ASP.NET Core Identity +* [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) + +For migration guidance, see . + +## Reference source guidance + +The links in this article to .NET reference source load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list. For more information, see [How to select a version tag of ASP.NET Core source code (dotnet/AspNetCore.Docs #26205)](https://github.com/dotnet/AspNetCore.Docs/discussions/26205). + +## Update Identity schema version + +In `Program.cs`, update the Identity configuration to use schema version 3, which includes passkey support: + +```csharp +builder.Services.AddIdentityCore(options => +{ + options.SignIn.RequireConfirmedAccount = true; + options.Stores.SchemaVersion = IdentitySchemaVersions.Version3; +}) +.AddEntityFrameworkStores() +.AddSignInManager() +.AddDefaultTokenProviders(); +``` + +## Create and run a database migration + +# [Visual Studio](#tab/visual-studio) + +In Visual Studio **Solution Explorer**, double-click **Connected Services**. In the **Service Dependencies** area, select the ellipsis (`...`) followed by **Add migration** in the **SQL Server Express LocalDB** area. + +Give the migration a **Migration name** of `AddPasskeySupport` to describe the migration. Wait for the database context to load in the **DbContext class names** field. Select **Finish** to create the migration. Select the **Close** button when the operation completes. + +Select the ellipsis (`...`) again followed by the **Update database** command. + +The **Update database with the latest migration** dialog opens. Wait for the **DbContext class names** field to update and for prior migrations to load. Select the **Finish** button. Select the **Close** button when the operation completes. + +# [Visual Studio Code](#tab/visual-studio-code) + +Use the following command in the **Terminal** (**Terminal** menu > **New Terminal**) to add a migration for the new data annotations: + +```dotnetcli +dotnet ef migrations add AddPasskeySupport +``` + +To apply the migration to the database, execute the following command: + +```dotnetcli +dotnet ef database update +``` + +# [.NET CLI](#tab/net-cli/) + +To add a migration for the new data annotations, execute the following command in a command shell opened to the project's root folder: + +```dotnetcli +dotnet ef migrations add AddPasskeySupport +``` + +To apply the migration to the database, execute the following command: + +```dotnetcli +dotnet ef database update +``` + +--- + +## Create passkey model classes + +Add the following model classes to the project in the `Components/Account` folder with `BlazorWebCSharp._1.Components.Account` namespace updates for the app (for example: `Contoso.Components.Account`): + +* [`Components/Account/PasskeyInputModel.cs`](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/PasskeyInputModel.cs): Holds the JSON passkey credential for passkey sign-in operations (`Login` component) and adding passkeys (`Passkeys` component). +* [`Components/Account/PasskeyOperation.cs`](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/PasskeyOperation.cs): Defines the authentication action to be performed (`PassKeySubmit` component), either registering a new passkey (`Create`/0) or authenticating with an existing passkey (`Request`/1). + +## Create the `PasskeySubmit` component + +Add the following `PasskeySubmit` component to handle passkey operations: + +[`Components/Account/Shared/PasskeySubmit.razor`](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Shared/PasskeySubmit.razor) + +## Add the JavaScript for passkey operations + +Add the following JavaScript file to handle WebAuthn API interactions: + +[`Components/Account/Shared/PasskeySubmit.razor.js`](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Shared/PasskeySubmit.razor.js) + +## Add passkey endpoints + +Update the `IdentityComponentsEndpointRouteBuilderExtensions.cs` file (or create the file if it doesn't exist and call `MapAdditionalIdentityEndpoints` in the [`Program` file](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Program.cs#L129-L130)) to include the passkey-specific endpoints: + +[`/PasskeyCreationOptions` and `/PasskeyRequestOptions` endpoints](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs#L53-L90) + +## Update the Login page + +Replace the existing `Login` component with the following component and update the `BlazorWebCSharp._1.Data` namespace to match the app (for example: `Contoso.Components.Account.Data`): + +[`Components/Account/Pages/Login.razor`](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Login.razor) + +## Create passkey management pages for adding and renaming passkeys + +Add the following `Passkeys` component for managing passkeys and update the `BlazorWebCSharp._1.Data` namespace to match the app (for example: `Contoso.Components.Account.Data`): + +[`Components/Account/Pages/Manage/Passkeys.razor`](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/Passkeys.razor) + +Add the following `RenamePasskey` component for renaming passkeys and update the `BlazorWebCSharp._1.Data` namespace to match the app (for example: `Contoso.Components.Account.Data`): + +[`Components/Account/Pages/Manage/RenamePasskey.razor`](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/RenamePasskey.razor) + +## Update the manage navigation menu + +Add a link to the passkey management page in the app's `ManageNavMenu` component. + +In `Components/Account/Shared/ManageNavMenu.razor`: + +```diff ++ +``` + +## Include the JavaScript file + +In the `App` component, add a reference to the `PasskeySubmit` JavaScript file after the Blazor script. + +In `Components/App.razor`: + +```diff + ++ +``` + +:::zone-end + +## Register a passkey + +To test passkey functionality: + +1. Register a new account or sign in with an existing account. +1. Navigate to **Manage your account** (select the username in the navigation menu). +1. Select **Passkeys** from the navigation menu. +1. Select **Add a new passkey** +1. Follow the browser's prompts to create a passkey using your device's authenticator. + +## Sign in with a passkey + +After a passkey is registered: + +1. Sign out of the app. +1. On the login page, enter your email address. +1. Select **Log in with a passkey**. +4. Follow the browser's prompts to authenticate with your passkey. +1. Navigate to `Account/Manage/Passkeys` to add, rename, or delete passkeys. +1. If the passkey supports passkey autofill (conditional UI) for login, test the passkey autofill feature by selecting the email input field when you have saved passkeys. + +## Additional resources + +* [Web Authentication API (MDN documentation)](https://developer.mozilla.org/docs/Web/API/Web_Authentication_API) +* [Get started with phishing-resistant passwordless authentication deployment in Microsoft Entra ID](/entra/identity/authentication/how-to-plan-prerequisites-phishing-resistant-passwordless-authentication) diff --git a/aspnetcore/security/authentication/passkeys/index.md b/aspnetcore/security/authentication/passkeys/index.md new file mode 100644 index 000000000000..617163a86fb1 --- /dev/null +++ b/aspnetcore/security/authentication/passkeys/index.md @@ -0,0 +1,579 @@ +--- +title: Enable Web Authentication API (WebAuthn) passkeys +author: guardrex +description: Discover how to enable Web Authentication API (WebAuthn) passkeys in ASP.NET Core apps. +ms.author: wpickett +monikerRange: '>= aspnetcore-10.0' +ms.date: 09/08/2025 +uid: security/authentication/passkeys/index +--- +# Enable Web Authentication API (WebAuthn) passkeys + + + + + +Passkeys provide a modern, phishing-resistant authentication method based on the [Web Authentication API (WebAuthn)](https://developer.mozilla.org/docs/Web/API/Web_Authentication_API) and [FIDO2](https://www.microsoft.com/security/business/security-101/what-is-fido2) standards. They are a secure alternative to passwords, using public key cryptography and device-based authentication. This article explains how to configure an ASP.NET Core app to use passkeys to authenticate users. + +For guidance specific to new and existing Blazor Web Apps, see after reading this article. + +## What are passkeys? + +Passkeys are a replacement for passwords that use cryptographic key pairs. The private key is stored securely on the user's device, such as in a hardware security module, platform authenticator (examples: Windows Hello, Touch ID, Face ID), or a password manager, while the public key is stored by the web app. During authentication, the user proves possession of the private key without it ever leaving their device. + +Key benefits of passkeys include: + +* **Phishing resistance**: Passkeys are bound to specific websites and can't be used on fake sites. +* **No shared secrets**: The server only stores public keys, eliminating the risk of password database breaches. +* **User convenience**: Simple biometric or PIN verification replaces complex password requirements. +* **Cross-device synchronization**: Many passkey providers sync credentials across a user's devices. + +For more information, see [Web Authentication API (MDN documentation)](https://developer.mozilla.org/docs/Web/API/Web_Authentication_API). + +## Passkeys in ASP.NET Core Identity + +ASP.NET Core Identity includes built-in support for passkey registration and authentication: + +* Seamless integration with Identity infrastructure. +* User authentication support for the most common WebAuthn scenarios. +* Built into the Blazor Web App project template, so only developer configuration is required. + +> [!IMPORTANT] +> The passkey implementation in ASP.NET Core Identity is deliberately scoped to authentication scenarios. It isn't intended as a general-purpose WebAuthn library. Developers requiring full WebAuthn functionality should consider community libraries that provide comprehensive protocol support. + +## Supported scenarios + +The ASP.NET Core Identity passkey implementation supports the following primary scenarios: + +* **Adding passkeys to existing accounts**: Users with password-based accounts can register passkeys as an additional authentication method. +* **Passwordless account creation**: Users can create accounts without a password by registering a passkey on account creation. +* **Passwordless sign-in**: Users can authenticate using only their passkey without entering a password. + +## Limitations + +The current implementation has the following limitations: + +* **Scoped to ASP.NET Core Identity**: The APIs are designed specifically for Identity authentication scenarios. +* **No default attestation validation**: The implementation doesn't validate attestation statements by default. +* **Template support**: Only the Blazor Web App template includes passkey support. +* **No built-in 2FA support**: Passkeys are treated as a primary authentication factor, not as a second factor. + +## Core concepts + +Two fundamental processes underpin passkey operations: attestation and assertion. + +### Attestation (registration) + +*Attestation* is the process of creating and registering a new passkey. During attestation, the server generates a unique challenge that the authenticator must include in the returned credential. The authenticator creates a new key pair and returns the public key along with attestation data proving the key's origin. The server then verifies this attestation and stores the public key for future authentication attempts. + +### Assertion (authentication) + +*Assertion* is the process of authenticating with an existing passkey. The server generates a unique challenge, which the authenticator signs using the private key. The authenticator returns this signed assertion to the server, which verifies the signature using the previously stored public key. If the signature is valid, the user is authenticated. + +## Prerequisites + + + +* [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +* A modern web browser that supports WebAuthn. +* A device with a platform authenticator, such as Windows Hello or Apple secure enclave, or a security key. + +## Security considerations + +When implementing passkeys in ASP.NET Core Identity, ensure the app meets the security requirements described in this section. + +### Host header validation + +The implementation infers the Relying Party ID from the host header when `ServerDomain` isn't explicitly configured. The hosting environment must validate host headers to prevent credential-scoping attacks, which involve using compromised or stolen user credentials (usernames, passwords, tokens) to gain unauthorized access. + +**Mitigation**: Either explicitly configure `ServerDomain` in `IdentityPasskeyOptions` or ensure that the hosting environment (Kestrel, IIS, reverse proxy) validates host headers. For configuration details, see your hosting platform's documentation. + +### Subdomain security + +ASP.NET Core's passkeys implementation handles subdomain security through the `ServerDomain` configuration option. When `ServerDomain` isn't explicitly specified, the implementation uses the host header to determine the domain. This means that ***the page on which the passkey was registered controls the domain*** for that credential. + +For example: + +* If a passkey is registered on `app.contoso.com`, it also works on `*.app.contoso.com`. +* If registered on `contoso.com`, it also works on `*.contoso.com`. +* The browser enforces that passkeys can only be used on the domain (and subdomains) where they were registered. + +**Requirement**: Apps requiring strict domain control should explicitly set `ServerDomain` rather than relying on the host header. Don't serve untrusted content on any subdomain within the `ServerDomain` scope. If you can't guarantee this, implement [custom origin validation](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin) to restrict passkey usage to specific origins. + +### HTTPS requirement + +All passkey operations require HTTPS. The implementation stores authentication data in encrypted and signed cookies that could be intercepted over unencrypted connections. + +**Requirement**: Always use HTTPS in production. Configure [HTTP Strict Transport Security Protocol (HSTS)](xref:security/enforcing-ssl#http-strict-transport-security-protocol-hsts) to prevent protocol downgrade attacks. + +### Account recovery + +Account recovery is primarily a concern for apps that allow passkeys as the only authentication mechanism. The default Blazor Web App project template already requires users to set up a backup authentication method (password or external provider) when creating an account, so account recovery is handled through these existing mechanisms. + +**Recommendations**: + +For applications implementing passkey-only authentication, consider: + +* Recovery codes generated during account creation. +* Email-based recovery flows. +* Mandatory registration of multiple passkeys. +* Monitoring the `IsBackedUp` flag on `UserPasskeyInfo` to prompt users to add additional credentials. + +### Administrative controls + +When an authenticator model is discovered to have security vulnerabilities, you may need to revoke affected credentials. The implementation stores the complete attestation object with each credential, including the Authenticator Attestation GUID (AAGUID), which is a 128-bit identifier indicating the key type. + +**Implementation**: Extract AAGUIDs from stored attestation objects, compare against known-compromised models, and revoke affected credentials. AAGUID reliability depends on whether your app validates attestation statements. To hook in custom attestation statement validation logic, see [Custom attestation statement validation](#custom-attestation-statement-validation). Third-party libraries are available for attestation validation, such as the [Passkeys - FIDO2 .NET Library (WebAuthn) (`passwordless-lib/fido2-net-lib` GitHub repository)](https://github.com/passwordless-lib/fido2-net-lib)†. + +> [!WARNING] +> †Third-party libraries, including `passwordless-lib/fido2-net-lib`, aren't owned or maintained by Microsoft and aren't covered by any Microsoft Support Agreement or license. Use caution when adopting a third-party library, especially for security features. Confirm that the library follows official specifications and adopts security best practices. Keep the library's version current to obtain the latest bug fixes. + +### Resource limits + +To prevent database exhaustion attacks, apps should enforce limits on passkey registration, such as: + +* Maximum number of passkeys per user account. +* Maximum length for passkey display names. + +The Blazor Web App template enforces these limits by default at the application level. For examples, see the following Razor components in the Blazor Web App project template: + +* [`Components/Account/Pages/Manage/Passkeys.razor`](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/Passkeys.razor) +* [`Components/Account/Pages/Manage/RenamePasskey.razor`](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/Pages/Manage/RenamePasskey.razor) + +[!INCLUDE[](~/includes/aspnetcore-repo-ref-source-links.md)] + +## Configure passkey options + +ASP.NET Core Identity provides various options to configure passkey behavior through the `IdentityPasskeyOptions` class, which include: + +* `AuthenticatorTimeout`: Gets or sets the time that the browser should wait for the authenticator to provide a passkey as a . This option applies to both creating a new passkey and requesting an existing passkey. This option is treated as a hint to the browser, and the browser may ignore the option. The default value is 5 minutes. +* `ChallengeSize`: Gets or sets the size of the challenge in bytes sent to the client during attestation and assertion. This option applies to both creating a new passkey and requesting an existing passkey. The default value is 32 bytes. +* `ServerDomain`: Gets or sets the effective Relying Party ID (domain) of the server. This should be unique and will be used as the identity for the server. This option applies to both creating a new passkey and requesting an existing passkey. If `null`, which is the default value, the server's origin is used. For more information, see [Relying Party Identifier RP ID](https://www.w3.org/TR/webauthn-3/#rp-id). + +Example configuration: + +```csharp +builder.Services.Configure(options => +{ + options.ServerDomain = "contoso.com"; + options.AuthenticatorTimeout = TimeSpan.FromMinutes(3); + options.ChallengeSize = 64; +}); +``` + + + +For a complete list of configuration options during the .NET 10 preview release period, see the [`IdentityPasskeyOptions` reference source (`dotnet/aspnetcore` GitHub repository)](https://github.com/dotnet/aspnetcore/blob/main/src/Identity/Core/src/IdentityPasskeyOptions.cs). + +> [!NOTE] +> Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next preview release of .NET. To select a tag for a specific release, use the **Switch branches or tags** dropdown list. For more information, see [How to select a version tag of ASP.NET Core source code (`dotnet/AspNetCore.Docs` #26205)](https://github.com/dotnet/AspNetCore.Docs/discussions/26205). + +> [!NOTE] +> The browser defaults mentioned in the API documentation were valid as of August, 2025. See the [W3C WebAuthn specification](https://www.w3.org/TR/webauthn-3/) for the most up-to-date defaults. + +## Custom attestation statement validation + +By default, ASP.NET Core Identity doesn't validate attestation statements. This is suitable for most consumer authentication scenarios. If your app requires verification of authenticator properties or if you want to disallow specific authenticators from being used, for example, in enterprise environments that require a higher level of security, you can implement custom attestation validation: + +```csharp +builder.Services.Configure(options => +{ + options.VerifyAttestationStatement = async (context) => + { + // Custom attestation validation logic + // Return 'true' if the attestation is valid + // Return 'false' if the attestation is invalid + return true; + }; +}); +``` + +> [!WARNING] +> Attestation validation is complex and requires maintaining trust stores for authenticator certificates. Only implement custom validation if your app requires verification of specific authenticator properties. + +## Custom origin validation + +The default origin validation allows requests from subdomains and disallows cross-origin iframes. To customize this behavior: + +```csharp +builder.Services.Configure(options => +{ + options.ValidateOrigin = async (context) => + { + // Custom origin validation logic + // Access the origin via 'context.Origin' + // Access the HTTP context via 'context.HttpContext' + // Return 'true' if the origin is valid + // Return 'false' if the origin is invalid + return true; + }; +}); +``` + +## Registration flow + +This section walks through each step of the passkey registration process, explaining how ASP.NET Core Identity facilitates the creation and storage of passkey credentials. + +```mermaid +sequenceDiagram + participant Authenticator + participant User + participant Browser + participant Server + + User->>Browser: Click "Add passkey" + Browser->>Server: Request creation options + Server->>Browser: Return creation options + Browser->>Authenticator: Request new credential + Authenticator->>User: Verify identity (biometric/PIN) + User->>Authenticator: Approve + Authenticator->>Browser: Return credential + Browser->>Server: Submit credential + Server->>Server: Verify and store + Server->>Browser: Registration complete + Browser->>User: Success message +``` + +### Step 1: Initiating registration + +The registration process begins when a user decides to add a passkey to their account. This typically happens through a button or link in the app's user interface. When selected, this element triggers JavaScript code to orchestrate the registration flow. + +The client-side implementation varies significantly between apps. In the Blazor Web App template, you can find a complete example in `PasskeySubmit.razor.js`, which shows how a custom web component handles the registration initiation and manages the subsequent WebAuthn API calls. + +### Step 2: Requesting creation options + +After registration is initiated, the browser must obtain creation options from the server. These options tell the browser what kind of credential to create and include important security parameters, such as the challenge that must be signed. + +From the browser's perspective, this step involves making an HTTP request to the server: + +```javascript +async function createCredential(headers, signal) { + // Step 2: Request creation options from the server + const optionsResponse = + await fetchWithErrorHandling('/Account/PasskeyCreationOptions', + { + method: 'POST', + headers, + signal, + }); + const optionsJson = await optionsResponse.json(); + const options = PublicKeyCredential.parseCreationOptionsFromJSON(optionsJson); + return await navigator.credentials.create({ publicKey: options, signal }); +} +``` + +The application should define an endpoint that generates these options: + +```csharp +app.MapPost("/Account/PasskeyCreationOptions", async ( + HttpContext context, + UserManager userManager, + SignInManager signInManager) => +{ + var user = await userManager.GetUserAsync(context.User); + + if (user is null) + { + return Results.NotFound(); + } + + var userId = await userManager.GetUserIdAsync(user); + var userName = await userManager.GetUserNameAsync(user) ?? "User"; + + var optionsJson = await signInManager.MakePasskeyCreationOptionsAsync(new() + { + Id = userId, + Name = userName, + DisplayName = userName + }); + + return TypedResults.Content(optionsJson, contentType: "application/json"); +}); +``` + +The `MakePasskeyCreationOptionsAsync` method is central to this process. The method accepts a `PasskeyUserEntity` that describes the user for whom the passkey is being created. This entity contains the user's ID, username (typically an email address), and a human-readable display name. The method returns a JSON string that conforms to the WebAuthn `PublicKeyCredentialCreationOptions` schema, which the browser uses in the next step. Behind the scenes, this method also stores temporary state in an authentication cookie to ensure that the response from the browser corresponds to these specific options. + +### Step 3: Server generates options + +When `MakePasskeyCreationOptionsAsync` executes, it uses the app's `IdentityPasskeyOptions` configuration to determine the specific parameters for credential creation. These options control various aspects of the passkey creation process. + +You can customize these options during application startup. For example: + +```csharp +builder.Services.Configure(options => +{ + options.ServerDomain = "contoso.com"; + options.AuthenticatorTimeout = TimeSpan.FromMinutes(3); + options.UserVerificationRequirement = "required"; + options.ResidentKeyRequirement = "preferred"; +}); +``` + +The `UserVerificationRequirement` option determines whether the authenticator must verify the user's identity (through biometric or PIN methods), while `ResidentKeyRequirement` indicates whether the credential should be discoverable, allowing authentication without first providing a username. For more information during the .NET 10 preview release period, see the [`IdentityPasskeyOptions` reference source (`dotnet/aspnetcore` GitHub repository)](https://github.com/dotnet/aspnetcore/blob/main/src/Identity/Core/src/IdentityPasskeyOptions.cs). + + + +### Step 4: Client requests credential + +With the creation options available, the client-side JavaScript passes the options to the WebAuthn API to create a new credential: + +```javascript +async function createCredential(headers, signal) { + // Step 4: Parse the options and request a new credential from the authenticator + const optionsResponse = + await fetchWithErrorHandling('/Account/PasskeyCreationOptions', + { + method: 'POST', + headers, + signal, + }); + const optionsJson = await optionsResponse.json(); + const options = PublicKeyCredential.parseCreationOptionsFromJSON(optionsJson); + return await navigator.credentials.create({ publicKey: options, signal }); +} +``` + +The `parseCreationOptionsFromJSON` function converts the JSON response into the format expected by the WebAuthn API, and `navigator.credentials.create()` initiates the credential creation process with the authenticator. + +### Step 5: Authenticator interaction + +At this point, the browser communicates with the authenticator to create the credential. The authenticator prompts the user for verification, which might involve scanning a fingerprint, entering a PIN, or using facial recognition. This interaction is handled entirely by the browser and the authenticator, requiring no app code. The user experience varies depending on the type of authenticator and the platform's capabilities. + +### Step 6: Credential submission + +After the authenticator creates the credential, the browser must send the credential back to the server for verification and storage. The credential must be serialized to JSON before submission: + +```javascript +async function createCredential(headers, signal) { + // Step 6: The credential is returned from navigator.credentials.create() + // and is serialized to JSON for submission to the server + const optionsResponse = + await fetchWithErrorHandling('/Account/PasskeyCreationOptions', + { + method: 'POST', + headers, + signal, + }); + const optionsJson = await optionsResponse.json(); + const options = PublicKeyCredential.parseCreationOptionsFromJSON(optionsJson); + return await navigator.credentials.create({ publicKey: options, signal }); +} +``` + +In the Blazor Web App template, the returned credential is automatically serialized and submitted through a form, but the exact submission mechanism varies by application. + +### Step 7: Server verification and storage + +When the server receives the credential, it must verify its validity and store the public key for future authentication. This is where ASP.NET Core Identity's passkey APIs become crucial. + +The `PerformPasskeyAttestationAsync` method validates the attestation response from the client. This comprehensive validation process: + +* Verifies that the credential type matches expectations. +* Validates the client data JSON including origin and challenge. +* Checks authenticator data flags for user presence and verification +* Extracts and validates the public key. + +If all checks pass, the method returns a `PasskeyAttestationResult` containing the verified passkey information. + +After the attestation is verified, the app uses `AddOrUpdatePasskeyAsync` to store the passkey in the database: + +```csharp +var attestationResult = + await signInManager.PerformPasskeyAttestationAsync(credentialJson); + +if (!attestationResult.Succeeded) +{ + return Results.BadRequest($"Error: {attestationResult.Failure.Message}"); +} + +var addResult = + await userManager.AddOrUpdatePasskeyAsync(user, attestationResult.Passkey); + +if (!addResult.Succeeded) +{ + return Results.BadRequest("Failed to store passkey"); +} +``` + +The stored `UserPasskeyInfo` contains all of the necessary information for future authentication, including the credential ID, public key, signature counter for replay protection, and flags indicating whether the passkey is backed up or eligible for backup. + +### Step 8: Post-registration tasks + +After successfully registering a passkey, apps often perform additional tasks to improve the user experience. A common pattern is to prompt users to provide a friendly name for their passkey, making it easier to identify among multiple credentials. The `UserPasskeyInfo.Name` property stores this user-friendly name, which can be updated using the same `AddOrUpdatePasskeyAsync` method: + +```csharp +passkey.Name = "My iPhone"; +await userManager.AddOrUpdatePasskeyAsync(user, passkey); +``` + +## Authentication flow + +This section explains how users authenticate with their passkeys, from initiating the sign-in process to establishing an authenticated session. + +```mermaid +sequenceDiagram + participant Authenticator + participant User + participant Browser + participant Server + + User->>Browser: Click "Sign in with passkey" + Browser->>Server: Request authentication options + Server->>Browser: Return authentication options + Browser->>Authenticator: Request assertion + Authenticator->>User: Verify identity + User->>Authenticator: Approve + Authenticator->>Browser: Return signed assertion + Browser->>Server: Submit assertion + Server->>Server: Verify signature + Server->>Browser: Authentication complete + Browser->>User: Redirect to app +``` + +### Step 1: Initiating authentication + +Users typically initiate passkey authentication through a dedicated button or link on the login page. Some apps also support conditional UI, where passkeys appear as autofill suggestions in the username field. The initiation method triggers JavaScript code that manages the authentication flow, similar to the registration process. + +### Step 2: Requesting authentication options + +The browser requests authentication options from the server to begin the authentication process. These options include a list of acceptable credentials and a new challenge to be signed: + +```javascript +async function requestCredential(email, mediation, headers, signal) { + // Step 2: Request authentication options from the server + const optionsResponse = + await fetchWithErrorHandling(`/Account/PasskeyRequestOptions?username=${email}`, + { + method: 'POST', + headers, + signal, + }); + const optionsJson = await optionsResponse.json(); + const options = PublicKeyCredential.parseRequestOptionsFromJSON(optionsJson); + return await navigator.credentials.get({ publicKey: options, mediation, signal }); +} +``` + +The `MakePasskeyRequestOptionsAsync` method generates these options. When you provide a specific user, it includes only that user's credentials in the allow list. When called without a user, it generates options suitable for conditional UI or username-less authentication: + +```csharp +app.MapPost("/Account/PasskeyRequestOptions", async ( + SignInManager signInManager, + string? username) => +{ + var user = string.IsNullOrEmpty(username) + ? null + : await userManager.FindByNameAsync(username); + + var optionsJson = await signInManager.MakePasskeyRequestOptionsAsync(user); + + return TypedResults.Content(optionsJson, contentType: "application/json"); +}); +``` + +### Step 3: Server generates options + +The server generates authentication options using the same `IdentityPasskeyOptions` configuration used during registration. The `ServerDomain` must match the domain where the passkey was originally registered, or authentication fails. The `UserVerificationRequirement` determines whether the authenticator must verify the user's identity during authentication. + +### Step 4: Client requests assertion + +The client-side JavaScript passes the authentication options to the WebAuthn API to request an assertion from the authenticator: + +```javascript +async function requestCredential(email, mediation, headers, signal) { + // Step 4: Parse the options and request an assertion from the authenticator + const optionsResponse = + await fetchWithErrorHandling(`/Account/PasskeyRequestOptions?username=${email}`, + { + method: 'POST', + headers, + signal, + }); + const optionsJson = await optionsResponse.json(); + const options = PublicKeyCredential.parseRequestOptionsFromJSON(optionsJson); + return await navigator.credentials.get({ publicKey: options, mediation, signal }); +} +``` + +The `navigator.credentials.get()` call initiates the authentication process with the authenticator, which prompts the user for verification. + +### Step 5: Authenticator verification + +The authenticator verifies the user's identity and signs the challenge with the private key. This process is handled entirely by the browser and authenticator, similar to the verification step during registration. The user experience depends on the authenticator type and may involve biometric verification or PIN entry. + +### Step 6: Assertion submission + +After the authenticator creates the signed assertion, the browser serializes it to JSON and submits it to the server: + +```javascript +async function requestCredential(email, mediation, headers, signal) { + // Step 6: The assertion is returned from navigator.credentials.get() + // and is serialized to JSON for submission to the server + const optionsResponse = + await fetchWithErrorHandling(`/Account/PasskeyRequestOptions?username=${email}`, + { + method: 'POST', + headers, + signal, + }); + const optionsJson = await optionsResponse.json(); + const options = PublicKeyCredential.parseRequestOptionsFromJSON(optionsJson); + return await navigator.credentials.get({ publicKey: options, mediation, signal }); +} +``` + +The submission mechanism varies by app but typically involves either a form submission or an API call. + +### Step 7: Server verification + +The server verifies the assertion to authenticate the user. ASP.NET Core Identity provides the `PasskeySignInAsync` method, which performs the complete authentication flow in a single call: + +```csharp +var result = await signInManager.PasskeySignInAsync(credentialJson); + +if (result.Succeeded) +{ + return Results.Ok("Authentication successful"); +} + +return Results.Unauthorized(); +``` + +The `PasskeySignInAsync` method internally calls `PerformPasskeyAssertionAsync` to: + +* Validate the assertion signature using the stored public key. +* Verify that the challenge matches the one originally sent. +* Check authenticator flags for user presence and verification. +* Update the signature counter to prevent replay attacks. + +If all checks pass, the method signs in the user and returns a `SignInResult` indicating success. + +For scenarios requiring more control, you can use `PerformPasskeyAssertionAsync` directly to validate the assertion without immediately signing in the user: + +* `PerformPasskeyAssertionAsync` returns a `PasskeyAssertionResult` containing the authenticated user and updated passkey information. +* Because the passkey's sign-in count and authenticator flags may have changed since the last assertion and the updated passkey isn't automatically stored when calling `PerformPasskeyAssertionAsync`, call `userManager.AddOrUpdatePasskeyAsync` with the returned `PasskeyAssertionResult`. + +### Step 8: Session establishment + +Upon successful authentication, ASP.NET Core Identity establishes an authenticated session for the user. The `PasskeySignInAsync` method handles this automatically, creating the necessary authentication cookies and claims. The app then redirects the user to protected resources or display personalized content. + +## Additional resources + +* [Web Authentication API (MDN documentation)](https://developer.mozilla.org/docs/Web/API/Web_Authentication_API) +* [Get started with phishing-resistant passwordless authentication deployment in Microsoft Entra ID](/entra/identity/authentication/how-to-plan-prerequisites-phishing-resistant-passwordless-authentication) diff --git a/aspnetcore/toc.yml b/aspnetcore/toc.yml index e0cdf8f5a8c4..632a917b7c8a 100644 --- a/aspnetcore/toc.yml +++ b/aspnetcore/toc.yml @@ -1734,6 +1734,12 @@ items: uid: security/authentication/identity-enable-qrcodes - name: Two-factor authentication with SMS uid: security/authentication/2fa + - name: Passkeys (WebAuthn) + items: + - name: Overview + uid: security/authentication/passkeys/index + - name: Blazor + uid: security/authentication/passkeys/blazor - name: External authentication providers items: - name: Overview @@ -2076,6 +2082,9 @@ items: uid: fundamentals/middleware/extensibility-third-party-container - name: Migration and updates items: + - name: Overview + displayName: migrate, migration + uid: migration/index - name: Version updates items: - name: 8 to 9 diff --git a/aspnetcore/zone-pivot-groups.yml b/aspnetcore/zone-pivot-groups.yml index f47bdcf821f6..b6bbe21fb21c 100644 --- a/aspnetcore/zone-pivot-groups.yml +++ b/aspnetcore/zone-pivot-groups.yml @@ -126,3 +126,11 @@ groups: title: Manually - id: aspire title: Aspire +- id: implementation + title: Feature implementation + prompt: Choose how the feature will be implemented + pivots: + - id: new-development + title: New app created from the project template + - id: existing-app + title: Implemented in an existing app diff --git a/cspell.json b/cspell.json index 594f84f0a120..c26a1977b54c 100644 --- a/cspell.json +++ b/cspell.json @@ -1,5 +1,5 @@ { - "version": "0.1", + "version": "0.2", "language": "en", "words": [ "antiforgery", @@ -13,6 +13,7 @@ "blazorserver", "blazorwasm", "componentized", + "Contoso", "cryptosystem", "cyberattacker", "cyberattackers", @@ -39,10 +40,10 @@ "rerender", "rerendering", "rerenders", - "riande", "routable", "Routable", "signalr", + "tdykstra", "typeof", "wadepickett", "wasm", @@ -51,6 +52,7 @@ "webforms", "websocket", "websockets", + "wpickett", "wwwroot" ], "flagWords": [