From ccdd04e3145f493373339933aba6cd8516389200 Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Thu, 14 Aug 2025 08:16:26 -0400 Subject: [PATCH 01/18] Passkeys coverage --- aspnetcore/blazor/security/passkeys.md | 660 ++++++++++++++ aspnetcore/migration/80-90.md | 9 +- aspnetcore/migration/90-to-100.md | 84 ++ .../includes/aspnetcore-app-passkeys.md | 334 +++++++ .../includes/blazor-web-app-passkeys.md | 843 ++++++++++++++++++ .../migration/90-to-100/includes/blazor.md | 5 + .../aspnetcore-10/includes/blazor.md | 11 +- aspnetcore/toc.yml | 8 + 8 files changed, 1943 insertions(+), 11 deletions(-) create mode 100644 aspnetcore/blazor/security/passkeys.md create mode 100644 aspnetcore/migration/90-to-100.md create mode 100644 aspnetcore/migration/90-to-100/includes/aspnetcore-app-passkeys.md create mode 100644 aspnetcore/migration/90-to-100/includes/blazor-web-app-passkeys.md create mode 100644 aspnetcore/migration/90-to-100/includes/blazor.md diff --git a/aspnetcore/blazor/security/passkeys.md b/aspnetcore/blazor/security/passkeys.md new file mode 100644 index 000000000000..db7b60375b61 --- /dev/null +++ b/aspnetcore/blazor/security/passkeys.md @@ -0,0 +1,660 @@ +--- +title: Enable Web Authentication API (WebAuthn) passkeys in an ASP.NET Core Blazor Web App +author: guardrex +description: Discover how to enable Web Authentication API (WebAuthn) passkeys with ASP.NET Core Blazor Web App authentication. +ms.author: wpickett +monikerRange: '>= aspnetcore-10.0' +ms.date: 08/14/2024 +uid: blazor/security/passkeys +--- +# Enable Web Authentication API (WebAuthn) passkeys in an ASP.NET Core Blazor Web App + + + +[!INCLUDE[](~/includes/not-latest-version.md)] + + + +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/en-us/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 Blazor Web App to use passkeys to authenticate users. + +The guidance in this article relies upon creating a .NET 10 or later Blazor Web App with **Individual Accounts** for the app's **Authentication type** or [scaffolding Identity](xref:security/authentication/scaffold-identity#scaffold-identity-into-a-blazor-project) into an existing .NET 10 or later Blazor Web App. For migration guidance from prior versions of .NET, see . + +## 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, Touch ID, or Face ID) 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 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. + +### 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. + +## Create or migrate a Blazor Web App + +For migration guidance to update an existing Blazor Web App to use passkeys, see . + +Use the following guidance to create a new Blazor Web App with 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 +``` + +## Register a passkey + +To test passkey functionality: + +1. Register a new account or sign in with an existing account. +2. Navigate to **Manage your account** (select the username in the navigation menu). +3. Select **Passkeys** from the navigation menu. +4. Select **Add a new passkey**. +5. 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. +2. On the login page, enter your email address. +3. Select **Log in with a passkey**. +4. Follow the browser's prompts to authenticate with your passkey. + +## 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 (for example, in enterprise environments), 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. This returns a `PasskeyAssertionResult` containing the authenticated user and updated passkey information. + +### 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/migration/80-90.md b/aspnetcore/migration/80-90.md index c8572f23ed40..91bf60ab6af8 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 @@ -113,5 +112,3 @@ For more information, see the following resources: ## Blazor [!INCLUDE[](~/migration/80-to-90/includes/blazor.md)] - -## Additional resources diff --git a/aspnetcore/migration/90-to-100.md b/aspnetcore/migration/90-to-100.md new file mode 100644 index 000000000000..9ac16f230c48 --- /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/13/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)] + +### Adopt passkey user authentication in an existing Razor Pages or MVC app + +For guidance, see . diff --git a/aspnetcore/migration/90-to-100/includes/aspnetcore-app-passkeys.md b/aspnetcore/migration/90-to-100/includes/aspnetcore-app-passkeys.md new file mode 100644 index 000000000000..cc7c502b3e1c --- /dev/null +++ b/aspnetcore/migration/90-to-100/includes/aspnetcore-app-passkeys.md @@ -0,0 +1,334 @@ +--- +title: Implement passkeys in an existing ASP.NET Core Razor Pages or MVC app +author: guardrex +description: Learn how to implement passkeys authentication in an ASP.NET Core Razor Pages or MVC app. +ms.author: wpickett +ms.custom: mvc +ms.date: 8/14/2025 +uid: migration/aspnetcore-app/passkeys +--- +# Implement passkeys in an existing Razor Pages or MVC app + +This guide walks through adding passkey support to an existing Razor Pages or MVC app that already has ASP.NET Core Identity authentication configured. + +The guidance in this article 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). + +## Prerequisites + + + +* An existing Razor Pages or MVC app with ASP.NET Core Identity +* [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) + +## Step 1: Update to .NET 10 + +Update the app's project file to target .NET 10: + +```xml +net10.0 +``` + +Update all `Microsoft.AspNetCore.*` and `Microsoft.EntityFrameworkCore.*` packages to their latest .NET 10 versions. + +## Step 2: 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(); +``` + +# [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 +``` + +--- + +## Step 3: Create passkey model classes + +Add the following model classes to the project. + +`PasskeyInputModel.cs`: + +```csharp +public class PasskeyInputModel +{ + public string? CredentialJson { get; set; } + public string? Error { get; set; } +} +``` + +`PasskeyOperation.cs`: + +```csharp +public enum PasskeyOperation +{ + Create = 0, + Request = 1, +} +``` + +## Step 4: Create a UI component to handle passkey operations + +*The implementation must be supplied by the developer at this time.* + +## Step 5: Add the JavaScript for passkey operations + +Add the following JavaScript file to handle WebAuthn API interactions. + +`PasskeySubmit.js`: + +```javascript +const browserSupportsPasskeys = + typeof navigator.credentials !== 'undefined' && + typeof window.PublicKeyCredential !== 'undefined' && + typeof window.PublicKeyCredential.parseCreationOptionsFromJSON === 'function' && + typeof window.PublicKeyCredential.parseRequestOptionsFromJSON === 'function'; + +async function fetchWithErrorHandling(url, options = {}) { + const response = await fetch(url, { + credentials: 'include', + ...options + }); + if (!response.ok) { + const text = await response.text(); + console.error(text); + throw new Error(`The server responded with status ${response.status}.`); + } + return response; +} + +async function createCredential(headers, signal) { + 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 }); +} + +async function requestCredential(email, mediation, headers, signal) { + 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 }); +} + +customElements.define('passkey-submit', class extends HTMLElement { + static formAssociated = true; + + connectedCallback() { + this.internals = this.attachInternals(); + this.attrs = { + operation: this.getAttribute('operation'), + name: this.getAttribute('name'), + emailName: this.getAttribute('email-name'), + requestTokenName: this.getAttribute('request-token-name'), + requestTokenValue: this.getAttribute('request-token-value'), + }; + + this.internals.form.addEventListener('submit', (event) => { + if (event.submitter?.name === '__passkeySubmit') { + event.preventDefault(); + this.obtainAndSubmitCredential(); + } + }); + + this.tryAutofillPasskey(); + } + + disconnectedCallback() { + this.abortController?.abort(); + } + + async obtainCredential(useConditionalMediation, signal) { + if (!browserSupportsPasskeys) { + throw new Error('Some passkey features are missing. Please update your browser.'); + } + + const headers = { + [this.attrs.requestTokenName]: this.attrs.requestTokenValue, + }; + + if (this.attrs.operation === 'Create') { + return await createCredential(headers, signal); + } else if (this.attrs.operation === 'Request') { + const email = new FormData(this.internals.form).get(this.attrs.emailName); + const mediation = useConditionalMediation ? 'conditional' : undefined; + return await requestCredential(email, mediation, headers, signal); + } else { + throw new Error(`Unknown passkey operation '${this.attrs.operation}'.`); + } + } + + async obtainAndSubmitCredential(useConditionalMediation = false) { + this.abortController?.abort(); + this.abortController = new AbortController(); + const signal = this.abortController.signal; + const formData = new FormData(); + try { + const credential = await this.obtainCredential(useConditionalMediation, signal); + const credentialJson = JSON.stringify(credential); + formData.append(`${this.attrs.name}.CredentialJson`, credentialJson); + } catch (error) { + if (error.name === 'AbortError') { + return; + } + console.error(error); + if (useConditionalMediation) { + return; + } + const errorMessage = error.name === 'NotAllowedError' + ? 'No passkey was provided by the authenticator.' + : error.message; + formData.append(`${this.attrs.name}.Error`, errorMessage); + } + this.internals.setFormValue(formData); + this.internals.form.submit(); + } + + async tryAutofillPasskey() { + if (browserSupportsPasskeys && this.attrs.operation === 'Request' && await PublicKeyCredential.isConditionalMediationAvailable?.()) { + await this.obtainAndSubmitCredential(true); + } + } +}); +``` + +## Step 6: Add passkey endpoints + +Update the `IdentityComponentsEndpointRouteBuilderExtensions.cs` file (or create the file if it doesn't exist) to include the passkey-specific endpoints: + +```csharp +// Add the following endpoints to the existing 'MapAdditionalIdentityEndpoints' method + +accountGroup.MapPost("/PasskeyCreationOptions", async ( + HttpContext context, + [FromServices] UserManager userManager, + [FromServices] SignInManager signInManager, + [FromServices] IAntiforgery antiforgery) => +{ + await antiforgery.ValidateRequestAsync(context); + + var user = await userManager.GetUserAsync(context.User); + if (user is null) + { + return Results.NotFound($"Unable to load user with ID '{userManager.GetUserId(context.User)}'."); + } + + 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"); +}); + +accountGroup.MapPost("/PasskeyRequestOptions", async ( + HttpContext context, + [FromServices] UserManager userManager, + [FromServices] SignInManager signInManager, + [FromServices] IAntiforgery antiforgery, + [FromQuery] string? username) => +{ + await antiforgery.ValidateRequestAsync(context); + + var user = string.IsNullOrEmpty(username) ? null : await userManager.FindByNameAsync(username); + var optionsJson = await signInManager.MakePasskeyRequestOptionsAsync(user); + + return TypedResults.Content(optionsJson, contentType: "application/json"); +}); +``` + +## Step 7: Update the Login page + +*The implementation must be supplied by the developer at this time.* + +## Step 8: Create passkey management pages for adding, deleting, and renaming passkeys + +*The implementation must be supplied by the developer at this time.* + +## Step 9: Update the manage navigation menu + +*The implementation must be supplied by the developer at this time.* + +Add a link to the passkey management page in the app's navigation menu. + +## Step 10: Include the JavaScript file + +*The implementation must be supplied by the developer at this time.* + +Add a reference to the `PasskeySubmit` JavaScript file: + +```html + +``` + +## Step 11: Test the implementation + +* Run the app and navigate to the login page. +* Test logging in with a passkey. +* Navigate to passkeys management page to add, rename, or delete passkeys. +* 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) +* [Passkeys configuration guidance](xref:blazor/security/passkeys#configure-passkey-options) diff --git a/aspnetcore/migration/90-to-100/includes/blazor-web-app-passkeys.md b/aspnetcore/migration/90-to-100/includes/blazor-web-app-passkeys.md new file mode 100644 index 000000000000..ff6ce0b1a0f2 --- /dev/null +++ b/aspnetcore/migration/90-to-100/includes/blazor-web-app-passkeys.md @@ -0,0 +1,843 @@ +--- +title: Implement passkeys in an existing Blazor Web App +author: guardrex +description: Learn how to implement passkeys authentication in an ASP.NET Core Blazor Web App. +ms.author: wpickett +ms.custom: mvc +ms.date: 8/14/2025 +uid: migration/blazor-web-app/passkeys +--- +# Implement passkeys in an existing Blazor Web App + +This guide explains how to add [passkey support](xref:blazor/security/passkeys) to an existing Blazor Web App that has ASP.NET Core Identity authentication configured. + +The guidance in this article 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 with ASP.NET Core Identity +* [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) + +## Step 1: Update to .NET 10 + +Update the app's project file to target .NET 10: + +```xml +net10.0 +``` + +Update all `Microsoft.AspNetCore.*` and `Microsoft.EntityFrameworkCore.*` packages to their latest .NET 10 versions. + +## Step 2: 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(); +``` + +# [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 +``` + +--- + +## Step 3: Create passkey model classes + +Add the following model classes to the project in the `Components/Account` folder. + +Replace the `{NAMESPACE}` placeholder in the following examples with the app's namespace. For example, `Contoso` for `Contoso.Components.Account` in the following examples. + +`Components/Account/PasskeyInputModel.cs`: + +```csharp +namespace {NAMESPACE}.Components.Account; + +public class PasskeyInputModel +{ + public string? CredentialJson { get; set; } + public string? Error { get; set; } +} +``` + +`Components/Account/PasskeyOperation.cs`: + +```csharp +namespace {NAMESPACE}.Components.Account; + +public enum PasskeyOperation +{ + Create = 0, + Request = 1, +} +``` + +## Step 4: Create the `PasskeySubmit` component + +Add the following `PasskeySubmit` component to handle passkey operations. + +`Components/Account/Shared/PasskeySubmit.razor`: + +```razor +@using Microsoft.AspNetCore.Antiforgery +@inject IServiceProvider Services + + + + + +@code { + private AntiforgeryTokenSet? tokens; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [Parameter] + [EditorRequired] + public PasskeyOperation Operation { get; set; } + + [Parameter] + [EditorRequired] + public string Name { get; set; } = default!; + + [Parameter] + public string? EmailName { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter(CaptureUnmatchedValues = true)] + public IDictionary? AdditionalAttributes { get; set; } + + protected override void OnInitialized() + { + tokens = Services.GetRequiredService()?.GetTokens(HttpContext); + } +} +``` + +## Step 5: Add the JavaScript for passkey operations + +Add the following JavaScript file to handle WebAuthn API interactions. + +`Components/Account/Shared/PasskeySubmit.razor.js`: + +```javascript +const browserSupportsPasskeys = + typeof navigator.credentials !== 'undefined' && + typeof window.PublicKeyCredential !== 'undefined' && + typeof window.PublicKeyCredential.parseCreationOptionsFromJSON === 'function' && + typeof window.PublicKeyCredential.parseRequestOptionsFromJSON === 'function'; + +async function fetchWithErrorHandling(url, options = {}) { + const response = await fetch(url, { + credentials: 'include', + ...options + }); + if (!response.ok) { + const text = await response.text(); + console.error(text); + throw new Error(`The server responded with status ${response.status}.`); + } + return response; +} + +async function createCredential(headers, signal) { + 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 }); +} + +async function requestCredential(email, mediation, headers, signal) { + 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 }); +} + +customElements.define('passkey-submit', class extends HTMLElement { + static formAssociated = true; + + connectedCallback() { + this.internals = this.attachInternals(); + this.attrs = { + operation: this.getAttribute('operation'), + name: this.getAttribute('name'), + emailName: this.getAttribute('email-name'), + requestTokenName: this.getAttribute('request-token-name'), + requestTokenValue: this.getAttribute('request-token-value'), + }; + + this.internals.form.addEventListener('submit', (event) => { + if (event.submitter?.name === '__passkeySubmit') { + event.preventDefault(); + this.obtainAndSubmitCredential(); + } + }); + + this.tryAutofillPasskey(); + } + + disconnectedCallback() { + this.abortController?.abort(); + } + + async obtainCredential(useConditionalMediation, signal) { + if (!browserSupportsPasskeys) { + throw new Error('Some passkey features are missing. Please update your browser.'); + } + + const headers = { + [this.attrs.requestTokenName]: this.attrs.requestTokenValue, + }; + + if (this.attrs.operation === 'Create') { + return await createCredential(headers, signal); + } else if (this.attrs.operation === 'Request') { + const email = new FormData(this.internals.form).get(this.attrs.emailName); + const mediation = useConditionalMediation ? 'conditional' : undefined; + return await requestCredential(email, mediation, headers, signal); + } else { + throw new Error(`Unknown passkey operation '${this.attrs.operation}'.`); + } + } + + async obtainAndSubmitCredential(useConditionalMediation = false) { + this.abortController?.abort(); + this.abortController = new AbortController(); + const signal = this.abortController.signal; + const formData = new FormData(); + try { + const credential = await this.obtainCredential(useConditionalMediation, signal); + const credentialJson = JSON.stringify(credential); + formData.append(`${this.attrs.name}.CredentialJson`, credentialJson); + } catch (error) { + if (error.name === 'AbortError') { + return; + } + console.error(error); + if (useConditionalMediation) { + return; + } + const errorMessage = error.name === 'NotAllowedError' + ? 'No passkey was provided by the authenticator.' + : error.message; + formData.append(`${this.attrs.name}.Error`, errorMessage); + } + this.internals.setFormValue(formData); + this.internals.form.submit(); + } + + async tryAutofillPasskey() { + if (browserSupportsPasskeys && this.attrs.operation === 'Request' && await PublicKeyCredential.isConditionalMediationAvailable?.()) { + await this.obtainAndSubmitCredential(true); + } + } +}); +``` + +## Step 6: Add passkey endpoints + +Update the `IdentityComponentsEndpointRouteBuilderExtensions.cs` file (or create the file if it doesn't exist) to include the passkey-specific endpoints: + +```csharp +// Add the following endpoints to the existing 'MapAdditionalIdentityEndpoints' method + +accountGroup.MapPost("/PasskeyCreationOptions", async ( + HttpContext context, + [FromServices] UserManager userManager, + [FromServices] SignInManager signInManager, + [FromServices] IAntiforgery antiforgery) => +{ + await antiforgery.ValidateRequestAsync(context); + + var user = await userManager.GetUserAsync(context.User); + if (user is null) + { + return Results.NotFound($"Unable to load user with ID '{userManager.GetUserId(context.User)}'."); + } + + 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"); +}); + +accountGroup.MapPost("/PasskeyRequestOptions", async ( + HttpContext context, + [FromServices] UserManager userManager, + [FromServices] SignInManager signInManager, + [FromServices] IAntiforgery antiforgery, + [FromQuery] string? username) => +{ + await antiforgery.ValidateRequestAsync(context); + + var user = string.IsNullOrEmpty(username) ? null : await userManager.FindByNameAsync(username); + var optionsJson = await signInManager.MakePasskeyRequestOptionsAsync(user); + + return TypedResults.Content(optionsJson, contentType: "application/json"); +}); +``` + +## Step 7: Update the Login page + +Replace the existing `Login` component with the following component. The passkey-specific additions are described by Razor comments (`@* ... *@`) in the file. + +`Components/Account/Pages/Login.razor`: + +```razor +@page "/Account/Login" + +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Authentication +@using Microsoft.AspNetCore.Identity +@using YourApp.Data + +@inject UserManager UserManager +@inject SignInManager SignInManager +@inject ILogger Logger +@inject NavigationManager NavigationManager +@inject IdentityRedirectManager RedirectManager + +Log in + +

