diff --git a/.openpublishing.redirection.json b/.openpublishing.redirection.json index 449e5179adec..707f187f62fb 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/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..298cdf80229e --- /dev/null +++ b/aspnetcore/migration/90-to-100.md @@ -0,0 +1,84 @@ +--- +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..4598252c3211 --- /dev/null +++ b/aspnetcore/migration/90-to-100/includes/blazor.md @@ -0,0 +1,5 @@ +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 . 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/release-notes/aspnetcore-10.0.md b/aspnetcore/release-notes/aspnetcore-10.0.md index c9f98af78c6d..d1570ecc1523 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 diff --git a/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md b/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md index e984b4222ce4..02638cd2a706 100644 --- a/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md +++ b/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md @@ -601,13 +601,12 @@ For more information, see +* ### Circuit state persistence 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 1993561aa819..9eeb376d7611 100644 --- a/aspnetcore/toc.yml +++ b/aspnetcore/toc.yml @@ -1738,6 +1738,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 @@ -2080,6 +2086,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": [