From 28f67fb8a974b7b0a85f40c2f5dbac1b830bddfd Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Wed, 30 Oct 2024 10:21:57 -0400 Subject: [PATCH 01/11] WASM+Identity acct conf and PW recovery --- .openpublishing.redirection.json | 10 + aspnetcore/blazor/call-web-api.md | 2 +- aspnetcore/blazor/fundamentals/index.md | 16 + ...ount-confirmation-and-password-recovery.md | 42 +- .../blazor/security/authentication-state.md | 4 +- ...ount-confirmation-and-password-recovery.md | 621 ++++++++++++++++++ .../index.md} | 13 +- aspnetcore/release-notes/aspnetcore-8.0.md | 2 +- .../security/authentication/accconfirm.md | 5 +- .../identity-api-authorization.md | 2 +- .../authentication/scaffold-identity.md | 2 +- aspnetcore/toc.yml | 10 +- .../whats-new/dotnet-AspNetCore.Docs-mod1.md | 4 +- .../whats-new/dotnet-AspNetCore.Docs-mod2.md | 2 +- .../whats-new/dotnet-AspNetCore.Docs-mod3.md | 2 +- 15 files changed, 703 insertions(+), 34 deletions(-) rename aspnetcore/blazor/security/{server => }/account-confirmation-and-password-recovery.md (84%) create mode 100644 aspnetcore/blazor/security/webassembly/standalone-with-identity/account-confirmation-and-password-recovery.md rename aspnetcore/blazor/security/webassembly/{standalone-with-identity.md => standalone-with-identity/index.md} (97%) diff --git a/.openpublishing.redirection.json b/.openpublishing.redirection.json index faaf355d258b..c9297ef09d17 100644 --- a/.openpublishing.redirection.json +++ b/.openpublishing.redirection.json @@ -1322,6 +1322,16 @@ "source_path": "aspnetcore/blazor/components/prerendering-and-integration.md", "redirect_url": "/aspnet/core/blazor/components/integration", "redirect_document_id": false + }, + { + "source_path": "aspnetcore/blazor/security/webassembly/standalone-with-identity.md", + "redirect_url": "/aspnet/core/blazor/security/webassembly/standalone-with-identity/", + "redirect_document_id": false + }, + { + "source_path": "blazor/security/server/account-confirmation-and-password-recovery.md", + "redirect_url": "/aspnet/core/blazor/security/account-confirmation-and-password-recovery", + "redirect_document_id": false } ] } diff --git a/aspnetcore/blazor/call-web-api.md b/aspnetcore/blazor/call-web-api.md index a446f9155d76..8ad2732ac2fd 100644 --- a/aspnetcore/blazor/call-web-api.md +++ b/aspnetcore/blazor/call-web-api.md @@ -684,7 +684,7 @@ builder.Services.AddHttpClient(...) :::moniker range=">= aspnetcore-8.0" -For a demonstration, see . +For a demonstration, see . :::moniker-end diff --git a/aspnetcore/blazor/fundamentals/index.md b/aspnetcore/blazor/fundamentals/index.md index a84a5333cac0..1bf538ba6cb1 100644 --- a/aspnetcore/blazor/fundamentals/index.md +++ b/aspnetcore/blazor/fundamentals/index.md @@ -178,6 +178,22 @@ Documentation sample apps are available for inspection and download: Locate a sample app by first selecting the version folder that matches the version of .NET that you're working with. +:::moniker range=">= aspnetcore-8.0" + +Samples apps in the repository: + +* Blazor Web App +* Blazor WebAssembly +* Blazor Web App with EF Core () +* Blazor Web App with SignalR () +* Two Blazor Web Apps and a Blazor WebAssembly app for calling web (server) APIs () +* Blazor Web App with OIDC (BFF and non-BFF patterns) () +* Blazor WebAssembly scopes-enabled logging () +* Blazor WebAssembly with ASP.NET Core Identity () +* .NET MAUI Blazor Hybrid app with a Blazor Web App and a shared UI provided by a Razor class library (RCL) () + +:::moniker-end + :::moniker range="< aspnetcore-8.0" The sample repo contains two types of samples: diff --git a/aspnetcore/blazor/security/server/account-confirmation-and-password-recovery.md b/aspnetcore/blazor/security/account-confirmation-and-password-recovery.md similarity index 84% rename from aspnetcore/blazor/security/server/account-confirmation-and-password-recovery.md rename to aspnetcore/blazor/security/account-confirmation-and-password-recovery.md index d6284841360b..3630c3e2c682 100644 --- a/aspnetcore/blazor/security/server/account-confirmation-and-password-recovery.md +++ b/aspnetcore/blazor/security/account-confirmation-and-password-recovery.md @@ -5,7 +5,7 @@ description: Learn how to configure an ASP.NET Core Blazor Web App with email co ms.author: riande monikerRange: '>= aspnetcore-8.0' ms.date: 11/12/2024 -uid: blazor/security/server/account-confirmation-and-password-recovery +uid: blazor/security/account-confirmation-and-password-recovery --- # Account confirmation and password recovery in ASP.NET Core Blazor @@ -13,6 +13,9 @@ uid: blazor/security/server/account-confirmation-and-password-recovery This article explains how to configure an ASP.NET Core Blazor Web App with email confirmation and password recovery. +> [!NOTE] +> This article only applies to Blazor Web Apps. To implement email confirmation and password recovery for standalone Blazor WebAssembly apps with ASP.NET Core Identity, see . + ## Namespace The app's namespace used by the example in this article is `BlazorSample`. Update the code examples to use the namespace of your app. @@ -23,7 +26,7 @@ In this article, [Mailchimp's Transactional API](https://mailchimp.com/developer Create a class to fetch the secure email API key. The example in this article uses a class named `AuthMessageSenderOptions` with a `EmailAuthKey` property to hold the key. -`AuthMessageSenderOptions`: +`AuthMessageSenderOptions.cs`: ```csharp namespace BlazorSample; @@ -42,19 +45,31 @@ builder.Services.Configure(builder.Configuration); ## Configure a user secret for the provider's security key -Set the key with the [Secret Manager tool](xref:security/app-secrets). In the following example, the key name is `EmailAuthKey`, and the key is represented by the `{KEY}` placeholder. In a command shell, navigate to the app's root folder and execute the following command with the API key: +If the project has already been initialized for the [Secret Manager tool](xref:security/app-secrets), it will already have an app secrets identifier (``) in its project file (`.csproj`). In Visual Studio, you can tell if the app secrets ID is present by looking at the **Properties** panel when the project is selected in **Solution Explorer**. If the app hasn't been initialized, execute the following command in a command shell opened to the project's directory. In Visual Studio, you can use the Developer PowerShell command prompt. + +```dotnetcli +dotnet user-secrets init +``` + +Set the key with the Secret Manager tool. In the following example, the key name is `EmailAuthKey`, and the key is represented by the `{KEY}` placeholder. In a command shell, navigate to the app's root folder and execute the following command with the API key: ```dotnetcli dotnet user-secrets set "EmailAuthKey" "{KEY}" ``` +If using Visual Studio, you can confirm the secret is set by right-clicking the server project in **Solution Explorer** and selecting **Manage User Secrets**. + For more information, see . [!INCLUDE[](~/blazor/security/includes/secure-authentication-flows.md)] ## Implement `IEmailSender` -Implement `IEmailSender` for the provider. The following example is based on Mailchimp's Transactional API using [Mandrill.net](https://www.nuget.org/packages/Mandrill.net). For a different provider, refer to their documentation on how to implement sending a message in the `Execute` method. +The following example is based on Mailchimp's Transactional API using [Mandrill.net](https://www.nuget.org/packages/Mandrill.net). For a different provider, refer to their documentation on how to implement sending an email message. + +Add the [Mandrill.net](https://www.nuget.org/packages/Mandrill.net) NuGet package to the project. + +Add the following `EmailSender` class to implement . `Components/Account/EmailSender.cs`: @@ -172,7 +187,7 @@ builder.Services.Configure(options => options.TokenLifespan = TimeSpan.FromHours(3)); ``` -The built in Identity user tokens ([AspNetCore/src/Identity/Extensions.Core/src/TokenOptions.cs](https://github.com/dotnet/aspnetcore/blob/main/src/Identity/Extensions.Core/src/TokenOptions.cs)) have a [one day timeout](https://github.com/dotnet/AspNetCore/blob/main/src/Identity/Core/src/DataProtectionTokenProviderOptions.cs). +The built-in Identity user tokens ([AspNetCore/src/Identity/Extensions.Core/src/TokenOptions.cs](https://github.com/dotnet/aspnetcore/blob/main/src/Identity/Extensions.Core/src/TokenOptions.cs)) have a [one day timeout](https://github.com/dotnet/AspNetCore/blob/main/src/Identity/Core/src/DataProtectionTokenProviderOptions.cs). [!INCLUDE[](~/includes/aspnetcore-repo-ref-source-links.md)] @@ -258,6 +273,13 @@ builder.Services .AddTransient>(); ``` +## Enable account confirmation after a site has users + +Enabling account confirmation on a site with users locks out all the existing users. Existing users are locked out because their accounts aren't confirmed. To work around existing user lockout, use one of the following approaches: + +* Update the database to mark all existing users as confirmed. +* Confirm existing users. For example, batch-send emails with confirmation links. + ## Troubleshoot If you can't get email working: @@ -269,16 +291,6 @@ If you can't get email working: * Try another email alias on a different email provider, such as Microsoft, Yahoo, or Gmail. * Try sending to different email accounts. -> [!WARNING] -> Do **not** use production secrets in test and development. If you publish the app to Azure, set secrets as application settings in the Azure Web App portal. The configuration system is set up to read keys from environment variables. - -## Enable account confirmation after a site has users - -Enabling account confirmation on a site with users locks out all the existing users. Existing users are locked out because their accounts aren't confirmed. To work around existing user lockout, use one of the following approaches: - -* Update the database to mark all existing users as confirmed. -* Confirm existing users. For example, batch-send emails with confirmation links. - ## Additional resources * [Mandrill.net (GitHub repository)](https://github.com/feinoujc/Mandrill.net) diff --git a/aspnetcore/blazor/security/authentication-state.md b/aspnetcore/blazor/security/authentication-state.md index 81aba37479d9..3e76e09c62da 100644 --- a/aspnetcore/blazor/security/authentication-state.md +++ b/aspnetcore/blazor/security/authentication-state.md @@ -474,7 +474,7 @@ The following component's `SignIn` method creates a claims principal for the use * [Server-side unauthorized content display while prerendering with a custom `AuthenticationStateProvider`](xref:blazor/security/server/index#unauthorized-content-display-while-prerendering-with-a-custom-authenticationstateprovider) * [How to access an `AuthenticationStateProvider` from a `DelegatingHandler` set up using an `IHttpClientFactory`](xref:blazor/security/server/additional-scenarios#access-authenticationstateprovider-in-outgoing-request-middleware) * -* +* :::moniker-end @@ -483,7 +483,7 @@ The following component's `SignIn` method creates a claims principal for the use * [Server-side unauthorized content display while prerendering with a custom `AuthenticationStateProvider`](xref:blazor/security/server/index#unauthorized-content-display-while-prerendering-with-a-custom-authenticationstateprovider) * [How to access an `AuthenticationStateProvider` from a `DelegatingHandler` set up using an `IHttpClientFactory`](xref:blazor/security/server/additional-scenarios#access-authenticationstateprovider-in-outgoing-request-middleware) * -* +* [Prerendering with authentication in hosted Blazor WebAssembly apps](xref:blazor/security/webassembly/additional-scenarios#prerendering-with-authentication) :::moniker-end diff --git a/aspnetcore/blazor/security/webassembly/standalone-with-identity/account-confirmation-and-password-recovery.md b/aspnetcore/blazor/security/webassembly/standalone-with-identity/account-confirmation-and-password-recovery.md new file mode 100644 index 000000000000..c386fc310809 --- /dev/null +++ b/aspnetcore/blazor/security/webassembly/standalone-with-identity/account-confirmation-and-password-recovery.md @@ -0,0 +1,621 @@ +--- +title: Account confirmation and password recovery in ASP.NET Core Blazor WebAssembly with ASP.NET Core Identity +author: guardrex +description: Learn how to configure an ASP.NET Core Blazor WebAssembly app with ASP.NET Core Identity with email confirmation and password recovery. +ms.author: riande +monikerRange: '>= aspnetcore-8.0' +ms.date: 10/30/2024 +uid: blazor/security/webassembly/standalone-with-identity/account-confirmation-and-password-recovery +--- +# Account confirmation and password recovery in ASP.NET Core Blazor WebAssembly with ASP.NET Core Identity + +This article explains how to configure an ASP.NET Core Blazor WebAssembly app with ASP.NET Core Identity with email confirmation and password recovery. + +> [!NOTE] +> This article only applies standalone Blazor WebAssembly apps with ASP.NET Core Identity. To implement email confirmation and password recovery for Blazor Web Apps, see . + +## Namespace + +The namespaces used by the examples in this article are: + +* `Backend` for the backend server web API project. +* `BlazorWasmAuth` for the front-end standalone Blazor WebAssembly app. + +These namespaces correspond to the projects in the `BlazorWebAssemblyStandaloneWithIdentity` sample solution in the [`dotnet/blazor-samples` GitHub repository](https://github.com/dotnet/blazor-samples). For more information, see . + +If you aren't using the `BlazorWebAssemblyStandaloneWithIdentity` sample solution, change the namespaces in the code examples to use the namespaces of your projects. + +## Select and configure an email provider for the server project + +In this article, [Mailchimp's Transactional API](https://mailchimp.com/developer/transactional/api/) is used via [Mandrill.net](https://www.nuget.org/packages/Mandrill.net) to send email. We recommend using an email service to send email rather than SMTP. SMTP is difficult to configure and secure properly. Whichever email service you use, access their guidance for .NET apps, create an account, configure an API key for their service, and install any NuGet packages required. + +In the backend server project, create a class to fetch the secure email API key. The example in this article uses a class named `AuthMessageSenderOptions` with a `EmailAuthKey` property to hold the key. + +`AuthMessageSenderOptions.cs`: + +```csharp +namespace Backend; + +public class AuthMessageSenderOptions +{ + public string? EmailAuthKey { get; set; } +} +``` + +Register the `AuthMessageSenderOptions` configuration instance in the backend server project's `Program` file: + +```csharp +builder.Services.Configure(builder.Configuration); +``` + +## Configure a user secret for the provider's security key + +If the backend server web API project (`Backend` in the [sample solution](xref:blazor/security/webassembly/standalone-with-identity/index#sample-apps)) has already been initialized for the [Secret Manager tool](xref:security/app-secrets), it will already have a app secrets identifier (``) in its project file (`.csproj`). In Visual Studio, you can tell if the app secrets ID is present by looking at the **Properties** panel when the project is selected in **Solution Explorer**. If the app hasn't been initialized, execute the following command in a command shell opened to the backend server project's directory. In Visual Studio, you can use the Developer PowerShell command prompt (use the `cd` command to change the directory to the server project after you open the command shell). + +```dotnetcli +dotnet user-secrets init +``` + +Set the email API key with the Secret Manager tool. In the following example, the key name is `EmailAuthKey`, and the key is represented by the `{KEY}` placeholder. Execute the following command with the API key: + +```dotnetcli +dotnet user-secrets set "EmailAuthKey" "{KEY}" +``` + +If using Visual Studio, you can confirm the secret is set by right-clicking the server project in **Solution Explorer** and selecting **Manage User Secrets**. + +For more information, see . + +[!INCLUDE[](~/blazor/security/includes/secure-authentication-flows.md)] + +## Implement `IEmailSender` in the server project + +The following example is based on Mailchimp's Transactional API using [Mandrill.net](https://www.nuget.org/packages/Mandrill.net). For a different provider, refer to their documentation on how to implement sending an email message. + +Add the [Mandrill.net](https://www.nuget.org/packages/Mandrill.net) NuGet package to the backend server project. + +Add the following `EmailSender` class to implement . In the following example, `AppUser` is a . + +`EmailSender.cs`: + +```csharp +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using Mandrill; +using Mandrill.Model; + +namespace Backend; + +public class EmailSender(IOptions optionsAccessor, + ILogger logger) : IEmailSender +{ + private readonly ILogger logger = logger; + + public AuthMessageSenderOptions Options { get; } = optionsAccessor.Value; + + public Task SendConfirmationLinkAsync(AppUser user, string email, + string confirmationLink) => SendEmailAsync(email, "Confirm your email", + "Please confirm your account by " + + $"clicking here."); + + public Task SendPasswordResetLinkAsync(AppUser user, string email, + string resetLink) => SendEmailAsync(email, "Reset your password", + $"Please reset your password by clicking here."); + + public Task SendPasswordResetCodeAsync(AppUser user, string email, + string resetCode) => SendEmailAsync(email, "Reset your password", + $"Please reset your password using the following code: {resetCode}"); + + public async Task SendEmailAsync(string toEmail, string subject, string message) + { + if (string.IsNullOrEmpty(Options.EmailAuthKey)) + { + throw new Exception("Null EmailAuthKey"); + } + + await Execute(Options.EmailAuthKey, subject, message, toEmail); + } + + public async Task Execute(string apiKey, string subject, string message, + string toEmail) + { + var api = new MandrillApi(apiKey); + var mandrillMessage = new MandrillMessage("sarah@contoso.com", toEmail, + subject, message); + await api.Messages.SendAsync(mandrillMessage); + + logger.LogInformation("Email to {EmailAddress} sent!", toEmail); + } +} +``` + +> [!NOTE] +> Body content for messages might require special encoding for the email service provider. If links in the message body can't be followed, consult the service provider's documentation. + +## Configure the server project to support email + +In the backend server's `Program` file, require a confirmed email to register an account. Locate the line that calls and set the property to `true`: + +```diff +- builder.Services.AddIdentityCore() ++ builder.Services.AddIdentityCore(o => o.SignIn.RequireConfirmedEmail = true) +``` + +Add the following service registration to set the email sender implementation to `EmailSender` for : + +```csharp +builder.Services.AddTransient, EmailSender>(); +``` + +## Update the client project's account registration response + +In the client project's `Register` component (`Components/Identity/Register.razor`), change the message to users on a successful account registration to instruct them to confirm their account. The following example includes a link to trigger Identity on the server to resend the confirmation email. + +```diff +-
+- You successfully registered. Now you can login. +-
++
++ You successfully registered. You must now confirm your account by clicking ++ the link in the email that was sent to you. After confirming your account, ++ you can login to the app. ++ Resend confirmation email ++
+``` + +## Update seed data code to confirm seeded accounts + +In the server project's seed data class (`SeedData.cs`), change the code in the `InitializeAsync` method to confirm the seeded accounts so that they don't require confirmation for each test run of the solution: + +```diff +- if (user.Email is not null) +- { +- var appUser = await userManager.FindByEmailAsync(user.Email); +- +- if (appUser is not null && user.RoleList is not null) +- { +- await userManager.AddToRolesAsync(appUser, user.RoleList); +- } +- } ++ if (appUser is not null) ++ { ++ if (user.RoleList is not null) ++ { ++ await userManager.AddToRolesAsync(appUser, user.RoleList); ++ } ++ ++ var token = await userManager.GenerateEmailConfirmationTokenAsync(appUser); ++ await userManager.ConfirmEmailAsync(appUser, token); ++ } +``` + +## Password recovery + +Password recovery requires the server app to adopt an email provider in order to send password resent codes to users. Therefore, the guidance earlier in this article to enable account confirmation should be followed to enable an email provider. + +Password recovery is a two-step process: + +1. A POST request is made to the `/forgotPassword` endpoint provided by in the server project. A message in the UI instructs the user to check their email for a reset code. +1. A POST request is made to the `/resetPassword` endpoint of the server project with the user's email addres, password reset code, and new password. + +The preceding steps are demonstrated by the following implementation guidance for the [sample apps](xref:blazor/security/webassembly/standalone-with-identity/index#sample-apps). + +Add the following method signatures to the `IAccountManagement` class (`Identity/IAccountManagement.cs`) in the client project (`BlazorWasmAuth`). + +```csharp +public Task ForgotPasswordAsync(string email); + +public Task ResetPasswordAsync(string email, string resetCode, + string newPassword); +``` + +Provide implementations for the preceding methods in the `CookieAuthenticationStateProvider` class (`Identity/CookieAuthenticationStateProvider.cs`): + +```csharp +/// +/// Begin the password recovery process by issuing a POST request to the +/// '/forgotPassword' endpoint. +/// +/// The user's email address. +/// A indicating success or failure. +public async Task ForgotPasswordAsync(string email) +{ + try + { + // make the request + var result = await httpClient.PostAsJsonAsync( + "forgotPassword", new + { + email + }); + + // successful? + if (result.IsSuccessStatusCode) + { + return true; + } + } + catch { } + + // unknown error + return false; +} + +/// +/// Reset the user's password by issuing a POST request to the +/// '/resetPassword' endpoint. +/// +/// The user's email address. +/// The user's reset code. +/// The user's new password. +/// The result serialized to a . +/// +public async Task ResetPasswordAsync(string email, string resetCode, + string newPassword) +{ + string[] defaultDetail = ["An unknown error prevented resetting the password."]; + + try + { + // make the request + var result = await httpClient.PostAsJsonAsync( + "resetPassword", new + { + email, + resetCode, + newPassword + }); + + // successful? + if (result.IsSuccessStatusCode) + { + return new FormResult { Succeeded = true }; + } + + // body should contain details about why it failed + var details = await result.Content.ReadAsStringAsync(); + var problemDetails = JsonDocument.Parse(details); + var errors = new List(); + var errorList = problemDetails.RootElement.GetProperty("errors"); + + foreach (var errorEntry in errorList.EnumerateObject()) + { + if (errorEntry.Value.ValueKind == JsonValueKind.String) + { + errors.Add(errorEntry.Value.GetString()!); + } + else if (errorEntry.Value.ValueKind == JsonValueKind.Array) + { + errors.AddRange( + errorEntry.Value.EnumerateArray().Select( + e => e.GetString() ?? string.Empty) + .Where(e => !string.IsNullOrEmpty(e))); + } + } + + // return the error list + return new FormResult + { + Succeeded = false, + ErrorList = problemDetails == null ? defaultDetail : [.. errors] + }; + } + catch { } + + // unknown error + return new FormResult + { + Succeeded = false, + ErrorList = defaultDetail + }; +} +``` + +Add the following `ForgotPassword` component to the client project (`BlazorWasmAuth`). Code lines in the following component are shortened for display in this article. + +`Components/Identity/ForgotPassword.razor`: + +```razor +@page "/forgot-password" +@using System.ComponentModel.DataAnnotations +@using BlazorWasmAuth.Identity +@inject IAccountManagement Acct + +Forgot your password? + +

Forgot your password?

+

Provide your email address and select the Reset password button.

+
+
+
+ @if (!passwordResetCodeSent) + { + + + + +
+ + + +
+ +
+ } + else + { + if (passwordResetSuccess) + { + if (errors) + { + foreach (var error in errorList) + { +
@error
+ } + } + else + { +
+ Your password was reset. You may login + to the app with your new password. +
+ } + } + else + { +
+ A password reset code has been sent to your email address. + Obtain the code from the email for this form. +
+ + + + +
+ + + +
+
+ + + +
+
+ + + +
+ +
+ } + } +
+
+ +@code { + private bool passwordResetCodeSent; + private bool passwordResetSuccess, errors; + private string[] errorList = []; + + [SupplyParameterFromForm(FormName = "forgot-password")] + private InputModel Input { get; set; } = new(); + + [SupplyParameterFromForm(FormName = "reset-password")] + private ResetModel Reset { get; set; } = new(); + + private async Task OnValidSubmitStep1Async() + { + passwordResetCodeSent = await Acct.ForgotPasswordAsync(Input.Email); + } + + private async Task OnValidSubmitStep2Async() + { + var result = await Acct.ResetPasswordAsync(Input.Email, Reset.ResetCode, + Reset.NewPassword); + + if (result.Succeeded) + { + passwordResetSuccess = true; + + } + else + { + errors = true; + errorList = result.ErrorList; + } + } + + private sealed class InputModel + { + [Required] + [EmailAddress] + [Display(Name = "Email")] + public string Email { get; set; } = string.Empty; + } + + private sealed class ResetModel + { + [Required] + [Base64String] + public string ResetCode { get; set; } = string.Empty; + + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at " + + "max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public string NewPassword { get; set; } = string.Empty; + + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation " + + "password do not match.")] + public string ConfirmPassword { get; set; } = string.Empty; + } +} +``` + +In the `Login` component (`Components/Identity/Login.razor`) of the client project immediately before the closing `` tag, add a forgot password link to reach the `ForgotPassword` component: + +```html + +``` + +## Email and activity timeout + +The default inactivity timeout is 14 days. The following code sets the inactivity timeout to five days with sliding expiration: + +```csharp +builder.Services.ConfigureApplicationCookie(options => { + options.ExpireTimeSpan = TimeSpan.FromDays(5); + options.SlidingExpiration = true; +}); +``` + +## Change all ASP.NET Core Data Protection token lifespans + +The following code changes Data Protection tokens' timeout period to three hours: + +```csharp +builder.Services.Configure(options => + options.TokenLifespan = TimeSpan.FromHours(3)); +``` + +The built-in Identity user tokens ([AspNetCore/src/Identity/Extensions.Core/src/TokenOptions.cs](https://github.com/dotnet/aspnetcore/blob/main/src/Identity/Extensions.Core/src/TokenOptions.cs)) have a [one day timeout](https://github.com/dotnet/AspNetCore/blob/main/src/Identity/Core/src/DataProtectionTokenProviderOptions.cs). + +[!INCLUDE[](~/includes/aspnetcore-repo-ref-source-links.md)] + +## Change the email token lifespan + +The default token lifespan of the [Identity user tokens](https://github.com/dotnet/AspNetCore/blob/main/src/Identity/Extensions.Core/src/TokenOptions.cs) is [one day](https://github.com/dotnet/AspNetCore/blob/main/src/Identity/Core/src/DataProtectionTokenProviderOptions.cs). + +[!INCLUDE[](~/includes/aspnetcore-repo-ref-source-links.md)] + +To change the email token lifespan, add a custom and : + +`CustomTokenProvider.cs`: + +```csharp +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; + +namespace BlazorSample; + +public class CustomEmailConfirmationTokenProvider + : DataProtectorTokenProvider where TUser : class +{ + public CustomEmailConfirmationTokenProvider( + IDataProtectionProvider dataProtectionProvider, + IOptions options, + ILogger> logger) + : base(dataProtectionProvider, options, logger) + { + } +} + +public class EmailConfirmationTokenProviderOptions + : DataProtectionTokenProviderOptions +{ + public EmailConfirmationTokenProviderOptions() + { + Name = "EmailDataProtectorTokenProvider"; + TokenLifespan = TimeSpan.FromHours(4); + } +} + +public class CustomPasswordResetTokenProvider + : DataProtectorTokenProvider where TUser : class +{ + public CustomPasswordResetTokenProvider( + IDataProtectionProvider dataProtectionProvider, + IOptions options, + ILogger> logger) + : base(dataProtectionProvider, options, logger) + { + } +} + +public class PasswordResetTokenProviderOptions : + DataProtectionTokenProviderOptions +{ + public PasswordResetTokenProviderOptions() + { + Name = "PasswordResetDataProtectorTokenProvider"; + TokenLifespan = TimeSpan.FromHours(3); + } +} +``` + +Configure the services to use the custom token provider in the `Program` file: + +```csharp +builder.Services.AddIdentityCore(options => + { + options.SignIn.RequireConfirmedAccount = true; + options.Tokens.ProviderMap.Add("CustomEmailConfirmation", + new TokenProviderDescriptor( + typeof(CustomEmailConfirmationTokenProvider))); + options.Tokens.EmailConfirmationTokenProvider = + "CustomEmailConfirmation"; + }) + .AddEntityFrameworkStores() + .AddSignInManager() + .AddDefaultTokenProviders(); + +builder.Services + .AddTransient>(); +``` + +## Enable account confirmation after a site has users + +Enabling account confirmation on a site with users locks out all the existing users. Existing users are locked out because their accounts aren't confirmed. To work around existing user lockout, use one of the following approaches: + +* Update the database to mark all existing users as confirmed. +* Confirm existing users. For example, batch-send emails with confirmation links. + +## Troubleshoot + +If you can't get email working: + +* Set a breakpoint in `EmailSender.Execute` to verify `SendEmailAsync` is called. +* Create a console app to send email using code similar to `EmailSender.Execute` to debug the problem. +* Review the account email history pages at the email provider's website. +* Check your spam folder for messages. +* Try another email alias on a different email provider, such as Microsoft, Yahoo, or Gmail. +* Try sending to different email accounts. + +## Additional resources + +* [Mandrill.net (GitHub repository)](https://github.com/feinoujc/Mandrill.net) +* [Mailchimp developer: Transactional API](https://mailchimp.com/developer/transactional/docs/fundamentals/) diff --git a/aspnetcore/blazor/security/webassembly/standalone-with-identity.md b/aspnetcore/blazor/security/webassembly/standalone-with-identity/index.md similarity index 97% rename from aspnetcore/blazor/security/webassembly/standalone-with-identity.md rename to aspnetcore/blazor/security/webassembly/standalone-with-identity/index.md index bff4e36f56f2..d65f44c4a47d 100644 --- a/aspnetcore/blazor/security/webassembly/standalone-with-identity.md +++ b/aspnetcore/blazor/security/webassembly/standalone-with-identity/index.md @@ -6,7 +6,7 @@ monikerRange: '>= aspnetcore-8.0' ms.author: riande ms.custom: mvc ms.date: 11/12/2024 -uid: blazor/security/webassembly/standalone-with-identity +uid: blazor/security/webassembly/standalone-with-identity/index --- # Secure ASP.NET Core Blazor WebAssembly with ASP.NET Core Identity @@ -82,12 +82,15 @@ At this point, you must provide custom code to parse the : +Scenarios covered by the Blazor documentation set: + +* [Account confirmation, password management, and recovery codes](xref:blazor/security/webassembly/standalone-with-identity/account-confirmation-and-password-recovery) +* Two-factor authentication (2FA): *Implementation guidance coming soon!* This work is tracked by [Add 2FA/TOTP coverage to the Standalone+Identity article+sample (`dotnet/AspNetCore.Docs` #33772)](https://github.com/dotnet/AspNetCore.Docs/issues/33772). In the meantime, general information to aid you with a custom implementation is available in . + +For information on additional Identity scenarios provided by the API, see : * Secure selected endpoints -* Token authentication -* Two-factor authentication (2FA) -* Recovery codes +* Two-factor authentication (2FA) and recovery codes * User info management ## Use secure authentication flows to maintain sensitive data and credentials diff --git a/aspnetcore/release-notes/aspnetcore-8.0.md b/aspnetcore/release-notes/aspnetcore-8.0.md index 1fbf19992879..29017d9c4bb5 100644 --- a/aspnetcore/release-notes/aspnetcore-8.0.md +++ b/aspnetcore/release-notes/aspnetcore-8.0.md @@ -311,7 +311,7 @@ The Blazor documentation hosts a new article and sample app to cover securing a For more information, see the following resources: -* +* * [What's new with identity in .NET 8 (blog post)](https://devblogs.microsoft.com/dotnet/whats-new-with-identity-in-dotnet-8/#the-blazor-identity-ui) ### Blazor Server with Yarp routing diff --git a/aspnetcore/security/authentication/accconfirm.md b/aspnetcore/security/authentication/accconfirm.md index c51718eeed7f..ac19e4377f13 100644 --- a/aspnetcore/security/authentication/accconfirm.md +++ b/aspnetcore/security/authentication/accconfirm.md @@ -19,7 +19,10 @@ This tutorial shows how to build an ASP.NET Core app with email confirmation and :::moniker range=">= aspnetcore-8.0" -For Blazor guidance, which adds to or supersedes the guidance in this article, see . +For Blazor guidance, which adds to or supersedes the guidance in this article, see the following resources: + +* +* :::moniker-end diff --git a/aspnetcore/security/authentication/identity-api-authorization.md b/aspnetcore/security/authentication/identity-api-authorization.md index ba4b53a0dca7..f1930e4b4dbd 100644 --- a/aspnetcore/security/authentication/identity-api-authorization.md +++ b/aspnetcore/security/authentication/identity-api-authorization.md @@ -15,7 +15,7 @@ uid: security/authentication/identity/spa [ASP.NET Core Identity](xref:security/authentication/identity) provides APIs that handle authentication, authorization, and identity management. The APIs make it possible to secure endpoints of a Web API backend with cookie-based authentication. A token-based option is available for clients that can't use cookies, but in using this you are responsible for ensuring the tokens are kept secure. We recommend using cookies for browser-based applications, because, by default, the browser automatically handles them without exposing them to JavaScript. -This article shows how to use Identity to secure a Web API backend for SPAs such as Angular, React, and Vue apps. The same backend APIs can be used to secure [Blazor WebAssembly apps](xref:blazor/security/webassembly/standalone-with-identity). +This article shows how to use Identity to secure a Web API backend for SPAs such as Angular, React, and Vue apps. The same backend APIs can be used to secure [Blazor WebAssembly apps](xref:blazor/security/webassembly/standalone-with-identity/index). ## Prerequisites diff --git a/aspnetcore/security/authentication/scaffold-identity.md b/aspnetcore/security/authentication/scaffold-identity.md index cc17a4c12da2..20b223a02ff5 100644 --- a/aspnetcore/security/authentication/scaffold-identity.md +++ b/aspnetcore/security/authentication/scaffold-identity.md @@ -24,7 +24,7 @@ Although the scaffolder generates the necessary C# code to scaffold Identity int Inspect the changes after running the Identity scaffolder. We recommend using GitHub or another source control system that shows file changes with a revert changes feature. -Services are required when using [two-factor authentication (2FA)](xref:blazor/security/server/qrcodes-for-authenticator-apps), [account confirmation and password recovery](xref:blazor/security/server/account-confirmation-and-password-recovery), and other security features with Identity. Services or service stubs aren't generated when scaffolding Identity. Services to enable these features must be added manually. +Services are required when using [two-factor authentication (2FA)](xref:blazor/security/server/qrcodes-for-authenticator-apps), [account confirmation and password recovery](xref:blazor/security/account-confirmation-and-password-recovery), and other security features with Identity. Services or service stubs aren't generated when scaffolding Identity. Services to enable these features must be added manually. ## Razor Pages and MVC Identity scaffolding diff --git a/aspnetcore/toc.yml b/aspnetcore/toc.yml index 0dc8b8a8ca05..02b22755aedc 100644 --- a/aspnetcore/toc.yml +++ b/aspnetcore/toc.yml @@ -610,8 +610,6 @@ items: uid: blazor/security/server/static-server-side-rendering - name: Interactive server-side rendering threats uid: blazor/security/server/interactive-server-side-rendering - - name: Account confirmation and password recovery - uid: blazor/security/server/account-confirmation-and-password-recovery - name: QR code generation uid: blazor/security/server/qrcodes-for-authenticator-apps - name: Additional scenarios @@ -621,7 +619,11 @@ items: - name: Overview uid: blazor/security/webassembly/index - name: Standalone with Identity - uid: blazor/security/webassembly/standalone-with-identity + items: + - name: Overview + uid: blazor/security/webassembly/standalone-with-identity/index + - name: Account confirmation and password recovery + uid: blazor/security/webassembly/standalone-with-identity/account-confirmation-and-password-recovery - name: Standalone with Authentication library uid: blazor/security/webassembly/standalone-with-authentication-library - name: Standalone with Microsoft Accounts @@ -648,6 +650,8 @@ items: uid: blazor/security/blazor-web-app-entra - name: Blazor Web App with OIDC uid: blazor/security/blazor-web-app-oidc + - name: Account confirmation and password recovery + uid: blazor/security/account-confirmation-and-password-recovery - name: Content Security Policy uid: blazor/security/content-security-policy - name: State management diff --git a/aspnetcore/whats-new/dotnet-AspNetCore.Docs-mod1.md b/aspnetcore/whats-new/dotnet-AspNetCore.Docs-mod1.md index f1f315a8435e..3ed917e19bf1 100644 --- a/aspnetcore/whats-new/dotnet-AspNetCore.Docs-mod1.md +++ b/aspnetcore/whats-new/dotnet-AspNetCore.Docs-mod1.md @@ -13,7 +13,7 @@ Welcome to what's new in the ASP.NET Core docs for January 2024. This article li ### New articles -- +- ### Updated articles @@ -59,7 +59,7 @@ Welcome to what's new in the ASP.NET Core docs for January 2024. This article li - - Content follow-up updates (8.0) - - Content follow-up updates (8.0) - - Content follow-up updates (8.0) -- - Add troubleshooting guidance +- - Add troubleshooting guidance - - Groups/roles article and Graph article updates - "Base address" clarifications for `HttpClient` diff --git a/aspnetcore/whats-new/dotnet-AspNetCore.Docs-mod2.md b/aspnetcore/whats-new/dotnet-AspNetCore.Docs-mod2.md index 1af658f3c6d1..20fd67d912d6 100644 --- a/aspnetcore/whats-new/dotnet-AspNetCore.Docs-mod2.md +++ b/aspnetcore/whats-new/dotnet-AspNetCore.Docs-mod2.md @@ -59,7 +59,7 @@ Welcome to what's new in the ASP.NET Core docs for February 2024. This article l - Add WebAssembly (runtime) startup callbacks - Blazor Startup - sample environment variable name - Startup - manually start Standalone Blazor WebAssembly -- - Add roles and test user guidance +- - Add roles and test user guidance - - [Blazor] SignalR - remove "using System" reminder - - Update 'Blazor Server' references - diff --git a/aspnetcore/whats-new/dotnet-AspNetCore.Docs-mod3.md b/aspnetcore/whats-new/dotnet-AspNetCore.Docs-mod3.md index 72e98b6c80ed..47eff425a726 100644 --- a/aspnetcore/whats-new/dotnet-AspNetCore.Docs-mod3.md +++ b/aspnetcore/whats-new/dotnet-AspNetCore.Docs-mod3.md @@ -82,7 +82,7 @@ Welcome to what's new in the ASP.NET Core docs for March 2024. This article list - - Blazor - data-binding - event fix - - WebSocket compression/CSP and security guidance - - Add BWA global Auto approach -- - WASM+Identity same-site & antiforgery updates +- - WASM+Identity same-site & antiforgery updates ## Fundamentals From 92c43e7b73b269a1597b570a337b6fd2dc91b232 Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Wed, 30 Oct 2024 10:41:21 -0400 Subject: [PATCH 02/11] Updates --- .openpublishing.redirection.json | 2 +- .../security/account-confirmation-and-password-recovery.md | 2 +- .../account-confirmation-and-password-recovery.md | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.openpublishing.redirection.json b/.openpublishing.redirection.json index c9297ef09d17..13596ca21d6a 100644 --- a/.openpublishing.redirection.json +++ b/.openpublishing.redirection.json @@ -1329,7 +1329,7 @@ "redirect_document_id": false }, { - "source_path": "blazor/security/server/account-confirmation-and-password-recovery.md", + "source_path": "aspnetcore/blazor/security/server/account-confirmation-and-password-recovery.md", "redirect_url": "/aspnet/core/blazor/security/account-confirmation-and-password-recovery", "redirect_document_id": false } diff --git a/aspnetcore/blazor/security/account-confirmation-and-password-recovery.md b/aspnetcore/blazor/security/account-confirmation-and-password-recovery.md index 3630c3e2c682..0f86170b982d 100644 --- a/aspnetcore/blazor/security/account-confirmation-and-password-recovery.md +++ b/aspnetcore/blazor/security/account-confirmation-and-password-recovery.md @@ -69,7 +69,7 @@ The following example is based on Mailchimp's Transactional API using [Mandrill. Add the [Mandrill.net](https://www.nuget.org/packages/Mandrill.net) NuGet package to the project. -Add the following `EmailSender` class to implement . +Add the following `EmailSender` class to implement . `Components/Account/EmailSender.cs`: diff --git a/aspnetcore/blazor/security/webassembly/standalone-with-identity/account-confirmation-and-password-recovery.md b/aspnetcore/blazor/security/webassembly/standalone-with-identity/account-confirmation-and-password-recovery.md index c386fc310809..0ce95e705242 100644 --- a/aspnetcore/blazor/security/webassembly/standalone-with-identity/account-confirmation-and-password-recovery.md +++ b/aspnetcore/blazor/security/webassembly/standalone-with-identity/account-confirmation-and-password-recovery.md @@ -74,7 +74,7 @@ The following example is based on Mailchimp's Transactional API using [Mandrill. Add the [Mandrill.net](https://www.nuget.org/packages/Mandrill.net) NuGet package to the backend server project. -Add the following `EmailSender` class to implement . In the following example, `AppUser` is a . +Add the following `EmailSender` class to implement . In the following example, `AppUser` is a . `EmailSender.cs`: @@ -141,7 +141,7 @@ In the backend server's `Program` file, require a confirmed email to register an + builder.Services.AddIdentityCore(o => o.SignIn.RequireConfirmedEmail = true) ``` -Add the following service registration to set the email sender implementation to `EmailSender` for : +Add the following service registration to set the email sender implementation to `EmailSender` for : ```csharp builder.Services.AddTransient, EmailSender>(); From a05d94249c4fb3a06af507d2d4f3cd4f6c06d2ea Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Wed, 30 Oct 2024 12:53:35 -0400 Subject: [PATCH 03/11] Updates --- ...ount-confirmation-and-password-recovery.md | 26 ++++++++++--------- ...ount-confirmation-and-password-recovery.md | 26 ++++++++++--------- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/aspnetcore/blazor/security/account-confirmation-and-password-recovery.md b/aspnetcore/blazor/security/account-confirmation-and-password-recovery.md index 0f86170b982d..18a913d56296 100644 --- a/aspnetcore/blazor/security/account-confirmation-and-password-recovery.md +++ b/aspnetcore/blazor/security/account-confirmation-and-password-recovery.md @@ -89,18 +89,20 @@ public class EmailSender(IOptions optionsAccessor, public AuthMessageSenderOptions Options { get; } = optionsAccessor.Value; - public Task SendConfirmationLinkAsync(ApplicationUser user, string email, - string confirmationLink) => SendEmailAsync(email, "Confirm your email", - "Please confirm your account by " + - $"clicking here."); - - public Task SendPasswordResetLinkAsync(ApplicationUser user, string email, - string resetLink) => SendEmailAsync(email, "Reset your password", - $"Please reset your password by clicking here."); - - public Task SendPasswordResetCodeAsync(ApplicationUser user, string email, - string resetCode) => SendEmailAsync(email, "Reset your password", - $"Please reset your password using the following code: {resetCode}"); + public Task SendConfirmationLinkAsync(AppUser user, string email, + string confirmationLink) => SendEmailAsync(email, "Confirm your email", + "Please confirm your account by " + + $"clicking here."); + + public Task SendPasswordResetLinkAsync(AppUser user, string email, + string resetLink) => SendEmailAsync(email, "Reset your password", + "Please reset your password by " + + $"clicking here."); + + public Task SendPasswordResetCodeAsync(AppUser user, string email, + string resetCode) => SendEmailAsync(email, "Reset your password", + "Please reset your password " + + $"using the following code:
{resetCode}"); public async Task SendEmailAsync(string toEmail, string subject, string message) { diff --git a/aspnetcore/blazor/security/webassembly/standalone-with-identity/account-confirmation-and-password-recovery.md b/aspnetcore/blazor/security/webassembly/standalone-with-identity/account-confirmation-and-password-recovery.md index 0ce95e705242..e8035eff51d9 100644 --- a/aspnetcore/blazor/security/webassembly/standalone-with-identity/account-confirmation-and-password-recovery.md +++ b/aspnetcore/blazor/security/webassembly/standalone-with-identity/account-confirmation-and-password-recovery.md @@ -93,18 +93,20 @@ public class EmailSender(IOptions optionsAccessor, public AuthMessageSenderOptions Options { get; } = optionsAccessor.Value; - public Task SendConfirmationLinkAsync(AppUser user, string email, - string confirmationLink) => SendEmailAsync(email, "Confirm your email", - "Please confirm your account by " + - $"clicking here."); - - public Task SendPasswordResetLinkAsync(AppUser user, string email, - string resetLink) => SendEmailAsync(email, "Reset your password", - $"Please reset your password by clicking here."); - - public Task SendPasswordResetCodeAsync(AppUser user, string email, - string resetCode) => SendEmailAsync(email, "Reset your password", - $"Please reset your password using the following code: {resetCode}"); + public Task SendConfirmationLinkAsync(AppUser user, string email, + string confirmationLink) => SendEmailAsync(email, "Confirm your email", + "Please confirm your account by " + + $"clicking here."); + + public Task SendPasswordResetLinkAsync(AppUser user, string email, + string resetLink) => SendEmailAsync(email, "Reset your password", + "Please reset your password by " + + $"clicking here."); + + public Task SendPasswordResetCodeAsync(AppUser user, string email, + string resetCode) => SendEmailAsync(email, "Reset your password", + "Please reset your password " + + $"using the following code:
{resetCode}"); public async Task SendEmailAsync(string toEmail, string subject, string message) { From 9fb40dafc86880231e99951b94f3af23a18a1a46 Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Wed, 30 Oct 2024 13:01:22 -0400 Subject: [PATCH 04/11] Updates --- .../security/account-confirmation-and-password-recovery.md | 2 +- .../account-confirmation-and-password-recovery.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aspnetcore/blazor/security/account-confirmation-and-password-recovery.md b/aspnetcore/blazor/security/account-confirmation-and-password-recovery.md index 18a913d56296..6e6b703f99f2 100644 --- a/aspnetcore/blazor/security/account-confirmation-and-password-recovery.md +++ b/aspnetcore/blazor/security/account-confirmation-and-password-recovery.md @@ -69,7 +69,7 @@ The following example is based on Mailchimp's Transactional API using [Mandrill. Add the [Mandrill.net](https://www.nuget.org/packages/Mandrill.net) NuGet package to the project. -Add the following `EmailSender` class to implement . +Add the following `EmailSender` class to implement . In the following example, `ApplicationUser` is a . The message HTML markup can be further customized. As long as the `message` passed to `MandrillMessage` starts with the `<` character, the Mandrill.net API assumes that the message body is composed in HTML. `Components/Account/EmailSender.cs`: diff --git a/aspnetcore/blazor/security/webassembly/standalone-with-identity/account-confirmation-and-password-recovery.md b/aspnetcore/blazor/security/webassembly/standalone-with-identity/account-confirmation-and-password-recovery.md index e8035eff51d9..2df2183069d6 100644 --- a/aspnetcore/blazor/security/webassembly/standalone-with-identity/account-confirmation-and-password-recovery.md +++ b/aspnetcore/blazor/security/webassembly/standalone-with-identity/account-confirmation-and-password-recovery.md @@ -74,7 +74,7 @@ The following example is based on Mailchimp's Transactional API using [Mandrill. Add the [Mandrill.net](https://www.nuget.org/packages/Mandrill.net) NuGet package to the backend server project. -Add the following `EmailSender` class to implement . In the following example, `AppUser` is a . +Add the following `EmailSender` class to implement . In the following example, `AppUser` is a . The message HTML markup can be further customized. As long as the `message` passed to `MandrillMessage` starts with the `<` character, the Mandrill.net API assumes that the message body is composed in HTML. `EmailSender.cs`: From d537fa67692de5f52b6668f1705003ab4ecd6d0b Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Thu, 31 Oct 2024 07:47:45 -0400 Subject: [PATCH 05/11] Updates --- .openpublishing.redirection.json | 25 + aspnetcore/blazor/call-web-api.md | 2 +- aspnetcore/blazor/components/prerender.md | 2 +- .../prerendering-and-integration.md | 3065 +++++++++++++++++ aspnetcore/blazor/components/render-modes.md | 2 +- aspnetcore/blazor/file-uploads.md | 2 +- .../fundamentals/dependency-injection.md | 2 +- aspnetcore/blazor/fundamentals/signalr.md | 20 +- aspnetcore/blazor/fundamentals/startup.md | 2 +- aspnetcore/blazor/host-and-deploy/server.md | 4 +- .../security/security-considerations.md | 2 +- .../compression-with-untrusted-data.md | 2 +- .../call-dotnet-from-javascript.md | 2 +- .../call-javascript-from-dotnet.md | 4 +- .../javascript-interoperability/index.md | 2 +- .../static-server-rendering.md | 2 +- .../{server => }/additional-scenarios.md | 10 +- .../blazor/security/authentication-state.md | 4 +- .../security/blazor-web-app-with-oidc.md | 4 +- .../blazor/security/includes/httpcontext.md | 6 +- aspnetcore/blazor/security/index.md | 495 ++- .../interactive-server-side-rendering.md | 2 +- .../qrcodes-for-authenticator-apps.md | 2 +- .../static-server-side-rendering.md | 4 +- aspnetcore/release-notes/aspnetcore-8.0.md | 2 +- .../aspnetcore-9/includes/blazor.md | 4 +- .../authentication/identity-enable-qrcodes.md | 2 +- .../authentication/scaffold-identity.md | 2 +- .../social/additional-claims.md | 4 +- aspnetcore/toc.yml | 20 +- .../whats-new/dotnet-AspNetCore.Docs-mod1.md | 2 +- .../whats-new/dotnet-AspNetCore.Docs-mod3.md | 4 +- .../whats-new/dotnet-AspNetCore.Docs-mod4.md | 2 +- 33 files changed, 3638 insertions(+), 71 deletions(-) create mode 100644 aspnetcore/blazor/components/prerendering-and-integration.md rename aspnetcore/blazor/security/{server => }/additional-scenarios.md (98%) rename aspnetcore/blazor/security/{server => }/interactive-server-side-rendering.md (99%) rename aspnetcore/blazor/security/{server => }/qrcodes-for-authenticator-apps.md (99%) rename aspnetcore/blazor/security/{server => }/static-server-side-rendering.md (99%) diff --git a/.openpublishing.redirection.json b/.openpublishing.redirection.json index 13596ca21d6a..133dd8d65645 100644 --- a/.openpublishing.redirection.json +++ b/.openpublishing.redirection.json @@ -1332,6 +1332,31 @@ "source_path": "aspnetcore/blazor/security/server/account-confirmation-and-password-recovery.md", "redirect_url": "/aspnet/core/blazor/security/account-confirmation-and-password-recovery", "redirect_document_id": false + }, + { + "source_path": "aspnetcore/blazor/security/server/interactive-server-side-rendering.md", + "redirect_url": "/aspnet/core/blazor/security/interactive-server-side-rendering", + "redirect_document_id": false + }, + { + "source_path": "aspnetcore/blazor/security/server/qrcodes-for-authenticator-apps.md", + "redirect_url": "/aspnet/core/blazor/security/qrcodes-for-authenticator-apps", + "redirect_document_id": false + }, + { + "source_path": "aspnetcore/blazor/security/server/static-server-side-rendering.md", + "redirect_url": "/aspnet/core/blazor/security/static-server-side-rendering", + "redirect_document_id": false + }, + { + "source_path": "aspnetcore/blazor/security/server/additional-scenarios.md", + "redirect_url": "/aspnet/core/blazor/security/additional-scenarios", + "redirect_document_id": false + }, + { + "source_path": "aspnetcore/blazor/security/server/index.md", + "redirect_url": "/aspnet/core/blazor/security/", + "redirect_document_id": false } ] } diff --git a/aspnetcore/blazor/call-web-api.md b/aspnetcore/blazor/call-web-api.md index 8ad2732ac2fd..35e820337b86 100644 --- a/aspnetcore/blazor/call-web-api.md +++ b/aspnetcore/blazor/call-web-api.md @@ -901,7 +901,7 @@ For guidance on mitigating overposting attacks, see : Includes coverage on using to make secure web API requests. +* : Includes coverage on using to make secure web API requests. * * * [Kestrel HTTPS endpoint configuration](xref:fundamentals/servers/kestrel/endpoints) diff --git a/aspnetcore/blazor/components/prerender.md b/aspnetcore/blazor/components/prerender.md index 03d1f6f3416b..b8492b860ac4 100644 --- a/aspnetcore/blazor/components/prerender.md +++ b/aspnetcore/blazor/components/prerender.md @@ -155,7 +155,7 @@ Prerendering guidance is organized in the Blazor documentation by subject matter * [Prerendering when integrating components into Razor Pages and MVC apps](xref:blazor/components/integration) * Authentication and authorization - * [Server-side threat mitigation: Cross-site scripting (XSS)](xref:blazor/security/server/interactive-server-side-rendering#cross-site-scripting-xss) + * [Server-side threat mitigation: Cross-site scripting (XSS)](xref:blazor/security/interactive-server-side-rendering#cross-site-scripting-xss) * [Server-side unauthorized content display while prerendering with a custom `AuthenticationStateProvider`](xref:blazor/security/server/index#unauthorized-content-display-while-prerendering-with-a-custom-authenticationstateprovider) * [Blazor WebAssembly rendered component authentication with prerendering](xref:blazor/security/webassembly/additional-scenarios#prerendering-with-authentication) diff --git a/aspnetcore/blazor/components/prerendering-and-integration.md b/aspnetcore/blazor/components/prerendering-and-integration.md new file mode 100644 index 000000000000..458646b68fe6 --- /dev/null +++ b/aspnetcore/blazor/components/prerendering-and-integration.md @@ -0,0 +1,3065 @@ +--- +title: Prerender and integrate ASP.NET Core Razor components +author: guardrex +description: Learn about Razor component integration scenarios for Blazor apps, including prerendering of Razor components on the server. +monikerRange: '>= aspnetcore-3.1 < aspnetcore-8.0' +ms.author: riande +ms.custom: mvc +ms.date: 02/09/2024 +uid: blazor/components/prerendering-and-integration +zone_pivot_groups: blazor-hosting-models +--- +# Prerender and integrate ASP.NET Core Razor components + +:::moniker range="< aspnetcore-7.0" + +> [!NOTE] +> This isn't the latest version of this article. For the latest version of this article, see the [.NET 7 version](?view=aspnetcore-7.0&preserve-view=true). + +:::moniker-end + +This article explains Razor component integration scenarios for Blazor apps, including prerendering of Razor components on the server. + +> [!IMPORTANT] +> Framework changes across ASP.NET Core releases led to different sets of instructions in this article. Before using this article's guidance, confirm that the document version selector on this page matches the version of ASP.NET Core that you intend to use for your app. + +:::moniker range=">= aspnetcore-7.0" + +:::zone pivot="webassembly" + +Razor components can be integrated into Razor Pages and MVC apps in a hosted Blazor WebAssembly [solution](xref:blazor/tooling#visual-studio-solution-file-sln). When the page or view is rendered, components can be prerendered at the same time. + +Prerendering can improve [Search Engine Optimization (SEO)](https://developer.mozilla.org/docs/Glossary/SEO) by rendering content for the initial HTTP response that search engines can use to calculate page rank. + +## Solution configuration + +### Prerendering configuration + +To set up prerendering for a hosted Blazor WebAssembly app: + +1. Host the Blazor WebAssembly app in an ASP.NET Core app. A standalone Blazor WebAssembly app can be added to an ASP.NET Core solution, or you can use a hosted Blazor WebAssembly app created from the [Blazor WebAssembly project template](xref:blazor/tooling) with the hosted option: + + * Visual Studio: In the **Additional information** dialog, select the **ASP.NET Core Hosted** checkbox when creating the Blazor WebAssembly app. In this article's examples, the solution is named `BlazorHosted`. + * Visual Studio Code/.NET CLI command shell: `dotnet new blazorwasm -ho` (use the `-ho|--hosted` option). Use the `-o|--output {LOCATION}` option to create a folder for the solution and set the solution's project namespaces. In this article's examples, the solution is named `BlazorHosted` (`dotnet new blazorwasm -ho -o BlazorHosted`). + + For the examples in this article, the hosted solution's name (assembly name) is `BlazorHosted`. The client project's namespace is `BlazorHosted.Client`, and the server project's namespace is `BlazorHosted.Server`. + +1. **Delete** the `wwwroot/index.html` file from the Blazor WebAssembly **:::no-loc text="Client":::** project. + +1. In the **:::no-loc text="Client":::** project, **delete** the following lines in `Program.cs`: + + ```diff + - builder.RootComponents.Add("#app"); + - builder.RootComponents.Add("head::after"); + ``` + +1. Add `_Host.cshtml` file to the **:::no-loc text="Server":::** project's `Pages` folder. You can obtain the files from a project created from the Blazor Server template using Visual Studio or using the .NET CLI with the `dotnet new blazorserver -o BlazorServer` command in a command shell (the `-o BlazorServer` option creates a folder for the project). After placing the files into the **:::no-loc text="Server":::** project's `Pages` folder, make the following changes to the files. + + Make the following changes to the `_Host.cshtml` file: + + * Update the `Pages` namespace at the top of the file to match the namespace of the **:::no-loc text="Server":::** app's pages. The `{APP NAMESPACE}` placeholder in the following example represents the namespace of the donor app's pages that provided the `_Host.cshtml` file: + + Delete: + + ```diff + - @namespace {APP NAMESPACE}.Pages + ``` + + Add: + + ```razor + @namespace BlazorHosted.Server.Pages + ``` + + * Add an [`@using`](xref:mvc/views/razor#using) directive for the **:::no-loc text="Client":::** project at the top of the file: + + ```razor + @using BlazorHosted.Client + ``` + + * Update the stylesheet links to point to the WebAssembly project's stylesheets. In the following example, the client project's namespace is `BlazorHosted.Client`. The `{APP NAMESPACE}` placeholder represents the namespace of the donor app that provided the `_Host.cshtml` file. Update the Component Tag Helper (`` tag) for the `HeadOutlet` component to prerender the component. + + Delete: + + ```diff + - + - + - + ``` + + Add: + + ```cshtml + + + + ``` + + > [!NOTE] + > Leave the `` element that requests the Bootstrap stylesheet (`css/bootstrap/bootstrap.min.css`) in place. + + * Update the Blazor script source to use the client-side Blazor WebAssembly script: + + Delete: + + ```diff + - + ``` + + Add: + + ```html + + ``` + + * Update the `render-mode` of the [Component Tag Helper](xref:mvc/views/tag-helpers/builtin-th/component-tag-helper) to prerender the root `App` component with : + + Delete: + + ```diff + - + ``` + + Add: + + ```cshtml + + ``` + + > [!IMPORTANT] + > Prerendering isn't supported for authentication endpoints (`/authentication/` path segment). For more information, see . + +1. In the `Program.cs` file of the **:::no-loc text="Server":::** project, change the fallback endpoint from the `index.html` file to the `_Host.cshtml` page: + + Delete: + + ```diff + - app.MapFallbackToFile("index.html"); + ``` + + Add: + + ```csharp + app.MapFallbackToPage("/_Host"); + ``` + +1. If the **:::no-loc text="Client":::** and **:::no-loc text="Server":::** projects use one or more common services during prerendering, factor the service registrations into a method that can be called from both projects. For more information, see . + +1. Run the **:::no-loc text="Server":::** project. The hosted Blazor WebAssembly app is prerendered by the **:::no-loc text="Server":::** project for clients. + +### Configuration for embedding Razor components into pages and views + +The following sections and examples for embedding Razor components from the **:::no-loc text="Client":::** Blazor WebAssembly app into pages and views of the server app require additional configuration. + +The **:::no-loc text="Server":::** project must have the following files and folders. + +Razor Pages: + +* `Pages/Shared/_Layout.cshtml` +* `Pages/Shared/_Layout.cshtml.css` +* `Pages/_ViewImports.cshtml` +* `Pages/_ViewStart.cshtml` + +MVC: + +* `Views/Shared/_Layout.cshtml` +* `Views/Shared/_Layout.cshtml.css` +* `Views/_ViewImports.cshtml` +* `Views/_ViewStart.cshtml` + +The preceding files can be obtained by generating an app from the ASP.NET Core project templates using: + +* Visual Studio's new project creation tools. +* Opening a command shell and executing `dotnet new webapp -o {PROJECT NAME}` (Razor Pages) or `dotnet new mvc -o {PROJECT NAME}` (MVC). The option `-o|--output` with a value for the `{PROJECT NAME}` placeholder provides a name for the app and creates a folder for the app. + +Update the namespaces in the imported `_ViewImports.cshtml` file to match those in use by the **:::no-loc text="Server":::** project receiving the files. + +`Pages/_ViewImports.cshtml` (Razor Pages): + +```razor +@using BlazorHosted.Server +@namespace BlazorHosted.Server.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +``` + +`Views/_ViewImports.cshtml` (MVC): + +```razor +@using BlazorHosted.Server +@using BlazorHosted.Server.Models +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +``` + +Update the imported layout file, which is `Pages/Shared/_Layout.cshtml` for Razor Pages or `Views/Shared/_Layout.cshtml` for MVC. + +First, delete the title and the stylesheet from the donor project, which is `RPDonor.styles.css` in the following example. The `{PROJECT NAME}` placeholder represents the donor project's app name. + +```diff +- @ViewData["Title"] - {PROJECT NAME} +- +``` + +Include the **:::no-loc text="Client":::** project's styles in the layout file. In the following example, the **:::no-loc text="Client":::** project's namespace is `BlazorHosted.Client`. The `` element can be updated at the same time. + +Place the following lines in the `<head>` content of the layout file: + +```cshtml +<title>@ViewData["Title"] - BlazorHosted + + + +``` + +The imported layout contains two `Home` (`Index` page) and `Privacy` navigation links. To make the `Home` links point to the hosted Blazor WebAssembly app, change the hyperlinks: + +```diff +- {PROJECT NAME} ++ BlazorHosted +``` + +```diff +- Home ++ Home +``` + +In an MVC layout file: + +```diff +- {PROJECT NAME} ++ BlazorHosted +``` + +```diff +- Home ++ Home +``` + +Update the `