Log in

+
+
+
+ + @* The EditContext is used instead of Model to allow conditional validation *@ + + +

Use a local account to log in.

+
+ +
+ @* Note the autocomplete="username webauthn" to enable passkey autofill *@ + + + +
+
+ + + +
+
+ +
+
+ +
+
+ @* Passkey login button *@ +
+ OR + Log in with a passkey +
+
+ +
+
+
+
+
+

Use another service to log in.

+
+ +
+
+
+ +@code { + private string? errorMessage; + private EditContext editContext = default!; // EditContext for conditional validation + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = default!; + + [SupplyParameterFromQuery] + private string? ReturnUrl { get; set; } + + protected override async Task OnInitializedAsync() + { + Input ??= new(); + + editContext = new EditContext(Input); // Initialize EditContext + + if (HttpMethods.IsGet(HttpContext.Request.Method)) + { + // Clear the existing external cookie to ensure a clean login process + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + } + } + + public async Task LoginUser() + { + // Handle passkey errors + if (!string.IsNullOrEmpty(Input.Passkey?.Error)) + { + errorMessage = $"Error: {Input.Passkey.Error}"; + return; + } + + SignInResult result; + + // Check if this is a passkey sign-in + if (!string.IsNullOrEmpty(Input.Passkey?.CredentialJson)) + { + // When performing passkey sign-in, don't perform form validation. + result = await SignInManager.PasskeySignInAsync(Input.Passkey.CredentialJson); + } + else + { + // If doing a password sign-in, validate the form. + if (!editContext.Validate()) + { + return; + } + + // This doesn't count login failures towards account lockout + // To enable password failures to trigger account lockout, set lockoutOnFailure: true + result = await SignInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false); + } + + if (result.Succeeded) + { + Logger.LogInformation("User logged in."); + RedirectManager.RedirectTo(ReturnUrl); + } + else if (result.RequiresTwoFactor) + { + RedirectManager.RedirectTo( + "Account/LoginWith2fa", + new() { ["returnUrl"] = ReturnUrl, ["rememberMe"] = Input.RememberMe }); + } + else if (result.IsLockedOut) + { + Logger.LogWarning("User account locked out."); + RedirectManager.RedirectTo("Account/Lockout"); + } + else + { + errorMessage = "Error: Invalid login attempt."; + } + } + + private sealed class InputModel + { + [Required] + [EmailAddress] + public string Email { get; set; } = ""; + + [Required] + [DataType(DataType.Password)] + public string Password { get; set; } = ""; + + [Display(Name = "Remember me?")] + public bool RememberMe { get; set; } + + // Passkey input model + public PasskeyInputModel? Passkey { get; set; } + } +} +``` + +## Step 8: Create passkey management pages for adding and renaming passkeys + +Add the following `Passkeys` component for managing passkeys. + +`Components/Account/Pages/Manage/Passkeys.razor`: + +```razor +@page "/Account/Manage/Passkeys" + +@using YourApp.Data +@using Microsoft.AspNetCore.Identity +@using System.Buffers.Text + +@inject UserManager UserManager +@inject SignInManager SignInManager +@inject IdentityRedirectManager RedirectManager + +Manage your passkeys + +

Manage your passkeys

+ + + +@if (currentPasskeys is { Count: > 0 }) +{ + + + @foreach (var passkey in currentPasskeys) + { + + + + + } + +
@(passkey.Name ?? "Unnamed passkey") + @{ + var credentialId = Base64Url.EncodeToString(passkey.CredentialId); + } +
+ +
+ + + +
+ +
+} +else +{ +

No passkeys are registered.

+} + +
+ + Add a new passkey + + +@code { + private ApplicationUser? user; + private IList? currentPasskeys; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromForm] + private string? Action { get; set; } + + [SupplyParameterFromForm] + private string? CredentialId { get; set; } + + [SupplyParameterFromForm(FormName = "add-passkey")] + private PasskeyInputModel Input { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + Input ??= new(); + + user = await UserManager.GetUserAsync(HttpContext.User); + if (user is null) + { + RedirectManager.RedirectToInvalidUser(UserManager, HttpContext); + return; + } + currentPasskeys = await UserManager.GetPasskeysAsync(user); + } + + private async Task AddPasskey() + { + if (user is null) + { + RedirectManager.RedirectToInvalidUser(UserManager, HttpContext); + return; + } + + if (!string.IsNullOrEmpty(Input.Error)) + { + RedirectManager.RedirectToCurrentPageWithStatus($"Error: {Input.Error}", HttpContext); + return; + } + + if (string.IsNullOrEmpty(Input.CredentialJson)) + { + RedirectManager.RedirectToCurrentPageWithStatus("Error: The browser did not provide a passkey.", HttpContext); + return; + } + + var attestationResult = await SignInManager.PerformPasskeyAttestationAsync(Input.CredentialJson); + if (!attestationResult.Succeeded) + { + RedirectManager.RedirectToCurrentPageWithStatus($"Error: Could not add the passkey: {attestationResult.Failure.Message}", HttpContext); + return; + } + + var addPasskeyResult = await UserManager.AddOrUpdatePasskeyAsync(user, attestationResult.Passkey); + if (!addPasskeyResult.Succeeded) + { + RedirectManager.RedirectToCurrentPageWithStatus("Error: The passkey could not be added to your account.", HttpContext); + return; + } + + // Immediately prompt the user to enter a name for the credential + var credentialIdBase64Url = Base64Url.EncodeToString(attestationResult.Passkey.CredentialId); + RedirectManager.RedirectTo($"Account/Manage/RenamePasskey/{credentialIdBase64Url}"); + } + + private async Task UpdatePasskey() + { + switch (Action) + { + case "rename": + RedirectManager.RedirectTo($"Account/Manage/RenamePasskey/{CredentialId}"); + break; + case "delete": + await DeletePasskey(); + break; + default: + RedirectManager.RedirectToCurrentPageWithStatus($"Error: Unknown action '{Action}'.", HttpContext); + break; + } + } + + private async Task DeletePasskey() + { + if (user is null) + { + RedirectManager.RedirectToInvalidUser(UserManager, HttpContext); + return; + } + + byte[] credentialId; + try + { + credentialId = Base64Url.DecodeFromChars(CredentialId); + } + catch (FormatException) + { + RedirectManager.RedirectToCurrentPageWithStatus("Error: The specified passkey ID had an invalid format.", HttpContext); + return; + } + + var result = await UserManager.RemovePasskeyAsync(user, credentialId); + if (!result.Succeeded) + { + RedirectManager.RedirectToCurrentPageWithStatus("Error: The passkey could not be deleted.", HttpContext); + return; + } + + RedirectManager.RedirectToCurrentPageWithStatus("Passkey deleted successfully.", HttpContext); + } +} +``` + +Add the following component for renaming passkeys. + +`Components/Account/Pages/Manage/RenamePasskey.razor`: + +```razor +@page "/Account/Manage/RenamePasskey/{Id}" + +@using YourApp.Data +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Identity +@using System.Buffers.Text + +@inject UserManager UserManager +@inject IdentityRedirectManager RedirectManager + + + + @if (passkey?.Name is { } name) + { +

Enter a new name for your "@name" passkey

+ } + else + { +

Enter a name for your passkey

+ } +
+ +
+ + + +
+
+ +
+
+ +@code { + private ApplicationUser? user; + private UserPasskeyInfo? passkey; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [Parameter] + public string? Id { get; set; } + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + Input ??= new(); + + user = (await UserManager.GetUserAsync(HttpContext.User))!; + if (user is null) + { + RedirectManager.RedirectToInvalidUser(UserManager, HttpContext); + return; + } + + byte[] credentialId; + try + { + credentialId = Base64Url.DecodeFromChars(Id); + } + catch (FormatException) + { + RedirectManager.RedirectToWithStatus("Account/Manage/Passkeys", "Error: The specified passkey ID had an invalid format.", HttpContext); + return; + } + + passkey = await UserManager.GetPasskeyAsync(user, credentialId); + if (passkey is null) + { + RedirectManager.RedirectToWithStatus("Account/Manage/Passkeys", "Error: The specified passkey could not be found.", HttpContext); + return; + } + } + + private async Task Rename() + { + passkey!.Name = Input.Name; + var result = await UserManager.AddOrUpdatePasskeyAsync(user!, passkey); + if (!result.Succeeded) + { + RedirectManager.RedirectToWithStatus("Account/Manage/Passkeys", "Error: The passkey could not be updated.", HttpContext); + return; + } + + RedirectManager.RedirectToWithStatus("Account/Manage/Passkeys", "Passkey updated successfully.", HttpContext); + } + + private sealed class InputModel + { + [Required] + public string Name { get; set; } = ""; + } +} +``` + +## Step 9: 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 ++ +``` + +## Step 10: 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 + ++ +``` + +## Step 11: Test the implementation + +* Run the app and navigate to the login page. +* Test logging in with a passkey using the **Log in with a passkey** button. +* Navigate to `Account/Manage/Passkeys` to add, rename, or delete passkeys. +* 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) +* [Passkeys configuration guidance](xref:blazor/security/passkeys#configure-passkey-options) 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..7bcedcbd22f8 --- /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/release-notes/aspnetcore-10/includes/blazor.md b/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md index a1d7cc99dfbe..3c4fb5bef0cd 100644 --- a/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md +++ b/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md @@ -601,13 +601,14 @@ For more information, see +* -We plan to publish migration guidance for existing apps by Friday, August 15. +Partial implementation guidance for existing Razor Pages and MVC apps is available in . ### Circuit state persistence diff --git a/aspnetcore/toc.yml b/aspnetcore/toc.yml index b52599b6b67b..b921004cec0d 100644 --- a/aspnetcore/toc.yml +++ b/aspnetcore/toc.yml @@ -628,6 +628,10 @@ items: uid: blazor/security/account-confirmation-and-password-recovery - name: QR codes with TOTP uid: blazor/security/qrcodes-for-authenticator-apps + - name: Passkeys + uid: blazor/security/passkeys + - name: Implement passkeys in an existing Blazor Web App + uid: migration/blazor-web-app/passkeys - name: Content Security Policy uid: blazor/security/content-security-policy - name: EU General Data Protection Regulation (GDPR) support @@ -2151,6 +2155,10 @@ items: uid: migration/fx-to-core/examples/configuration - name: Identity uid: migration/fx-to-core/examples/identity + - name: Implement passkeys in an existing Blazor Web App + uid: migration/blazor-web-app/passkeys + - name: Implement passkeys in an existing Razor Pages or MVC app + uid: migration/aspnetcore-app/passkeys - name: API reference href: /dotnet/api/ - name: Contribute From e6cdb5356a79669f6787899036160a19ffabf258 Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Thu, 14 Aug 2025 08:39:28 -0400 Subject: [PATCH 02/18] Updates --- .../security/passkeys-migration.md} | 2 +- aspnetcore/blazor/security/passkeys.md | 8 +++----- aspnetcore/migration/90-to-100.md | 2 +- aspnetcore/migration/90-to-100/includes/blazor.md | 2 +- aspnetcore/release-notes/aspnetcore-10/includes/blazor.md | 4 ++-- .../authentication/passkeys-migration.md} | 2 +- aspnetcore/toc.yml | 8 +++----- 7 files changed, 12 insertions(+), 16 deletions(-) rename aspnetcore/{migration/90-to-100/includes/blazor-web-app-passkeys.md => blazor/security/passkeys-migration.md} (99%) rename aspnetcore/{migration/90-to-100/includes/aspnetcore-app-passkeys.md => security/authentication/passkeys-migration.md} (99%) diff --git a/aspnetcore/migration/90-to-100/includes/blazor-web-app-passkeys.md b/aspnetcore/blazor/security/passkeys-migration.md similarity index 99% rename from aspnetcore/migration/90-to-100/includes/blazor-web-app-passkeys.md rename to aspnetcore/blazor/security/passkeys-migration.md index ff6ce0b1a0f2..4ea3492fb77c 100644 --- a/aspnetcore/migration/90-to-100/includes/blazor-web-app-passkeys.md +++ b/aspnetcore/blazor/security/passkeys-migration.md @@ -5,7 +5,7 @@ description: Learn how to implement passkeys authentication in an ASP.NET Core B ms.author: wpickett ms.custom: mvc ms.date: 8/14/2025 -uid: migration/blazor-web-app/passkeys +uid: blazor/security/passkeys-migration --- # Implement passkeys in an existing Blazor Web App diff --git a/aspnetcore/blazor/security/passkeys.md b/aspnetcore/blazor/security/passkeys.md index db7b60375b61..ae65b1dd6baa 100644 --- a/aspnetcore/blazor/security/passkeys.md +++ b/aspnetcore/blazor/security/passkeys.md @@ -9,15 +9,13 @@ uid: blazor/security/passkeys --- # Enable Web Authentication API (WebAuthn) passkeys in an ASP.NET Core Blazor Web App - - -[!INCLUDE[](~/includes/not-latest-version.md)] +[!INCLUDE[](~/includes/not-latest-version-without-not-supported-content.md)] 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/en-us/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 Blazor Web App to use passkeys to authenticate users. -The guidance in this article relies upon creating a .NET 10 or later Blazor Web App with **Individual Accounts** for the app's **Authentication type** or [scaffolding Identity](xref:security/authentication/scaffold-identity#scaffold-identity-into-a-blazor-project) into an existing .NET 10 or later Blazor Web App. For migration guidance from prior versions of .NET, see . +The guidance in this article relies upon creating a .NET 10 or later Blazor Web App with **Individual Accounts** for the app's **Authentication type** or [scaffolding Identity](xref:security/authentication/scaffold-identity#scaffold-identity-into-a-blazor-project) into an existing .NET 10 or later Blazor Web App. For migration guidance from prior versions of .NET, see . ## What are passkeys? @@ -142,7 +140,7 @@ The Blazor Web App template enforces these limits by default. ## Create or migrate a Blazor Web App -For migration guidance to update an existing Blazor Web App to use passkeys, see . +For migration guidance to update an existing Blazor Web App to use passkeys, see . Use the following guidance to create a new Blazor Web App with passkeys support. diff --git a/aspnetcore/migration/90-to-100.md b/aspnetcore/migration/90-to-100.md index 9ac16f230c48..d21bc68f088c 100644 --- a/aspnetcore/migration/90-to-100.md +++ b/aspnetcore/migration/90-to-100.md @@ -81,4 +81,4 @@ In the project file, update each [`Microsoft.AspNetCore.*`](https://www.nuget.or ### Adopt passkey user authentication in an existing Razor Pages or MVC app -For guidance, see . +For guidance, see . diff --git a/aspnetcore/migration/90-to-100/includes/blazor.md b/aspnetcore/migration/90-to-100/includes/blazor.md index 7bcedcbd22f8..79a7c6d1cc77 100644 --- a/aspnetcore/migration/90-to-100/includes/blazor.md +++ b/aspnetcore/migration/90-to-100/includes/blazor.md @@ -2,4 +2,4 @@ Complete migration coverage for Blazor apps is scheduled for September and Octob ### Adopt passkey user authentication in an existing Blazor Web App -For guidance, see . +For guidance, see . diff --git a/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md b/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md index 3c4fb5bef0cd..cee31faef8c8 100644 --- a/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md +++ b/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md @@ -606,9 +606,9 @@ The Blazor Web App project template provides out-of-the-box passkey management a For more information, see the following articles: * -* +* -Partial implementation guidance for existing Razor Pages and MVC apps is available in . +Partial implementation guidance for existing Razor Pages and MVC apps is available in . ### Circuit state persistence diff --git a/aspnetcore/migration/90-to-100/includes/aspnetcore-app-passkeys.md b/aspnetcore/security/authentication/passkeys-migration.md similarity index 99% rename from aspnetcore/migration/90-to-100/includes/aspnetcore-app-passkeys.md rename to aspnetcore/security/authentication/passkeys-migration.md index cc7c502b3e1c..27b85f8abbd3 100644 --- a/aspnetcore/migration/90-to-100/includes/aspnetcore-app-passkeys.md +++ b/aspnetcore/security/authentication/passkeys-migration.md @@ -5,7 +5,7 @@ description: Learn how to implement passkeys authentication in an ASP.NET Core R ms.author: wpickett ms.custom: mvc ms.date: 8/14/2025 -uid: migration/aspnetcore-app/passkeys +uid: security/authentication/passkeys-migration --- # Implement passkeys in an existing Razor Pages or MVC app diff --git a/aspnetcore/toc.yml b/aspnetcore/toc.yml index b921004cec0d..67c3b48a3854 100644 --- a/aspnetcore/toc.yml +++ b/aspnetcore/toc.yml @@ -631,7 +631,7 @@ items: - name: Passkeys uid: blazor/security/passkeys - name: Implement passkeys in an existing Blazor Web App - uid: migration/blazor-web-app/passkeys + uid: blazor/security/passkeys-migration - name: Content Security Policy uid: blazor/security/content-security-policy - name: EU General Data Protection Regulation (GDPR) support @@ -1718,6 +1718,8 @@ items: uid: security/authentication/identity-enable-qrcodes - name: Two-factor authentication with SMS uid: security/authentication/2fa + - name: Implement passkeys in an existing Razor Pages or MVC app + uid: security/authentication/passkeys-migration - name: External authentication providers items: - name: Overview @@ -2155,10 +2157,6 @@ items: uid: migration/fx-to-core/examples/configuration - name: Identity uid: migration/fx-to-core/examples/identity - - name: Implement passkeys in an existing Blazor Web App - uid: migration/blazor-web-app/passkeys - - name: Implement passkeys in an existing Razor Pages or MVC app - uid: migration/aspnetcore-app/passkeys - name: API reference href: /dotnet/api/ - name: Contribute From 02c805e0d8805712fe46f9e91ad4db345cd50b6a Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Thu, 14 Aug 2025 08:48:51 -0400 Subject: [PATCH 03/18] Updates --- aspnetcore/blazor/security/passkeys.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/aspnetcore/blazor/security/passkeys.md b/aspnetcore/blazor/security/passkeys.md index ae65b1dd6baa..231f59bb11f8 100644 --- a/aspnetcore/blazor/security/passkeys.md +++ b/aspnetcore/blazor/security/passkeys.md @@ -9,8 +9,13 @@ uid: blazor/security/passkeys --- # Enable Web Authentication API (WebAuthn) passkeys in an ASP.NET Core Blazor Web App + + 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/en-us/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 Blazor Web App to use passkeys to authenticate users. From 8a8ba497c710af4d0b26f8c12f1b20af4cb8d338 Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Thu, 14 Aug 2025 09:06:32 -0400 Subject: [PATCH 04/18] Updates --- aspnetcore/blazor/security/passkeys.md | 2 +- aspnetcore/migration/90-to-100.md | 2 +- aspnetcore/release-notes/aspnetcore-10.0.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/aspnetcore/blazor/security/passkeys.md b/aspnetcore/blazor/security/passkeys.md index 231f59bb11f8..c21f07b4c9a4 100644 --- a/aspnetcore/blazor/security/passkeys.md +++ b/aspnetcore/blazor/security/passkeys.md @@ -4,7 +4,7 @@ author: guardrex description: Discover how to enable Web Authentication API (WebAuthn) passkeys with ASP.NET Core Blazor Web App authentication. ms.author: wpickett monikerRange: '>= aspnetcore-10.0' -ms.date: 08/14/2024 +ms.date: 08/14/2025 uid: blazor/security/passkeys --- # Enable Web Authentication API (WebAuthn) passkeys in an ASP.NET Core Blazor Web App diff --git a/aspnetcore/migration/90-to-100.md b/aspnetcore/migration/90-to-100.md index d21bc68f088c..397f015725f5 100644 --- a/aspnetcore/migration/90-to-100.md +++ b/aspnetcore/migration/90-to-100.md @@ -3,7 +3,7 @@ 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/13/2025 +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 diff --git a/aspnetcore/release-notes/aspnetcore-10.0.md b/aspnetcore/release-notes/aspnetcore-10.0.md index 70fc8d2c1a2c..10cf1f1428bb 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: 8/11/2025 +ms.date: 8/12/2025 uid: aspnetcore-10 --- # What's new in ASP.NET Core in .NET 10 From a0a1aa6682b05b124e4fb536841f939e0eda0ae9 Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Thu, 14 Aug 2025 09:09:25 -0400 Subject: [PATCH 05/18] Updates --- aspnetcore/blazor/security/passkeys-migration.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/aspnetcore/blazor/security/passkeys-migration.md b/aspnetcore/blazor/security/passkeys-migration.md index 4ea3492fb77c..871f995d2bf8 100644 --- a/aspnetcore/blazor/security/passkeys-migration.md +++ b/aspnetcore/blazor/security/passkeys-migration.md @@ -352,6 +352,8 @@ accountGroup.MapPost("/PasskeyRequestOptions", async ( Replace the existing `Login` component with the following component. The passkey-specific additions are described by Razor comments (`@* ... *@`) in the file. +Replace the `{NAMESPACE}` placeholder in the following example with the app's namespace. For example, `Contoso` for `Contoso.Data` in the following examples. + `Components/Account/Pages/Login.razor`: ```razor @@ -360,7 +362,7 @@ Replace the existing `Login` component with the following component. The passkey @using System.ComponentModel.DataAnnotations @using Microsoft.AspNetCore.Authentication @using Microsoft.AspNetCore.Identity -@using YourApp.Data +@using {NAMESPACE}.Data @inject UserManager UserManager @inject SignInManager SignInManager @@ -535,12 +537,14 @@ Replace the existing `Login` component with the following component. The passkey Add the following `Passkeys` component for managing passkeys. +Replace the `{NAMESPACE}` placeholder in the following examples with the app's namespace. For example, `Contoso` for `Contoso.Data` in the following examples. + `Components/Account/Pages/Manage/Passkeys.razor`: ```razor @page "/Account/Manage/Passkeys" -@using YourApp.Data +@using {NAMESPACE}.Data @using Microsoft.AspNetCore.Identity @using System.Buffers.Text @@ -707,12 +711,14 @@ else Add the following component for renaming passkeys. + + `Components/Account/Pages/Manage/RenamePasskey.razor`: ```razor @page "/Account/Manage/RenamePasskey/{Id}" -@using YourApp.Data +@using {NAMESPACE}.Data @using System.ComponentModel.DataAnnotations @using Microsoft.AspNetCore.Identity @using System.Buffers.Text From 146c7fc88d7a8b6ad00ca987451ccbe25b0d4eab Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Mon, 18 Aug 2025 08:10:11 -0400 Subject: [PATCH 06/18] Updates --- aspnetcore/migration/90-to-100.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/aspnetcore/migration/90-to-100.md b/aspnetcore/migration/90-to-100.md index 397f015725f5..5e16e726b46a 100644 --- a/aspnetcore/migration/90-to-100.md +++ b/aspnetcore/migration/90-to-100.md @@ -82,3 +82,7 @@ In the project file, update each [`Microsoft.AspNetCore.*`](https://www.nuget.or ### Adopt passkey user authentication in an existing Razor Pages or MVC app For guidance, see . + +## 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. From ee805c8aef668250d3d01e3443167eb31a60f5ac Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Mon, 18 Aug 2025 09:30:50 -0400 Subject: [PATCH 07/18] Updates --- .../blazor/security/passkeys-migration.md | 9 +- aspnetcore/blazor/security/passkeys.md | 5 +- aspnetcore/migration/90-to-100.md | 4 - .../aspnetcore-10/includes/blazor.md | 2 - .../authentication/passkeys-migration.md | 334 ------------------ aspnetcore/toc.yml | 2 - 6 files changed, 10 insertions(+), 346 deletions(-) delete mode 100644 aspnetcore/security/authentication/passkeys-migration.md diff --git a/aspnetcore/blazor/security/passkeys-migration.md b/aspnetcore/blazor/security/passkeys-migration.md index 871f995d2bf8..1ad7436074c0 100644 --- a/aspnetcore/blazor/security/passkeys-migration.md +++ b/aspnetcore/blazor/security/passkeys-migration.md @@ -164,7 +164,7 @@ Add the following `PasskeySubmit` component to handle passkey operations. protected override void OnInitialized() { - tokens = Services.GetRequiredService()?.GetTokens(HttpContext); + tokens = Services.GetService()?.GetTokens(HttpContext); } } ``` @@ -838,9 +838,12 @@ In `Components/App.razor`: ## Step 11: Test the implementation * Run the app and navigate to the login page. -* Test logging in with a passkey using the **Log in with a passkey** button. +* Log in with a username and password. +* Register a passkey. +* Sign out of the app. +* Sign back into the app with a passkey using the **Log in with a passkey** button. * Navigate to `Account/Manage/Passkeys` to add, rename, or delete passkeys. -* Test the passkey autofill feature by selecting the email input field when you have saved passkeys. +* If the passkey supports autofill, test the autofill feature by selecting the email input field when you have saved passkeys. ## Additional resources diff --git a/aspnetcore/blazor/security/passkeys.md b/aspnetcore/blazor/security/passkeys.md index c21f07b4c9a4..fe4f0eadfc35 100644 --- a/aspnetcore/blazor/security/passkeys.md +++ b/aspnetcore/blazor/security/passkeys.md @@ -651,7 +651,10 @@ The `PasskeySignInAsync` method internally calls `PerformPasskeyAssertionAsync` 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. This returns a `PasskeyAssertionResult` containing the authenticated user and updated passkey information. +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 diff --git a/aspnetcore/migration/90-to-100.md b/aspnetcore/migration/90-to-100.md index 5e16e726b46a..298cdf80229e 100644 --- a/aspnetcore/migration/90-to-100.md +++ b/aspnetcore/migration/90-to-100.md @@ -79,10 +79,6 @@ In the project file, update each [`Microsoft.AspNetCore.*`](https://www.nuget.or [!INCLUDE[](~/migration/90-to-100/includes/blazor.md)] -### Adopt passkey user authentication in an existing Razor Pages or MVC app - -For guidance, see . - ## 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/release-notes/aspnetcore-10/includes/blazor.md b/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md index 0f4d84eee1f3..d49e4268d37d 100644 --- a/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md +++ b/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md @@ -608,8 +608,6 @@ For more information, see the following articles: * * -Partial implementation guidance for existing Razor Pages and MVC apps is available in . - ### Circuit state persistence During server-side rendering, Blazor Web Apps can now persist a user's session (circuit) state when the connection to the server is lost for an extended period of time or proactively paused, as long as a full-page refresh isn't triggered. This allows users to resume their session without losing unsaved work in the following scenarios: diff --git a/aspnetcore/security/authentication/passkeys-migration.md b/aspnetcore/security/authentication/passkeys-migration.md deleted file mode 100644 index 27b85f8abbd3..000000000000 --- a/aspnetcore/security/authentication/passkeys-migration.md +++ /dev/null @@ -1,334 +0,0 @@ ---- -title: Implement passkeys in an existing ASP.NET Core Razor Pages or MVC app -author: guardrex -description: Learn how to implement passkeys authentication in an ASP.NET Core Razor Pages or MVC app. -ms.author: wpickett -ms.custom: mvc -ms.date: 8/14/2025 -uid: security/authentication/passkeys-migration ---- -# Implement passkeys in an existing Razor Pages or MVC app - -This guide walks through adding passkey support to an existing Razor Pages or MVC app that already has ASP.NET Core Identity authentication configured. - -The guidance in this article 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). - -## Prerequisites - - - -* An existing Razor Pages or MVC app with ASP.NET Core Identity -* [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) - -## Step 1: Update to .NET 10 - -Update the app's project file to target .NET 10: - -```xml -net10.0 -``` - -Update all `Microsoft.AspNetCore.*` and `Microsoft.EntityFrameworkCore.*` packages to their latest .NET 10 versions. - -## Step 2: 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(); -``` - -# [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 -``` - ---- - -## Step 3: Create passkey model classes - -Add the following model classes to the project. - -`PasskeyInputModel.cs`: - -```csharp -public class PasskeyInputModel -{ - public string? CredentialJson { get; set; } - public string? Error { get; set; } -} -``` - -`PasskeyOperation.cs`: - -```csharp -public enum PasskeyOperation -{ - Create = 0, - Request = 1, -} -``` - -## Step 4: Create a UI component to handle passkey operations - -*The implementation must be supplied by the developer at this time.* - -## Step 5: Add the JavaScript for passkey operations - -Add the following JavaScript file to handle WebAuthn API interactions. - -`PasskeySubmit.js`: - -```javascript -const browserSupportsPasskeys = - typeof navigator.credentials !== 'undefined' && - typeof window.PublicKeyCredential !== 'undefined' && - typeof window.PublicKeyCredential.parseCreationOptionsFromJSON === 'function' && - typeof window.PublicKeyCredential.parseRequestOptionsFromJSON === 'function'; - -async function fetchWithErrorHandling(url, options = {}) { - const response = await fetch(url, { - credentials: 'include', - ...options - }); - if (!response.ok) { - const text = await response.text(); - console.error(text); - throw new Error(`The server responded with status ${response.status}.`); - } - return response; -} - -async function createCredential(headers, signal) { - 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 }); -} - -async function requestCredential(email, mediation, headers, signal) { - 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 }); -} - -customElements.define('passkey-submit', class extends HTMLElement { - static formAssociated = true; - - connectedCallback() { - this.internals = this.attachInternals(); - this.attrs = { - operation: this.getAttribute('operation'), - name: this.getAttribute('name'), - emailName: this.getAttribute('email-name'), - requestTokenName: this.getAttribute('request-token-name'), - requestTokenValue: this.getAttribute('request-token-value'), - }; - - this.internals.form.addEventListener('submit', (event) => { - if (event.submitter?.name === '__passkeySubmit') { - event.preventDefault(); - this.obtainAndSubmitCredential(); - } - }); - - this.tryAutofillPasskey(); - } - - disconnectedCallback() { - this.abortController?.abort(); - } - - async obtainCredential(useConditionalMediation, signal) { - if (!browserSupportsPasskeys) { - throw new Error('Some passkey features are missing. Please update your browser.'); - } - - const headers = { - [this.attrs.requestTokenName]: this.attrs.requestTokenValue, - }; - - if (this.attrs.operation === 'Create') { - return await createCredential(headers, signal); - } else if (this.attrs.operation === 'Request') { - const email = new FormData(this.internals.form).get(this.attrs.emailName); - const mediation = useConditionalMediation ? 'conditional' : undefined; - return await requestCredential(email, mediation, headers, signal); - } else { - throw new Error(`Unknown passkey operation '${this.attrs.operation}'.`); - } - } - - async obtainAndSubmitCredential(useConditionalMediation = false) { - this.abortController?.abort(); - this.abortController = new AbortController(); - const signal = this.abortController.signal; - const formData = new FormData(); - try { - const credential = await this.obtainCredential(useConditionalMediation, signal); - const credentialJson = JSON.stringify(credential); - formData.append(`${this.attrs.name}.CredentialJson`, credentialJson); - } catch (error) { - if (error.name === 'AbortError') { - return; - } - console.error(error); - if (useConditionalMediation) { - return; - } - const errorMessage = error.name === 'NotAllowedError' - ? 'No passkey was provided by the authenticator.' - : error.message; - formData.append(`${this.attrs.name}.Error`, errorMessage); - } - this.internals.setFormValue(formData); - this.internals.form.submit(); - } - - async tryAutofillPasskey() { - if (browserSupportsPasskeys && this.attrs.operation === 'Request' && await PublicKeyCredential.isConditionalMediationAvailable?.()) { - await this.obtainAndSubmitCredential(true); - } - } -}); -``` - -## Step 6: Add passkey endpoints - -Update the `IdentityComponentsEndpointRouteBuilderExtensions.cs` file (or create the file if it doesn't exist) to include the passkey-specific endpoints: - -```csharp -// Add the following endpoints to the existing 'MapAdditionalIdentityEndpoints' method - -accountGroup.MapPost("/PasskeyCreationOptions", async ( - HttpContext context, - [FromServices] UserManager userManager, - [FromServices] SignInManager signInManager, - [FromServices] IAntiforgery antiforgery) => -{ - await antiforgery.ValidateRequestAsync(context); - - var user = await userManager.GetUserAsync(context.User); - if (user is null) - { - return Results.NotFound($"Unable to load user with ID '{userManager.GetUserId(context.User)}'."); - } - - 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"); -}); - -accountGroup.MapPost("/PasskeyRequestOptions", async ( - HttpContext context, - [FromServices] UserManager userManager, - [FromServices] SignInManager signInManager, - [FromServices] IAntiforgery antiforgery, - [FromQuery] string? username) => -{ - await antiforgery.ValidateRequestAsync(context); - - var user = string.IsNullOrEmpty(username) ? null : await userManager.FindByNameAsync(username); - var optionsJson = await signInManager.MakePasskeyRequestOptionsAsync(user); - - return TypedResults.Content(optionsJson, contentType: "application/json"); -}); -``` - -## Step 7: Update the Login page - -*The implementation must be supplied by the developer at this time.* - -## Step 8: Create passkey management pages for adding, deleting, and renaming passkeys - -*The implementation must be supplied by the developer at this time.* - -## Step 9: Update the manage navigation menu - -*The implementation must be supplied by the developer at this time.* - -Add a link to the passkey management page in the app's navigation menu. - -## Step 10: Include the JavaScript file - -*The implementation must be supplied by the developer at this time.* - -Add a reference to the `PasskeySubmit` JavaScript file: - -```html - -``` - -## Step 11: Test the implementation - -* Run the app and navigate to the login page. -* Test logging in with a passkey. -* Navigate to passkeys management page to add, rename, or delete passkeys. -* 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) -* [Passkeys configuration guidance](xref:blazor/security/passkeys#configure-passkey-options) diff --git a/aspnetcore/toc.yml b/aspnetcore/toc.yml index ec65bcd1e9b3..49b0c6989f6b 100644 --- a/aspnetcore/toc.yml +++ b/aspnetcore/toc.yml @@ -1718,8 +1718,6 @@ items: uid: security/authentication/identity-enable-qrcodes - name: Two-factor authentication with SMS uid: security/authentication/2fa - - name: Implement passkeys in an existing Razor Pages or MVC app - uid: security/authentication/passkeys-migration - name: External authentication providers items: - name: Overview From 0942d3b1a5ef013a51ade5f6fa2304b8adaf45e8 Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Tue, 19 Aug 2025 07:43:03 -0400 Subject: [PATCH 08/18] Go with ref source links for code --- .../blazor/security/passkeys-migration.md | 726 +----------------- aspnetcore/migration/index.md | 11 + aspnetcore/toc.yml | 3 + cspell.json | 6 +- 4 files changed, 39 insertions(+), 707 deletions(-) create mode 100644 aspnetcore/migration/index.md diff --git a/aspnetcore/blazor/security/passkeys-migration.md b/aspnetcore/blazor/security/passkeys-migration.md index 1ad7436074c0..d5b8503c5a3b 100644 --- a/aspnetcore/blazor/security/passkeys-migration.md +++ b/aspnetcore/blazor/security/passkeys-migration.md @@ -24,15 +24,13 @@ The guidance in this article relies upon an app that was created with **Individu * An existing Blazor Web App with ASP.NET Core Identity * [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) -## Step 1: Update to .NET 10 +## Reference source guidance -Update the app's project file to target .NET 10: +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). -```xml -net10.0 -``` +## Step 1: Update to .NET 10 -Update all `Microsoft.AspNetCore.*` and `Microsoft.EntityFrameworkCore.*` packages to their latest .NET 10 versions. +Update the app to .NET 10 or later. For more information, see . ## Step 2: Update Identity schema version @@ -91,726 +89,44 @@ dotnet ef database update ## Step 3: Create passkey model classes -Add the following model classes to the project in the `Components/Account` folder. - -Replace the `{NAMESPACE}` placeholder in the following examples with the app's namespace. For example, `Contoso` for `Contoso.Components.Account` in the following examples. - -`Components/Account/PasskeyInputModel.cs`: - -```csharp -namespace {NAMESPACE}.Components.Account; - -public class PasskeyInputModel -{ - public string? CredentialJson { get; set; } - public string? Error { get; set; } -} -``` - -`Components/Account/PasskeyOperation.cs`: - -```csharp -namespace {NAMESPACE}.Components.Account; +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`): -public enum PasskeyOperation -{ - Create = 0, - Request = 1, -} -``` +* [`Components/Account/PasskeyInputModel.cs`](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/PasskeyInputModel.cs) +* [`Components/Account/PasskeyOperation.cs`](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/PasskeyOperation.cs) ## Step 4: Create the `PasskeySubmit` component -Add the following `PasskeySubmit` component to handle passkey operations. - -`Components/Account/Shared/PasskeySubmit.razor`: - -```razor -@using Microsoft.AspNetCore.Antiforgery -@inject IServiceProvider Services - - - - - -@code { - private AntiforgeryTokenSet? tokens; - - [CascadingParameter] - private HttpContext HttpContext { get; set; } = default!; - - [Parameter] - [EditorRequired] - public PasskeyOperation Operation { get; set; } - - [Parameter] - [EditorRequired] - public string Name { get; set; } = default!; - - [Parameter] - public string? EmailName { get; set; } - - [Parameter] - public RenderFragment? ChildContent { get; set; } +Add the following `PasskeySubmit` component to handle passkey operations: - [Parameter(CaptureUnmatchedValues = true)] - public IDictionary? AdditionalAttributes { get; set; } - - protected override void OnInitialized() - { - tokens = Services.GetService()?.GetTokens(HttpContext); - } -} -``` +[`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) ## Step 5: Add the JavaScript for passkey operations -Add the following JavaScript file to handle WebAuthn API interactions. - -`Components/Account/Shared/PasskeySubmit.razor.js`: - -```javascript -const browserSupportsPasskeys = - typeof navigator.credentials !== 'undefined' && - typeof window.PublicKeyCredential !== 'undefined' && - typeof window.PublicKeyCredential.parseCreationOptionsFromJSON === 'function' && - typeof window.PublicKeyCredential.parseRequestOptionsFromJSON === 'function'; - -async function fetchWithErrorHandling(url, options = {}) { - const response = await fetch(url, { - credentials: 'include', - ...options - }); - if (!response.ok) { - const text = await response.text(); - console.error(text); - throw new Error(`The server responded with status ${response.status}.`); - } - return response; -} - -async function createCredential(headers, signal) { - 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 }); -} - -async function requestCredential(email, mediation, headers, signal) { - 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 }); -} - -customElements.define('passkey-submit', class extends HTMLElement { - static formAssociated = true; - - connectedCallback() { - this.internals = this.attachInternals(); - this.attrs = { - operation: this.getAttribute('operation'), - name: this.getAttribute('name'), - emailName: this.getAttribute('email-name'), - requestTokenName: this.getAttribute('request-token-name'), - requestTokenValue: this.getAttribute('request-token-value'), - }; - - this.internals.form.addEventListener('submit', (event) => { - if (event.submitter?.name === '__passkeySubmit') { - event.preventDefault(); - this.obtainAndSubmitCredential(); - } - }); - - this.tryAutofillPasskey(); - } - - disconnectedCallback() { - this.abortController?.abort(); - } - - async obtainCredential(useConditionalMediation, signal) { - if (!browserSupportsPasskeys) { - throw new Error('Some passkey features are missing. Please update your browser.'); - } - - const headers = { - [this.attrs.requestTokenName]: this.attrs.requestTokenValue, - }; - - if (this.attrs.operation === 'Create') { - return await createCredential(headers, signal); - } else if (this.attrs.operation === 'Request') { - const email = new FormData(this.internals.form).get(this.attrs.emailName); - const mediation = useConditionalMediation ? 'conditional' : undefined; - return await requestCredential(email, mediation, headers, signal); - } else { - throw new Error(`Unknown passkey operation '${this.attrs.operation}'.`); - } - } - - async obtainAndSubmitCredential(useConditionalMediation = false) { - this.abortController?.abort(); - this.abortController = new AbortController(); - const signal = this.abortController.signal; - const formData = new FormData(); - try { - const credential = await this.obtainCredential(useConditionalMediation, signal); - const credentialJson = JSON.stringify(credential); - formData.append(`${this.attrs.name}.CredentialJson`, credentialJson); - } catch (error) { - if (error.name === 'AbortError') { - return; - } - console.error(error); - if (useConditionalMediation) { - return; - } - const errorMessage = error.name === 'NotAllowedError' - ? 'No passkey was provided by the authenticator.' - : error.message; - formData.append(`${this.attrs.name}.Error`, errorMessage); - } - this.internals.setFormValue(formData); - this.internals.form.submit(); - } - - async tryAutofillPasskey() { - if (browserSupportsPasskeys && this.attrs.operation === 'Request' && await PublicKeyCredential.isConditionalMediationAvailable?.()) { - await this.obtainAndSubmitCredential(true); - } - } -}); -``` - -## Step 6: Add passkey endpoints - -Update the `IdentityComponentsEndpointRouteBuilderExtensions.cs` file (or create the file if it doesn't exist) to include the passkey-specific endpoints: +Add the following JavaScript file to handle WebAuthn API interactions: -```csharp -// Add the following endpoints to the existing 'MapAdditionalIdentityEndpoints' method +[`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) -accountGroup.MapPost("/PasskeyCreationOptions", async ( - HttpContext context, - [FromServices] UserManager userManager, - [FromServices] SignInManager signInManager, - [FromServices] IAntiforgery antiforgery) => -{ - await antiforgery.ValidateRequestAsync(context); - - var user = await userManager.GetUserAsync(context.User); - if (user is null) - { - return Results.NotFound($"Unable to load user with ID '{userManager.GetUserId(context.User)}'."); - } - - 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"); -}); - -accountGroup.MapPost("/PasskeyRequestOptions", async ( - HttpContext context, - [FromServices] UserManager userManager, - [FromServices] SignInManager signInManager, - [FromServices] IAntiforgery antiforgery, - [FromQuery] string? username) => -{ - await antiforgery.ValidateRequestAsync(context); +## Step 6: Add passkey endpoints - var user = string.IsNullOrEmpty(username) ? null : await userManager.FindByNameAsync(username); - var optionsJson = await signInManager.MakePasskeyRequestOptionsAsync(user); +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: - return TypedResults.Content(optionsJson, contentType: "application/json"); -}); -``` +[`/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) ## Step 7: Update the Login page -Replace the existing `Login` component with the following component. The passkey-specific additions are described by Razor comments (`@* ... *@`) in the file. - -Replace the `{NAMESPACE}` placeholder in the following example with the app's namespace. For example, `Contoso` for `Contoso.Data` in the following examples. - -`Components/Account/Pages/Login.razor`: - -```razor -@page "/Account/Login" - -@using System.ComponentModel.DataAnnotations -@using Microsoft.AspNetCore.Authentication -@using Microsoft.AspNetCore.Identity -@using {NAMESPACE}.Data - -@inject UserManager UserManager -@inject SignInManager SignInManager -@inject ILogger Logger -@inject NavigationManager NavigationManager -@inject IdentityRedirectManager RedirectManager - -Log in - -

Log in

-
-
-
- - @* The EditContext is used instead of Model to allow conditional validation *@ - - -

Use a local account to log in.

-
- -
- @* Note the autocomplete="username webauthn" to enable passkey autofill *@ - - - -
-
- - - -
-
- -
-
- -
-
- @* Passkey login button *@ -
- OR - Log in with a passkey -
-
- -
-
-
-
-
-

Use another service to log in.

-
- -
-
-
- -@code { - private string? errorMessage; - private EditContext editContext = default!; // EditContext for conditional validation - - [CascadingParameter] - private HttpContext HttpContext { get; set; } = default!; - - [SupplyParameterFromForm] - private InputModel Input { get; set; } = default!; - - [SupplyParameterFromQuery] - private string? ReturnUrl { get; set; } - - protected override async Task OnInitializedAsync() - { - Input ??= new(); - - editContext = new EditContext(Input); // Initialize EditContext - - if (HttpMethods.IsGet(HttpContext.Request.Method)) - { - // Clear the existing external cookie to ensure a clean login process - await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); - } - } - - public async Task LoginUser() - { - // Handle passkey errors - if (!string.IsNullOrEmpty(Input.Passkey?.Error)) - { - errorMessage = $"Error: {Input.Passkey.Error}"; - return; - } - - SignInResult result; - - // Check if this is a passkey sign-in - if (!string.IsNullOrEmpty(Input.Passkey?.CredentialJson)) - { - // When performing passkey sign-in, don't perform form validation. - result = await SignInManager.PasskeySignInAsync(Input.Passkey.CredentialJson); - } - else - { - // If doing a password sign-in, validate the form. - if (!editContext.Validate()) - { - return; - } - - // This doesn't count login failures towards account lockout - // To enable password failures to trigger account lockout, set lockoutOnFailure: true - result = await SignInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false); - } - - if (result.Succeeded) - { - Logger.LogInformation("User logged in."); - RedirectManager.RedirectTo(ReturnUrl); - } - else if (result.RequiresTwoFactor) - { - RedirectManager.RedirectTo( - "Account/LoginWith2fa", - new() { ["returnUrl"] = ReturnUrl, ["rememberMe"] = Input.RememberMe }); - } - else if (result.IsLockedOut) - { - Logger.LogWarning("User account locked out."); - RedirectManager.RedirectTo("Account/Lockout"); - } - else - { - errorMessage = "Error: Invalid login attempt."; - } - } - - private sealed class InputModel - { - [Required] - [EmailAddress] - public string Email { get; set; } = ""; - - [Required] - [DataType(DataType.Password)] - public string Password { get; set; } = ""; - - [Display(Name = "Remember me?")] - public bool RememberMe { get; set; } - - // Passkey input model - public PasskeyInputModel? Passkey { get; set; } - } -} -``` - -## Step 8: Create passkey management pages for adding and renaming passkeys - -Add the following `Passkeys` component for managing passkeys. - -Replace the `{NAMESPACE}` placeholder in the following examples with the app's namespace. For example, `Contoso` for `Contoso.Data` in the following examples. - -`Components/Account/Pages/Manage/Passkeys.razor`: - -```razor -@page "/Account/Manage/Passkeys" +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`): -@using {NAMESPACE}.Data -@using Microsoft.AspNetCore.Identity -@using System.Buffers.Text +[`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) -@inject UserManager UserManager -@inject SignInManager SignInManager -@inject IdentityRedirectManager RedirectManager - -Manage your passkeys +## Step 8: Create passkey management pages for adding and renaming passkeys -

Manage your 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) -@if (currentPasskeys is { Count: > 0 }) -{ - - - @foreach (var passkey in currentPasskeys) - { - - - - - } - -
@(passkey.Name ?? "Unnamed passkey") - @{ - var credentialId = Base64Url.EncodeToString(passkey.CredentialId); - } -
- -
- - - -
- -
-} -else -{ -

No passkeys are registered.

-} - -
- - Add a new passkey - - -@code { - private ApplicationUser? user; - private IList? currentPasskeys; - - [CascadingParameter] - private HttpContext HttpContext { get; set; } = default!; - - [SupplyParameterFromForm] - private string? Action { get; set; } - - [SupplyParameterFromForm] - private string? CredentialId { get; set; } - - [SupplyParameterFromForm(FormName = "add-passkey")] - private PasskeyInputModel Input { get; set; } = default!; - - protected override async Task OnInitializedAsync() - { - Input ??= new(); - - user = await UserManager.GetUserAsync(HttpContext.User); - if (user is null) - { - RedirectManager.RedirectToInvalidUser(UserManager, HttpContext); - return; - } - currentPasskeys = await UserManager.GetPasskeysAsync(user); - } - - private async Task AddPasskey() - { - if (user is null) - { - RedirectManager.RedirectToInvalidUser(UserManager, HttpContext); - return; - } - - if (!string.IsNullOrEmpty(Input.Error)) - { - RedirectManager.RedirectToCurrentPageWithStatus($"Error: {Input.Error}", HttpContext); - return; - } - - if (string.IsNullOrEmpty(Input.CredentialJson)) - { - RedirectManager.RedirectToCurrentPageWithStatus("Error: The browser did not provide a passkey.", HttpContext); - return; - } - - var attestationResult = await SignInManager.PerformPasskeyAttestationAsync(Input.CredentialJson); - if (!attestationResult.Succeeded) - { - RedirectManager.RedirectToCurrentPageWithStatus($"Error: Could not add the passkey: {attestationResult.Failure.Message}", HttpContext); - return; - } - - var addPasskeyResult = await UserManager.AddOrUpdatePasskeyAsync(user, attestationResult.Passkey); - if (!addPasskeyResult.Succeeded) - { - RedirectManager.RedirectToCurrentPageWithStatus("Error: The passkey could not be added to your account.", HttpContext); - return; - } - - // Immediately prompt the user to enter a name for the credential - var credentialIdBase64Url = Base64Url.EncodeToString(attestationResult.Passkey.CredentialId); - RedirectManager.RedirectTo($"Account/Manage/RenamePasskey/{credentialIdBase64Url}"); - } - - private async Task UpdatePasskey() - { - switch (Action) - { - case "rename": - RedirectManager.RedirectTo($"Account/Manage/RenamePasskey/{CredentialId}"); - break; - case "delete": - await DeletePasskey(); - break; - default: - RedirectManager.RedirectToCurrentPageWithStatus($"Error: Unknown action '{Action}'.", HttpContext); - break; - } - } - - private async Task DeletePasskey() - { - if (user is null) - { - RedirectManager.RedirectToInvalidUser(UserManager, HttpContext); - return; - } - - byte[] credentialId; - try - { - credentialId = Base64Url.DecodeFromChars(CredentialId); - } - catch (FormatException) - { - RedirectManager.RedirectToCurrentPageWithStatus("Error: The specified passkey ID had an invalid format.", HttpContext); - return; - } - - var result = await UserManager.RemovePasskeyAsync(user, credentialId); - if (!result.Succeeded) - { - RedirectManager.RedirectToCurrentPageWithStatus("Error: The passkey could not be deleted.", HttpContext); - return; - } - - RedirectManager.RedirectToCurrentPageWithStatus("Passkey deleted successfully.", HttpContext); - } -} -``` +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`): -Add the following component for renaming passkeys. - - - -`Components/Account/Pages/Manage/RenamePasskey.razor`: - -```razor -@page "/Account/Manage/RenamePasskey/{Id}" - -@using {NAMESPACE}.Data -@using System.ComponentModel.DataAnnotations -@using Microsoft.AspNetCore.Identity -@using System.Buffers.Text - -@inject UserManager UserManager -@inject IdentityRedirectManager RedirectManager - - - - @if (passkey?.Name is { } name) - { -

Enter a new name for your "@name" passkey

- } - else - { -

Enter a name for your passkey

- } -
- -
- - - -
-
- -
-
- -@code { - private ApplicationUser? user; - private UserPasskeyInfo? passkey; - - [CascadingParameter] - private HttpContext HttpContext { get; set; } = default!; - - [Parameter] - public string? Id { get; set; } - - [SupplyParameterFromForm] - private InputModel Input { get; set; } = default!; - - protected override async Task OnInitializedAsync() - { - Input ??= new(); - - user = (await UserManager.GetUserAsync(HttpContext.User))!; - if (user is null) - { - RedirectManager.RedirectToInvalidUser(UserManager, HttpContext); - return; - } - - byte[] credentialId; - try - { - credentialId = Base64Url.DecodeFromChars(Id); - } - catch (FormatException) - { - RedirectManager.RedirectToWithStatus("Account/Manage/Passkeys", "Error: The specified passkey ID had an invalid format.", HttpContext); - return; - } - - passkey = await UserManager.GetPasskeyAsync(user, credentialId); - if (passkey is null) - { - RedirectManager.RedirectToWithStatus("Account/Manage/Passkeys", "Error: The specified passkey could not be found.", HttpContext); - return; - } - } - - private async Task Rename() - { - passkey!.Name = Input.Name; - var result = await UserManager.AddOrUpdatePasskeyAsync(user!, passkey); - if (!result.Succeeded) - { - RedirectManager.RedirectToWithStatus("Account/Manage/Passkeys", "Error: The passkey could not be updated.", HttpContext); - return; - } - - RedirectManager.RedirectToWithStatus("Account/Manage/Passkeys", "Passkey updated successfully.", HttpContext); - } - - private sealed class InputModel - { - [Required] - public string Name { get; set; } = ""; - } -} -``` +[`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) ## Step 9: Update the manage navigation menu 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/toc.yml b/aspnetcore/toc.yml index 49b0c6989f6b..c2618948b738 100644 --- a/aspnetcore/toc.yml +++ b/aspnetcore/toc.yml @@ -2060,6 +2060,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/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": [ From f99a74ccca93900c678901373a6b070d4c631f9f Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Tue, 19 Aug 2025 07:53:48 -0400 Subject: [PATCH 09/18] Updates --- .openpublishing.redirection.json | 5 ----- 1 file changed, 5 deletions(-) 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/", From fc5cf75526612a55fae432016f25a5cab99cc049 Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Wed, 27 Aug 2025 11:07:01 -0400 Subject: [PATCH 10/18] Updates --- .../migration/90-to-100/includes/blazor.md | 2 +- .../aspnetcore-10/includes/blazor.md | 4 +- .../authentication/passkeys/blazor.md} | 171 +++++++++++++++--- .../authentication/passkeys/index.md} | 121 +------------ aspnetcore/toc.yml | 10 +- aspnetcore/zone-pivot-groups.yml | 8 + 6 files changed, 169 insertions(+), 147 deletions(-) rename aspnetcore/{blazor/security/passkeys-migration.md => security/authentication/passkeys/blazor.md} (50%) rename aspnetcore/{blazor/security/passkeys.md => security/authentication/passkeys/index.md} (82%) diff --git a/aspnetcore/migration/90-to-100/includes/blazor.md b/aspnetcore/migration/90-to-100/includes/blazor.md index 79a7c6d1cc77..a24d7ac26f6a 100644 --- a/aspnetcore/migration/90-to-100/includes/blazor.md +++ b/aspnetcore/migration/90-to-100/includes/blazor.md @@ -2,4 +2,4 @@ Complete migration coverage for Blazor apps is scheduled for September and Octob ### Adopt passkey user authentication in an existing Blazor Web App -For guidance, see . +For guidance, see . diff --git a/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md b/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md index d49e4268d37d..02638cd2a706 100644 --- a/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md +++ b/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md @@ -605,8 +605,8 @@ The Blazor Web App project template provides out-of-the-box passkey management a For more information, see the following articles: -* -* +* +* ### Circuit state persistence diff --git a/aspnetcore/blazor/security/passkeys-migration.md b/aspnetcore/security/authentication/passkeys/blazor.md similarity index 50% rename from aspnetcore/blazor/security/passkeys-migration.md rename to aspnetcore/security/authentication/passkeys/blazor.md index d5b8503c5a3b..f1548e9de82a 100644 --- a/aspnetcore/blazor/security/passkeys-migration.md +++ b/aspnetcore/security/authentication/passkeys/blazor.md @@ -1,17 +1,122 @@ --- -title: Implement passkeys in an existing Blazor Web App +title: Implement passkeys in ASP.NET Core Blazor Web Apps author: guardrex -description: Learn how to implement passkeys authentication in an ASP.NET Core Blazor Web App. +description: Learn how to implement passkeys authentication in ASP.NET Core Blazor Web Apps. ms.author: wpickett ms.custom: mvc -ms.date: 8/14/2025 -uid: blazor/security/passkeys-migration +ms.date: 8/27/2025 +uid: security/authentication/passkeys/blazor +zone_pivot_groups: implementation --- -# Implement passkeys in an existing Blazor Web App +# Implement passkeys in ASP.NET Core Blazor Web Apps -This guide explains how to add [passkey support](xref:blazor/security/passkeys) to an existing Blazor Web App that has ASP.NET Core Identity authentication configured. +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. -The guidance in this article 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). +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 @@ -28,11 +133,11 @@ The guidance in this article relies upon an app that was created with **Individu 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). -## Step 1: Update to .NET 10 +## Update to .NET 10 Update the app to .NET 10 or later. For more information, see . -## Step 2: Update Identity schema version +## Update Identity schema version In `Program.cs`, update the Identity configuration to use schema version 3, which includes passkey support: @@ -47,6 +152,8 @@ builder.Services.AddIdentityCore(options => .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. @@ -87,38 +194,38 @@ dotnet ef database update --- -## Step 3: Create passkey model classes +## 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) * [`Components/Account/PasskeyOperation.cs`](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/PasskeyOperation.cs) -## Step 4: Create the `PasskeySubmit` component +## 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) -## Step 5: Add the JavaScript for passkey operations +## 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) -## Step 6: Add passkey endpoints +## 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) -## Step 7: Update the Login page +## 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) -## Step 8: Create passkey management pages for adding and renaming passkeys +## 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`): @@ -128,7 +235,7 @@ Add the following `RenamePasskey` component for renaming passkeys and update the [`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) -## Step 9: Update the manage navigation menu +## Update the manage navigation menu Add a link to the passkey management page in the app's `ManageNavMenu` component. @@ -140,7 +247,7 @@ In `Components/Account/Shared/ManageNavMenu.razor`: + ``` -## Step 10: Include the JavaScript file +## Include the JavaScript file In the `App` component, add a reference to the `PasskeySubmit` JavaScript file after the Blazor script. @@ -151,18 +258,30 @@ In `Components/App.razor`: + ``` -## Step 11: Test the implementation +:::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: -* Run the app and navigate to the login page. -* Log in with a username and password. -* Register a passkey. -* Sign out of the app. -* Sign back into the app with a passkey using the **Log in with a passkey** button. -* Navigate to `Account/Manage/Passkeys` to add, rename, or delete passkeys. -* If the passkey supports autofill, test the autofill feature by selecting the email input field when you have saved passkeys. +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 autofill, test the 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) -* [Passkeys configuration guidance](xref:blazor/security/passkeys#configure-passkey-options) diff --git a/aspnetcore/blazor/security/passkeys.md b/aspnetcore/security/authentication/passkeys/index.md similarity index 82% rename from aspnetcore/blazor/security/passkeys.md rename to aspnetcore/security/authentication/passkeys/index.md index fe4f0eadfc35..99921bb8d83d 100644 --- a/aspnetcore/blazor/security/passkeys.md +++ b/aspnetcore/security/authentication/passkeys/index.md @@ -1,13 +1,13 @@ --- -title: Enable Web Authentication API (WebAuthn) passkeys in an ASP.NET Core Blazor Web App +title: Enable Web Authentication API (WebAuthn) passkeys author: guardrex -description: Discover how to enable Web Authentication API (WebAuthn) passkeys with ASP.NET Core Blazor Web App authentication. +description: Discover how to enable Web Authentication API (WebAuthn) passkeys in ASP.NET Core apps. ms.author: wpickett monikerRange: '>= aspnetcore-10.0' -ms.date: 08/14/2025 -uid: blazor/security/passkeys +ms.date: 08/27/2025 +uid: security/authentication/passkeys/index --- -# Enable Web Authentication API (WebAuthn) passkeys in an ASP.NET Core Blazor Web App +# 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/en-us/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 Blazor Web App to use passkeys to authenticate users. +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. -The guidance in this article relies upon creating a .NET 10 or later Blazor Web App with **Individual Accounts** for the app's **Authentication type** or [scaffolding Identity](xref:security/authentication/scaffold-identity#scaffold-identity-into-a-blazor-project) into an existing .NET 10 or later Blazor Web App. For migration guidance from prior versions of .NET, see . +For guidance specific to new and existing Blazor Web Apps, see after reading this article. ## What are passkeys? @@ -143,113 +143,6 @@ To prevent database exhaustion attacks, apps should enforce limits on passkey re The Blazor Web App template enforces these limits by default. -## Create or migrate a Blazor Web App - -For migration guidance to update an existing Blazor Web App to use passkeys, see . - -Use the following guidance to create a new Blazor Web App with 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 -``` - -## Register a passkey - -To test passkey functionality: - -1. Register a new account or sign in with an existing account. -2. Navigate to **Manage your account** (select the username in the navigation menu). -3. Select **Passkeys** from the navigation menu. -4. Select **Add a new passkey**. -5. 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. -2. On the login page, enter your email address. -3. Select **Log in with a passkey**. -4. Follow the browser's prompts to authenticate with your passkey. - ## Configure passkey options ASP.NET Core Identity provides various options to configure passkey behavior through the `IdentityPasskeyOptions` class, which include: diff --git a/aspnetcore/toc.yml b/aspnetcore/toc.yml index 3f68dc5d0a6b..9eeb376d7611 100644 --- a/aspnetcore/toc.yml +++ b/aspnetcore/toc.yml @@ -652,10 +652,6 @@ items: uid: blazor/security/account-confirmation-and-password-recovery - name: QR codes with TOTP uid: blazor/security/qrcodes-for-authenticator-apps - - name: Passkeys - uid: blazor/security/passkeys - - name: Implement passkeys in an existing Blazor Web App - uid: blazor/security/passkeys-migration - name: Content Security Policy uid: blazor/security/content-security-policy - name: EU General Data Protection Regulation (GDPR) support @@ -1742,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 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 From 3ac946025edb63a9c68295ea4b82f56cca45be42 Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Wed, 27 Aug 2025 12:29:33 -0400 Subject: [PATCH 11/18] Updates --- .../security/authentication/passkeys/blazor.md | 12 +++++------- aspnetcore/security/authentication/passkeys/index.md | 12 +++++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/aspnetcore/security/authentication/passkeys/blazor.md b/aspnetcore/security/authentication/passkeys/blazor.md index f1548e9de82a..66f4e062df55 100644 --- a/aspnetcore/security/authentication/passkeys/blazor.md +++ b/aspnetcore/security/authentication/passkeys/blazor.md @@ -126,17 +126,15 @@ The following guidance relies upon an app that was created with **Individual Acc --> -* An existing Blazor Web App with ASP.NET Core Identity +* 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 to .NET 10 - -Update the app to .NET 10 or later. For more information, see . - ## Update Identity schema version In `Program.cs`, update the Identity configuration to use schema version 3, which includes passkey support: @@ -198,8 +196,8 @@ dotnet ef database update 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) -* [`Components/Account/PasskeyOperation.cs`](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/PasskeyOperation.cs) +* [`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 diff --git a/aspnetcore/security/authentication/passkeys/index.md b/aspnetcore/security/authentication/passkeys/index.md index 99921bb8d83d..634f87b0d3aa 100644 --- a/aspnetcore/security/authentication/passkeys/index.md +++ b/aspnetcore/security/authentication/passkeys/index.md @@ -507,11 +507,13 @@ After the authenticator creates the signed assertion, the browser serializes it 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 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 }); From 1f8653936844fa79e204c889851f24a47488693d Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Wed, 3 Sep 2025 12:16:11 -0400 Subject: [PATCH 12/18] Apply suggestions from code review Co-authored-by: Mike Kistler --- aspnetcore/security/authentication/passkeys/index.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/aspnetcore/security/authentication/passkeys/index.md b/aspnetcore/security/authentication/passkeys/index.md index 634f87b0d3aa..98a4d228d4f1 100644 --- a/aspnetcore/security/authentication/passkeys/index.md +++ b/aspnetcore/security/authentication/passkeys/index.md @@ -145,7 +145,7 @@ The Blazor Web App template enforces these limits by default. ## Configure passkey options -ASP.NET Core Identity provides various options to configure passkey behavior through the `IdentityPasskeyOptions` class, which include: +ASP.NET Core Identity provides various options to configure passkey behavior through the 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. @@ -170,10 +170,7 @@ For a complete list of configuration options, see -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). +For a complete list of configuration options during the .NET 10 preview release period, see . > [!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. @@ -314,7 +311,7 @@ builder.Services.Configure(options => }); ``` -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). +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 . - -For a complete list of configuration options during the .NET 10 preview release period, see . - > [!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. From 8e778870219ae4596d2e66255b3737a86a109b93 Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Wed, 3 Sep 2025 12:25:37 -0400 Subject: [PATCH 14/18] Updates --- .../security/authentication/passkeys/index.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/aspnetcore/security/authentication/passkeys/index.md b/aspnetcore/security/authentication/passkeys/index.md index 892e253ea766..225843fabe15 100644 --- a/aspnetcore/security/authentication/passkeys/index.md +++ b/aspnetcore/security/authentication/passkeys/index.md @@ -145,7 +145,7 @@ The Blazor Web App template enforces these limits by default. ## Configure passkey options -ASP.NET Core Identity provides various options to configure passkey behavior through the class, which include: +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. @@ -162,8 +162,17 @@ builder.Services.Configure(options => }); ``` + + +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. @@ -303,7 +312,7 @@ builder.Services.Configure(options => }); ``` -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 `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).