From 637b568cb9723b58bace82d4774be64d84797fca Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Mon, 12 Sep 2022 07:03:42 -0500 Subject: [PATCH 1/2] Updates --- aspnetcore/blazor/fundamentals/routing.md | 4 + .../security/includes/fetchdata-component.md | 5 +- .../includes/logindisplay-component.md | 28 +- aspnetcore/blazor/security/index.md | 653 +++++++- .../webassembly/additional-scenarios.md | 1342 ++++++++++++++++- .../hosted-with-identity-server.md | 99 +- .../blazor/security/webassembly/index.md | 201 ++- .../standalone-with-authentication-library.md | 96 +- 8 files changed, 2183 insertions(+), 245 deletions(-) diff --git a/aspnetcore/blazor/fundamentals/routing.md b/aspnetcore/blazor/fundamentals/routing.md index 5ed784995f62..422c9daba745 100644 --- a/aspnetcore/blazor/fundamentals/routing.md +++ b/aspnetcore/blazor/fundamentals/routing.md @@ -1568,6 +1568,10 @@ The following component: For more information on component disposal, see . +## Navigation history state + +The uses the browser's [History API](https://developer.mozilla.org/docs/Web/API/History_API) to maintain navigation history state associated with each location change made by the app. Maintaining history state is particularly useful in external redirect scenarios, such as when [authenticating users with external identity providers](xref:blazor/security/webassembly/index#customize-authorization). For more information, see the [Navigation options](#navigation-options) section. + ## Navigation options Pass `NavigationOptions` to to control the following behaviors: diff --git a/aspnetcore/blazor/security/includes/fetchdata-component.md b/aspnetcore/blazor/security/includes/fetchdata-component.md index 50538a8456ea..2e818163785d 100644 --- a/aspnetcore/blazor/security/includes/fetchdata-component.md +++ b/aspnetcore/blazor/security/includes/fetchdata-component.md @@ -11,7 +11,10 @@ In order to obtain the actual token to include in the request, the app must chec If the request was successful, the token variable is populated with the access token. The property of the token exposes the literal string to include in the `Authorization` request header. -If the request failed because the token couldn't be provisioned without user interaction, the token result contains a redirect URL. Navigating to this URL takes the user to the login page and back to the current page after a successful authentication. +If the request failed because the token couldn't be provisioned without user interaction: + +* ASP.NET Core 7.0 or later: The app navigates to `AccessTokenResult.InteractiveRequestUrl` using the given `AccessTokenResult.InteractionOptions` to allow refreshing the access token. +* ASP.NET Core 6.0 or earlier: The token result contains a redirect URL. Navigating to this URL takes the user to the login page and back to the current page after a successful authentication. ```razor @page "/fetchdata" diff --git a/aspnetcore/blazor/security/includes/logindisplay-component.md b/aspnetcore/blazor/security/includes/logindisplay-component.md index 6792acfdca84..96d7a7a332fc 100644 --- a/aspnetcore/blazor/security/includes/logindisplay-component.md +++ b/aspnetcore/blazor/security/includes/logindisplay-component.md @@ -5,29 +5,9 @@ The `LoginDisplay` component (`Shared/LoginDisplay.razor`) is rendered in the `M * Offers a button to log out of the app. * For anonymous users, offers the option to log in. -```razor -@using Microsoft.AspNetCore.Components.Authorization -@using Microsoft.AspNetCore.Components.WebAssembly.Authentication -@inject NavigationManager Navigation -@inject SignOutSessionStateManager SignOutManager +Due to changes in the framework across releases of ASP.NET Core, Razor markup for the `LoginDisplay` component isn't shown in this section. To inspect the markup of the component for a given release, use ***either*** of the following approaches: - - - Hello, @context.User.Identity.Name! - - - - Log in - - +* Create an app provisioned for authentication from the default Blazor WebAssembly project template for the version of ASP.NET Core that you intend to use. Inspect the `LoginDisplay` component in the generated app. +* Inspect the `LoginDisplay` component in [reference source](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Shared/LoginDisplay.IndividualMsalAuth.razor). -@code { - private async Task BeginLogout(MouseEventArgs args) - { - await SignOutManager.SetSignOutState(); - Navigation.NavigateTo("authentication/logout"); - } -} -``` + [!INCLUDE[](~/includes/aspnetcore-repo-ref-source-links.md)] diff --git a/aspnetcore/blazor/security/index.md b/aspnetcore/blazor/security/index.md index 8f2b96c6f5bd..c00cb61efd9b 100644 --- a/aspnetcore/blazor/security/index.md +++ b/aspnetcore/blazor/security/index.md @@ -12,7 +12,7 @@ uid: blazor/security/index This article describes ASP.NET Core's support for the configuration and management of security in Blazor apps. -:::moniker range=">= aspnetcore-6.0" +:::moniker range=">= aspnetcore-6.0 < aspnetcore-7.0" Security scenarios differ between Blazor Server and Blazor WebAssembly apps. Because Blazor Server apps run on the server, authorization checks are able to determine: @@ -156,14 +156,14 @@ Set up the `Task<` - - + + - -

Sorry, there's nothing at this address.

+ + ...
@@ -455,29 +455,24 @@ The component, in conjunct * Asynchronous authorization is in progress, which usually means that the process of authenticating the user is in progress. The markup of the [``](xref:Microsoft.AspNetCore.Components.Authorization.AuthorizeRouteView.Authorizing?displayProperty=nameWithType) element is displayed. * Content isn't found. The markup of the [``](xref:Microsoft.AspNetCore.Components.Routing.Router.NotFound?displayProperty=nameWithType) element is displayed. -In the default [Blazor Server project template](xref:blazor/project-structure), the `App` component (`App.razor`) demonstrates how to set custom content: +In the `App` component (`App.razor`): -```razor +``` - - - + + + -

Sorry

-

You're not authorized to reach this page.

-

You may need to log in as a different user.

+ ...
-

Authorization in progress

-

Only visible while authorization is in progress.

+ ...
- -

Sorry

-

Sorry, there's nothing at this address.

+ + ...
@@ -552,7 +547,7 @@ It's likely that the project wasn't created using a Blazor Server template with ```razor - + ... @@ -720,14 +715,14 @@ Set up the `Task<` - - + + - -

Sorry, there's nothing at this address.

+ + ...
@@ -1021,29 +1016,24 @@ The component, in conjunct * Asynchronous authorization is in progress, which usually means that the process of authenticating the user is in progress. The markup of the [``](xref:Microsoft.AspNetCore.Components.Authorization.AuthorizeRouteView.Authorizing?displayProperty=nameWithType) element is displayed. * Content isn't found. The markup of the [``](xref:Microsoft.AspNetCore.Components.Routing.Router.NotFound?displayProperty=nameWithType) element is displayed. -In the default [Blazor Server project template](xref:blazor/project-structure), the `App` component (`App.razor`) demonstrates how to set custom content: +In the `App` component (`App.razor`): ```razor - - - + + + -

Sorry

-

You're not authorized to reach this page.

-

You may need to log in as a different user.

+ ...
-

Authorization in progress

-

Only visible while authorization is in progress.

+ ...
- -

Sorry

-

Sorry, there's nothing at this address.

+ + ...
@@ -1120,7 +1110,7 @@ It's likely that the project wasn't created using a Blazor Server template with ```razor - + ... @@ -1289,14 +1279,14 @@ Set up the `Task<` - - + + - -

Sorry, there's nothing at this address.

+ + ...
@@ -1514,29 +1504,24 @@ The component, in conjunct * Asynchronous authorization is in progress, which usually means that the process of authenticating the user is in progress. The markup of the [``](xref:Microsoft.AspNetCore.Components.Authorization.AuthorizeRouteView.Authorizing?displayProperty=nameWithType) element is displayed. * Content isn't found. The markup of the [``](xref:Microsoft.AspNetCore.Components.Routing.Router.NotFound?displayProperty=nameWithType) element is displayed. -In the default [Blazor Server project template](xref:blazor/project-structure), the `App` component (`App.razor`) demonstrates how to set custom content: +In the `App` component (`App.razor`): ```razor - - - + + + -

Sorry

-

You're not authorized to reach this page.

-

You may need to log in as a different user.

+ ...
-

Authorization in progress

-

Only visible while authorization is in progress.

+ ...
- -

Sorry

-

Sorry, there's nothing at this address.

+ + ...
@@ -1611,7 +1596,7 @@ It's likely that the project wasn't created using a Blazor Server template with ```razor - + ... @@ -1628,3 +1613,559 @@ The and , aren't supported in Razor components. For more information on using ASP.NET Core Identity with Blazor, see [Scaffold ASP.NET Core Identity into a Blazor Server app](xref:security/authentication/scaffold-identity#scaffold-identity-into-a-blazor-server-project). + +## Authentication + +Blazor uses the existing ASP.NET Core authentication mechanisms to establish the user's identity. The exact mechanism depends on how the Blazor app is hosted, Blazor WebAssembly or Blazor Server. + +### Blazor WebAssembly authentication + +In Blazor WebAssembly apps, authentication checks can be bypassed because all client-side code can be modified by users. The same is true for all client-side app technologies, including JavaScript SPA frameworks or native apps for any operating system. + +Add the following: + +* A package reference for [`Microsoft.AspNetCore.Components.Authorization`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.Authorization). + + [!INCLUDE[](~/includes/package-reference.md)] + +* The `Microsoft.AspNetCore.Components.Authorization` namespace to the app's `_Imports.razor` file. + +To handle authentication, use of a built-in or custom service is covered in the following sections. + +For more information on creating apps and configuration, see . + +### Blazor Server authentication + +Blazor Server apps operate over a real-time connection that's created using SignalR. [Authentication in SignalR-based apps](xref:signalr/authn-and-authz) is handled when the connection is established. Authentication can be based on a cookie or some other bearer token. + +The built-in service for Blazor Server apps obtains authentication state data from ASP.NET Core's `HttpContext.User`. This is how authentication state integrates with existing ASP.NET Core authentication mechanisms. + +For more information on creating apps and configuration, see . + +## `AuthenticationStateProvider` service + + is the underlying service used by the component and component to get the authentication state. + +You don't typically use directly. Use the [`AuthorizeView` component](#authorizeview-component) or [`Task`](#expose-the-authentication-state-as-a-cascading-parameter) approaches described later in this article. The main drawback to using directly is that the component isn't notified automatically if the underlying authentication state data changes. + +The service can provide the current user's data, as shown in the following example: + +```razor +@page "/" +@using System.Security.Claims +@using Microsoft.AspNetCore.Components.Authorization +@inject AuthenticationStateProvider AuthenticationStateProvider + +

ClaimsPrincipal Data

+ + + +

@authMessage

+ +@if (claims.Count() > 0) +{ +
    + @foreach (var claim in claims) + { +
  • @claim.Type: @claim.Value
  • + } +
+} + +

@surnameMessage

+ +@code { + private string authMessage; + private string surnameMessage; + private IEnumerable claims = Enumerable.Empty(); + + private async Task GetClaimsPrincipalData() + { + var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); + var user = authState.User; + + if (user.Identity.IsAuthenticated) + { + authMessage = $"{user.Identity.Name} is authenticated."; + claims = user.Claims; + surnameMessage = + $"Surname: {user.FindFirst(c => c.Type == ClaimTypes.Surname)?.Value}"; + } + else + { + authMessage = "The user is NOT authenticated."; + } + } +} +``` + +If `user.Identity.IsAuthenticated` is `true` and because the user is a , claims can be enumerated and membership in roles evaluated. + +For more information on dependency injection (DI) and services, see and . For information on how to implement a custom in Blazor Server apps, see . + +## Expose the authentication state as a cascading parameter + +If authentication state data is required for procedural logic, such as when performing an action triggered by the user, obtain the authentication state data by defining a cascading parameter of type `Task<``>`: + +```razor +@page "/" + + + +

@authMessage

+ +@code { + [CascadingParameter] + private Task authenticationStateTask { get; set; } + + private string authMessage; + + private async Task LogUsername() + { + var authState = await authenticationStateTask; + var user = authState.User; + + if (user.Identity.IsAuthenticated) + { + authMessage = $"{user.Identity.Name} is authenticated."; + } + else + { + authMessage = "The user is NOT authenticated."; + } + } +} +``` + +If `user.Identity.IsAuthenticated` is `true`, claims can be enumerated and membership in roles evaluated. + +Set up the `Task<``>` cascading parameter using the and components in the `App` component (`App.razor`): + +```razor + + + + + + + + ... + + + + +``` + +In a Blazor WebAssembly App, add services for options and authorization to `Program.cs`: + +```csharp +builder.Services.AddOptions(); +builder.Services.AddAuthorizationCore(); +``` + +In a Blazor Server app, services for options and authorization are already present, so no further action is required. + +## Authorization + +After a user is authenticated, *authorization* rules are applied to control what the user can do. + +Access is typically granted or denied based on whether: + +* A user is authenticated (signed in). +* A user is in a *role*. +* A user has a *claim*. +* A *policy* is satisfied. + +Each of these concepts is the same as in an ASP.NET Core MVC or Razor Pages app. For more information on ASP.NET Core security, see the articles under [ASP.NET Core Security and Identity](xref:security/index). + +## AuthorizeView component + +The component selectively displays UI content depending on whether the user is authorized. This approach is useful when you only need to *display* data for the user and don't need to use the user's identity in procedural logic. + +The component exposes a `context` variable of type , which you can use to access information about the signed-in user: + +```razor + +

Hello, @context.User.Identity.Name!

+

You can only see this content if you're authenticated.

+
+``` + +You can also supply different content for display if the user isn't authorized: + +```razor + + +

Hello, @context.User.Identity.Name!

+

You can only see this content if you're authorized.

+ +
+ +

Authentication Failure!

+

You're not signed in.

+
+
+ +@code { + private void SecureMethod() { ... } +} +``` + +The content of `` and `` tags can include arbitrary items, such as other interactive components. + +A default event handler for an authorized element, such as the `SecureMethod` method for the ` + + + Log in + + + +@code{ + public void BeginLogOut() + { + Navigation.NavigateToLogout("authentication/logout"); + } +} +``` + +The following example is from the [Blazor Server project template](xref:blazor/project-structure) and uses ASP.NET Core Identity endpoints in the `Identity` area of the app to process Identity-related work. + +`Shared/LoginDisplay.razor`: + +```razor + + + Hello, @context.User.Identity.Name! +
+ +
+
+ + Register + Log in + +
+``` + +### Role-based and policy-based authorization + +The component supports *role-based* or *policy-based* authorization. + +For role-based authorization, use the parameter: + +```razor + +

You can only see this if you're an admin or superuser.

+
+``` + +For more information, including configuration guidance, see . + +For policy-based authorization, use the parameter: + +```razor + +

You can only see this if you satisfy the "content-editor" policy.

+
+``` + +Claims-based authorization is a special case of policy-based authorization. For example, you can define a policy that requires users to have a certain claim. For more information, see . + +These APIs can be used in either Blazor Server or Blazor WebAssembly apps. + +If neither nor is specified, uses the default policy. + +### Content displayed during asynchronous authentication + +Blazor allows for authentication state to be determined *asynchronously*. The primary scenario for this approach is in Blazor WebAssembly apps that make a request to an external endpoint for authentication. + +While authentication is in progress, displays no content by default. To display content while authentication occurs, use the `` tag: + +```razor + + +

Hello, @context.User.Identity.Name!

+

You can only see this content if you're authenticated.

+
+ +

Authentication in progress

+

You can only see this content while authentication is in progress.

+
+
+``` + +This approach isn't normally applicable to Blazor Server apps. Blazor Server apps know the authentication state as soon as the state is established. content can be provided in a Blazor Server app's component, but the content is never displayed. + +## [Authorize] attribute + +The [`[Authorize]` attribute](xref:Microsoft.AspNetCore.Authorization.AuthorizeAttribute) can be used in Razor components: + +```razor +@page "/" +@attribute [Authorize] + +You can only see this if you're signed in. +``` + +> [!IMPORTANT] +> Only use [`[Authorize]`](xref:Microsoft.AspNetCore.Authorization.AuthorizeAttribute) on `@page` components reached via the Blazor Router. Authorization is only performed as an aspect of routing and *not* for child components rendered within a page. To authorize the display of specific parts within a page, use instead. + +The [`[Authorize]` attribute](xref:Microsoft.AspNetCore.Authorization.AuthorizeAttribute) also supports role-based or policy-based authorization. For role-based authorization, use the parameter: + +```razor +@page "/" +@attribute [Authorize(Roles = "admin, superuser")] + +

You can only see this if you're in the 'admin' or 'superuser' role.

+``` + +For policy-based authorization, use the parameter: + +```razor +@page "/" +@attribute [Authorize(Policy = "content-editor")] + +

You can only see this if you satisfy the 'content-editor' policy.

+``` + +If neither nor is specified, [`[Authorize]`](xref:Microsoft.AspNetCore.Authorization.AuthorizeAttribute) uses the default policy, which by default is to treat: + +* Authenticated (signed-in) users as authorized. +* Unauthenticated (signed-out) users as unauthorized. + +## Resource authorization + +To authorize users for resources, pass the request's route data to the parameter of . + +In the content for a requested route in the `App` component (`App.razor`): + +```razor + +``` + +For more information on how authorization state data is passed and used in procedural logic, see the [Expose the authentication state as a cascading parameter](#expose-the-authentication-state-as-a-cascading-parameter) section. + +When the receives the route data for the resource, authorization policies have access to and that permit custom logic to make authorization decisions. + +In the following example, an `EditUser` policy is created in for the app's authorization service configuration () with the following logic: + +* Determine if a route value exists with a key of `id`. If the key exists, the route value is stored in `value`. +* In a variable named `id`, store `value` as a string or set an empty string value (`string.Empty`). +* If `id` isn't an empty string, assert that the policy is satisfied (return `true`) if the string's value starts with `EMP`. Otherwise, assert that the policy fails (return `false`). + +In either `Program.cs` or `Startup.cs` (depending on the hosting model and framework version): + +* Add namespaces for and : + + ```csharp + using Microsoft.AspNetCore.Components; + using System.Linq; + ``` + +* Add the policy: + + ```csharp + options.AddPolicy("EditUser", policy => + policy.RequireAssertion(context => + { + if (context.Resource is RouteData rd) + { + var routeValue = rd.RouteValues.TryGetValue("id", out var value); + var id = Convert.ToString(value, + System.Globalization.CultureInfo.InvariantCulture) ?? string.Empty; + + if (!string.IsNullOrEmpty(id)) + { + return id.StartsWith("EMP", StringComparison.InvariantCulture); + } + } + + return false; + }) + ); + ``` + +The preceding example is an oversimplified authorization policy, merely used to demonstrate the concept with a working example. For more information on creating and configuring authorization policies, see . + +In the following `EditUser` component, the resource at `/users/{id}/edit` has a route parameter for the user's identifier (`{id}`). The component uses the preceding `EditUser` authorization policy to determine if the route value for `id` starts with `EMP`. If `id` starts with `EMP`, the policy succeeds and access to the component is authorized. If `id` starts with a value other than `EMP` or if `id` is an empty string, the policy fails, and the component doesn't load. + +`Pages/EditUser.razor`: + +```razor +@page "/users/{id}/edit" +@using Microsoft.AspNetCore.Authorization +@attribute [Authorize(Policy = "EditUser")] + +

Edit User

+ +

The 'EditUser' policy is satisfied! Id starts with 'EMP'.

+ +@code { + [Parameter] + public string Id { get; set; } +} +``` + +## Customize unauthorized content with the Router component + +The component, in conjunction with the component, allows the app to specify custom content if: + +* The user fails an [`[Authorize]`](xref:Microsoft.AspNetCore.Authorization.AuthorizeAttribute) condition applied to the component. The markup of the [``](xref:Microsoft.AspNetCore.Components.Authorization.AuthorizeRouteView.NotAuthorized?displayProperty=nameWithType) element is displayed. The [`[Authorize]`](xref:Microsoft.AspNetCore.Authorization.AuthorizeAttribute) attribute is covered in the [`[Authorize]` attribute](#authorize-attribute) section. +* Asynchronous authorization is in progress, which usually means that the process of authenticating the user is in progress. The markup of the [``](xref:Microsoft.AspNetCore.Components.Authorization.AuthorizeRouteView.Authorizing?displayProperty=nameWithType) element is displayed. +* Content isn't found. The markup of the [``](xref:Microsoft.AspNetCore.Components.Routing.Router.NotFound?displayProperty=nameWithType) element is displayed. + +In the `App` component (`App.razor`): + +```razor + + + + + + ... + + + ... + + + + + + ... + + + + +``` + +The content of ``, ``, and `` tags can include arbitrary items, such as other interactive components. + +If the `` tag isn't specified, the uses the following fallback message: + +```html +Not authorized. +``` + +## Procedural logic + +If the app is required to check authorization rules as part of procedural logic, use a cascaded parameter of type `Task<``>` to obtain the user's . `Task<``>` can be combined with other services, such as `IAuthorizationService`, to evaluate policies. + +```razor +@using Microsoft.AspNetCore.Authorization +@inject IAuthorizationService AuthorizationService + + + +@code { + [CascadingParameter] + private Task authenticationStateTask { get; set; } + + private async Task DoSomething() + { + var user = (await authenticationStateTask).User; + + if (user.Identity.IsAuthenticated) + { + // Perform an action only available to authenticated (signed-in) users. + } + + if (user.IsInRole("admin")) + { + // Perform an action only available to users in the 'admin' role. + } + + if ((await AuthorizationService.AuthorizeAsync(user, "content-editor")) + .Succeeded) + { + // Perform an action only available to users satisfying the + // 'content-editor' policy. + } + } +} +``` + +> [!NOTE] +> In a Blazor WebAssembly app component, add the and namespaces: +> +> ```razor +> @using Microsoft.AspNetCore.Authorization +> @using Microsoft.AspNetCore.Components.Authorization +> ``` +> +> These namespaces can be provided globally by adding them to the app's `_Imports.razor` file. + +## Troubleshoot errors + +Common errors: + +* **Authorization requires a cascading parameter of type `Task`. Consider using `CascadingAuthenticationState` to supply this.** + +* **`null` value is received for `authenticationStateTask`** + +It's likely that the project wasn't created using a Blazor Server template with authentication enabled. Wrap a `` around some part of the UI tree, for example in the `App` component (`App.razor`) as follows: + +```razor + + + ... + + +``` + +The supplies the `Task<``>` cascading parameter, which in turn it receives from the underlying DI service. + +## Additional resources + +* Microsoft identity platform documentation + * [Overview](/azure/active-directory/develop/) + * [OAuth 2.0 and OpenID Connect protocols on the Microsoft identity platform](/azure/active-directory/develop/active-directory-v2-protocols) + * [Microsoft identity platform and OAuth 2.0 authorization code flow](/azure/active-directory/develop/v2-oauth2-auth-code-flow) + * [Microsoft identity platform ID tokens](/azure/active-directory/develop/id-tokens) + * [Microsoft identity platform access tokens](/azure/active-directory/develop/access-tokens) +* +* +* [Build a custom version of the Authentication.MSAL JavaScript library](xref:blazor/security/webassembly/additional-scenarios#build-a-custom-version-of-the-authenticationmsal-javascript-library) +* +* [Awesome Blazor: Authentication](https://github.com/AdrienTorris/awesome-blazor#authentication) community sample links + +:::moniker-end diff --git a/aspnetcore/blazor/security/webassembly/additional-scenarios.md b/aspnetcore/blazor/security/webassembly/additional-scenarios.md index 6f8e481e4a8a..86b9fa7ecdef 100644 --- a/aspnetcore/blazor/security/webassembly/additional-scenarios.md +++ b/aspnetcore/blazor/security/webassembly/additional-scenarios.md @@ -12,7 +12,7 @@ uid: blazor/security/webassembly/additional-scenarios This article describes additional security scenarios for Blazor WebAssembly apps. -:::moniker range=">= aspnetcore-6.0" +:::moniker range=">= aspnetcore-6.0 < aspnetcore-7.0" ## Attach tokens to outgoing requests @@ -1056,7 +1056,7 @@ The following subsections explain how to replace: Create a JavaScript library to handle your custom authentication details. > [!WARNING] -> The guidance in this section is an implementation detail of the default and subject to change without notice in upcoming releases of ASP.NET Core. +> The guidance in this section is an implementation detail of the default . The TypeScript code in this section applies specifically to ASP.NET Core 7.0 and is subject to change without notice in upcoming releases of ASP.NET Core. ```typescript // .NET makes calls to an AuthenticationService object in the Window. @@ -2284,7 +2284,7 @@ The following subsections explain how to replace: Create a JavaScript library to handle your custom authentication details. > [!WARNING] -> The guidance in this section is an implementation detail of the default and subject to change without notice in upcoming releases of ASP.NET Core. +> The guidance in this section is an implementation detail of the default . The TypeScript code in this section applies specifically to ASP.NET Core 7.0 and is subject to change without notice in upcoming releases of ASP.NET Core. ```typescript // .NET makes calls to an AuthenticationService object in the Window. @@ -3507,7 +3507,7 @@ The following subsections explain how to replace: Create a JavaScript library to handle your custom authentication details. > [!WARNING] -> The guidance in this section is an implementation detail of the default and subject to change without notice in upcoming releases of ASP.NET Core. +> The guidance in this section is an implementation detail of the default . The TypeScript code in this section applies specifically to ASP.NET Core 7.0 and is subject to change without notice in upcoming releases of ASP.NET Core. ```typescript // .NET makes calls to an AuthenticationService object in the Window. @@ -3689,3 +3689,1337 @@ The preceding example sets redirect URIs with regular string literals. The follo * [`HttpClient` and `HttpRequestMessage` with Fetch API request options](xref:blazor/call-web-api#httpclient-and-httprequestmessage-with-fetch-api-request-options) :::moniker-end + +:::moniker range=">= aspnetcore-7.0" + +## Attach tokens to outgoing requests + + is a used to process access tokens. Tokens are acquired using the service, which is registered by the framework. If a token can't be acquired, an is thrown. has a method that navigates to `AccessTokenResult.InteractiveRequestUrl` using the given `AccessTokenResult.InteractionOptions` to allow refreshing the access token. + +For convenience, the framework provides the preconfigured with the app's base address as an authorized URL. **Access tokens are only added when the request URI is within the app's base URI.** When outgoing request URIs aren't within the app's base URI, use a [custom `AuthorizationMessageHandler` class (*recommended*)](#custom-authorizationmessagehandler-class) or [configure the `AuthorizationMessageHandler`](#configure-authorizationmessagehandler). + +> [!NOTE] +> In addition to the client app configuration for server API access, the server API must also allow cross-origin requests (CORS) when the client and the server don't reside at the same base address. For more information on server-side CORS configuration, see the [Cross-origin resource sharing (CORS)](#cross-origin-resource-sharing-cors) section later in this article. + +In the following example: + +* adds and related services to the service collection and configures a named (`WebAPI`). is the base address of the resource URI when sending requests. is provided by the [`Microsoft.Extensions.Http`](https://www.nuget.org/packages/Microsoft.Extensions.Http) NuGet package. +* is the used to process access tokens. Access tokens are only added when the request URI is within the app's base URI. +* creates and configures an instance for outgoing requests using the configuration that corresponds to the named (`WebAPI`). + +```csharp +using System.Net.Http; +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; + +... + +// AddHttpClient is an extension in Microsoft.Extensions.Http +builder.Services.AddHttpClient("WebAPI", + client => client.BaseAddress = new Uri("https://www.example.com/base")) + .AddHttpMessageHandler(); + +builder.Services.AddScoped(sp => sp.GetRequiredService() + .CreateClient("WebAPI")); +``` + +For a hosted Blazor [solution](xref:blazor/tooling#visual-studio-solution-file-sln) based on the [Blazor WebAssembly project template](xref:blazor/project-structure), request URIs are within the app's base URI by default. Therefore, (`new Uri(builder.HostEnvironment.BaseAddress)`) is assigned to the in an app generated from the project template. + +The configured is used to make authorized requests using the [`try-catch`](/dotnet/csharp/language-reference/keywords/try-catch) pattern: + +```razor +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@inject HttpClient Http + +... + +protected override async Task OnInitializedAsync() +{ + private ExampleType[] examples; + + try + { + examples = + await Http.GetFromJsonAsync("ExampleAPIMethod"); + + ... + } + catch (AccessTokenNotAvailableException exception) + { + exception.Redirect(); + } +} +``` + +## Custom authentication request scenarios + +The following scenarios demonstrate how to customize authentication requests. + +### Customize the login process + +Add additional parameters to a login request by calling `TryAddAdditionalParameter` one or more times on a new instance of `InteractiveRequestOptions`: + +```csharp +var requestOptions = + new InteractiveRequestOptions + { + Interaction = InteractionType.SignIn, + ReturnUrl = Navigation.Uri + }; + +requestOptions.TryAddAdditionalParameter("prompt", "login"); +requestOptions.TryAddAdditionalParameter("login_hint", "peter@example.com"); + +Navigation.NavigateToLogin("authentication/login", requestOptions); +``` + +The preceding example assumes the presence of an `@using`/`using` statement for API in the namespace. + +Obtain an additional parameter by calling `TryGetAdditionalParameter` with the name of the parameter. Remove an additional parameter by calling `TryRemoveAdditionalParameter` with the name of the parameter. + + + +### Logout with a custom return URL + +The following example logs out the user and returns the user to the `/goodbye` endpoint: + +```csharp +Navigation.NavigateToLogout("authentication/logout", "goodbye"); +``` + +### Customize options before obtaining a token interactively + +If an occurs, attach additional parameters for a new identity provider access token request by calling `TryAddAdditionalParameter` one or more times: + +```csharp +try +{ + examples = await Http.GetFromJsonAsync("ExampleAPIMethod"); + + ... +} +catch (AccessTokenNotAvailableException ex) +{ + ex.Redirect(requestOptions => { + requestOptions.TryAddAdditionalParameter("prompt", "login"); + requestOptions.TryAddAdditionalParameter("login_hint", "peter@example.com"); + }); +} +``` + +The preceding example assumes that: + +* The presence of an `@using`/`using` statement for API in the namespace. +* `HttpClient` injected as `Http`. + + + +### Customize options when using an `IAccessTokenProvider` + +If obtaining a token fails when using an , attach additional parameters for the new identity provider access token request by calling `TryAddAdditionalParameter` one or more times: + +```csharp +var accessTokenResult = await AuthorizationService.RequestAccessToken( + new AccessTokenRequestOptions + { + Scopes = new[] { ... } + }); + +if (!accessTokenResult.TryGetToken(out var token)) +{ + var requestOptions = result.InteractiveRequest; + + requestOptions.TryAddAdditionalParameter("prompt", "login"); + requestOptions.TryAddAdditionalParameter("login_hint", "peter@example.com"); + + Navigation.NavigateToLogin(result.InteractiveRequestUrl, requestOptions); +} +``` + +The preceding example assumes: + +* The presence of an `@using`/`using` statement for API in the namespace. +* injected as `AuthorizationService`. + + + +### Obtain the login path from authentication options + +Obtain the configured login path from `RemoteAuthenticationOptions`: + +```csharp +var loginPath = RemoteAuthOptions.Get(Options.DefaultName).AuthenticationPaths.LogInPath; +``` + +The preceding example assumes: + +* The presence of an `@using`/`using` statement for API in the following namespaces: + * + * +* `IOptionsSnapshot>` injected as `RemoteAuthOptions`. + +### Custom `AuthorizationMessageHandler` class + +*This guidance in this section is recommended for client apps that make outgoing requests to URIs that aren't within the app's base URI.* + +In the following example, a custom class extends for use as the for an . configures this handler to authorize outbound HTTP requests using an access token. The access token is only attached if at least one of the authorized URLs is a base of the request URI (). + +```csharp +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; + +public class CustomAuthorizationMessageHandler : AuthorizationMessageHandler +{ + public CustomAuthorizationMessageHandler(IAccessTokenProvider provider, + NavigationManager navigationManager) + : base(provider, navigationManager) + { + ConfigureHandler( + authorizedUrls: new[] { "https://www.example.com/base" }, + scopes: new[] { "example.read", "example.write" }); + } +} +``` + +In the preceding code, the scopes `example.read` and `example.write` are generic examples not meant to reflect valid scopes for any particular provider. For apps that use Azure Active Directory, scopes are similar to `api://41451fa7-82d9-4673-8fa5-69eff5a761fd/API.Access` (trusted publisher domain) or `https://contoso.onmicrosoft.com/41451fa7-82d9-4673-8fa5-69eff5a761fd/API.Access` (untrusted publisher domain). + +In `Program.cs`, `CustomAuthorizationMessageHandler` is registered as a transient service and is configured as the for outgoing instances made by a named : + +```csharp +builder.Services.AddTransient(); + +// AddHttpClient is an extension in Microsoft.Extensions.Http +builder.Services.AddHttpClient("WebAPI", + client => client.BaseAddress = new Uri("https://www.example.com/base")) + .AddHttpMessageHandler(); +``` + +> [!NOTE] +> In the preceding example, the `CustomAuthorizationMessageHandler` is registered as a transient service for . Transient registration is recommended for , which manages its own DI scopes. For more information, see the following resources: +> +> * [Utility base component classes to manage a DI scope](xref:blazor/fundamentals/dependency-injection#utility-base-component-classes-to-manage-a-di-scope) +> * [Detect transient disposables in Blazor WebAssembly apps](xref:blazor/fundamentals/dependency-injection#detect-transient-disposables-in-blazor-webassembly-apps) + +For a hosted Blazor solution based on the [Blazor WebAssembly project template](xref:blazor/project-structure), (`new Uri(builder.HostEnvironment.BaseAddress)`) is assigned to the by default. + +The configured is used to make authorized requests using the [`try-catch`](/dotnet/csharp/language-reference/keywords/try-catch) pattern. Where the client is created with ([`Microsoft.Extensions.Http`](https://www.nuget.org/packages/Microsoft.Extensions.Http) package), the is supplied instances that include access tokens when making requests to the server API. If the request URI is a relative URI, as it is in the following example (`ExampleAPIMethod`), it's combined with the when the client app makes the request: + +```razor +@inject IHttpClientFactory ClientFactory + +... + +@code { + private ExampleType[] examples; + + protected override async Task OnInitializedAsync() + { + try + { + var client = ClientFactory.CreateClient("WebAPI"); + + examples = + await client.GetFromJsonAsync("ExampleAPIMethod"); + } + catch (AccessTokenNotAvailableException exception) + { + exception.Redirect(); + } + } +} +``` + +### Configure `AuthorizationMessageHandler` + + can be configured with authorized URLs, scopes, and a return URL using the method. configures the handler to authorize outbound HTTP requests using an access token. The access token is only attached if at least one of the authorized URLs is a base of the request URI (). If the request URI is a relative URI, it's combined with the . + +In the following example, configures an in `Program.cs`: + +```csharp +using System.Net.Http; +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; + +... + +builder.Services.AddScoped(sp => new HttpClient( + sp.GetRequiredService() + .ConfigureHandler( + authorizedUrls: new[] { "https://www.example.com/base" }, + scopes: new[] { "example.read", "example.write" })) + { + BaseAddress = new Uri("https://www.example.com/base") + }); +``` + +In the preceding code, the scopes `example.read` and `example.write` are generic examples not meant to reflect valid scopes for any particular provider. For apps that use Azure Active Directory, scopes are similar to `api://41451fa7-82d9-4673-8fa5-69eff5a761fd/API.Access` (trusted publisher domain) or `https://contoso.onmicrosoft.com/41451fa7-82d9-4673-8fa5-69eff5a761fd/API.Access` (untrusted publisher domain). + +For a hosted Blazor solution based on the [Blazor WebAssembly project template](xref:blazor/project-structure), is assigned to the following by default: + +* The (`new Uri(builder.HostEnvironment.BaseAddress)`). +* A URL of the `authorizedUrls` array. + +## Typed `HttpClient` + +A typed client can be defined that handles all of the HTTP and token acquisition concerns within a single class. + +`WeatherForecastClient.cs`: + +```csharp +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; +using static {APP ASSEMBLY}.Data; + +public class WeatherForecastClient +{ + private readonly HttpClient http; + + public WeatherForecastClient(HttpClient http) + { + this.http = http; + } + + public async Task GetForecastAsync() + { + var forecasts = new WeatherForecast[0]; + + try + { + forecasts = await http.GetFromJsonAsync( + "WeatherForecast"); + } + catch (AccessTokenNotAvailableException exception) + { + exception.Redirect(); + } + + return forecasts; + } +} +``` + +The placeholder `{APP ASSEMBLY}` is the app's assembly name (for example, `using static BlazorSample.Data;`). + +In `Program.cs`: + +```csharp +using System.Net.Http; +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; + +... + +// AddHttpClient is an extension in Microsoft.Extensions.Http +builder.Services.AddHttpClient( + client => client.BaseAddress = new Uri("https://www.example.com/base")) + .AddHttpMessageHandler(); +``` + +For a hosted Blazor solution based on the [Blazor WebAssembly project template](xref:blazor/project-structure), (`new Uri(builder.HostEnvironment.BaseAddress)`) is assigned to the by default. + +`FetchData` component (`Pages/FetchData.razor`): + +```razor +@inject WeatherForecastClient Client + +... + +protected override async Task OnInitializedAsync() +{ + forecasts = await Client.GetForecastAsync(); +} +``` + +## Configure the `HttpClient` handler + +The handler can be further configured with for outbound HTTP requests. + +In `Program.cs`: + +```csharp +// AddHttpClient is an extension in Microsoft.Extensions.Http +builder.Services.AddHttpClient( + client => client.BaseAddress = new Uri("https://www.example.com/base")) + .AddHttpMessageHandler(sp => sp.GetRequiredService() + .ConfigureHandler( + authorizedUrls: new [] { "https://www.example.com/base" }, + scopes: new[] { "example.read", "example.write" })); +``` + +In the preceding code, the scopes `example.read` and `example.write` are generic examples not meant to reflect valid scopes for any particular provider. For apps that use Azure Active Directory, scopes are similar to `api://41451fa7-82d9-4673-8fa5-69eff5a761fd/API.Access` (trusted publisher domain) or `https://contoso.onmicrosoft.com/41451fa7-82d9-4673-8fa5-69eff5a761fd/API.Access` (untrusted publisher domain). + +For a hosted Blazor solution based on the [Blazor WebAssembly project template](xref:blazor/project-structure), is assigned to the following by default: + +* The (`new Uri(builder.HostEnvironment.BaseAddress)`). +* A URL of the `authorizedUrls` array. + +## Unauthenticated or unauthorized web API requests in an app with a secure default client + +If the Blazor WebAssembly app ordinarily uses a secure default , the app can also make unauthenticated or unauthorized web API requests by configuring a named : + +In `Program.cs`: + +```csharp +// AddHttpClient is an extension in Microsoft.Extensions.Http +builder.Services.AddHttpClient("WebAPI.NoAuthenticationClient", + client => client.BaseAddress = new Uri("https://www.example.com/base")); +``` + +For a hosted Blazor solution based on the [Blazor WebAssembly project template](xref:blazor/project-structure), (`new Uri(builder.HostEnvironment.BaseAddress)`) is assigned to the by default. + +The preceding registration is in addition to the existing secure default registration. + +A component creates the from the ([`Microsoft.Extensions.Http`](https://www.nuget.org/packages/Microsoft.Extensions.Http) package) to make unauthenticated or unauthorized requests: + +```razor +@inject IHttpClientFactory ClientFactory + +... + +@code { + private WeatherForecast[] forecasts; + + protected override async Task OnInitializedAsync() + { + var client = ClientFactory.CreateClient("WebAPI.NoAuthenticationClient"); + + forecasts = await client.GetFromJsonAsync( + "WeatherForecastNoAuthentication"); + } +} +``` + +> [!NOTE] +> The controller in the server API, `WeatherForecastNoAuthenticationController` for the preceding example, isn't marked with the [`[Authorize]` attribute](xref:Microsoft.AspNetCore.Authorization.AuthorizeAttribute). + +The decision whether to use a secure client or an insecure client as the default instance is up to the developer. One way to make this decision is to consider the number of authenticated versus unauthenticated endpoints that the app contacts. If the majority of the app's requests are to secure API endpoints, use the authenticated instance as the default. Otherwise, register the unauthenticated instance as the default. + +An alternative approach to using the is to create a [typed client](#typed-httpclient) for unauthenticated access to anonymous endpoints. + +## Request additional access tokens + +Access tokens can be manually obtained by calling `IAccessTokenProvider.RequestAccessToken`. In the following example, an additional scope is required by an app for the default . The Microsoft Authentication Library (MSAL) example configures the scope with `MsalProviderOptions`: + +In `Program.cs`: + +```csharp +builder.Services.AddMsalAuthentication(options => +{ + ... + + options.ProviderOptions.AdditionalScopesToConsent.Add("{CUSTOM SCOPE 1}"); + options.ProviderOptions.AdditionalScopesToConsent.Add("{CUSTOM SCOPE 2}"); +} +``` + +The `{CUSTOM SCOPE 1}` and `{CUSTOM SCOPE 2}` placeholders in the preceding example are custom scopes. + +The `IAccessTokenProvider.RequestToken` method provides an overload that allows an app to provision an access token with a given set of scopes. + +In a Razor component: + +```razor +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@inject IAccessTokenProvider TokenProvider + +... + +var tokenResult = await TokenProvider.RequestAccessToken( + new AccessTokenRequestOptions + { + Scopes = new[] { "{CUSTOM SCOPE 1}", "{CUSTOM SCOPE 2}" } + }); + +if (tokenResult.TryGetToken(out var token)) +{ + ... +} +``` + +The `{CUSTOM SCOPE 1}` and `{CUSTOM SCOPE 2}` placeholders in the preceding example are custom scopes. + + returns: + +* `true` with the `token` for use. +* `false` if the token isn't retrieved. + +## Cross-origin resource sharing (CORS) + +When sending credentials (authorization cookies/headers) on CORS requests, the `Authorization` header must be allowed by the CORS policy. + +The following policy includes configuration for: + +* Request origins (`http://localhost:5000`, `https://localhost:5001`). +* Any method (verb). +* `Content-Type` and `Authorization` headers. To allow a custom header (for example, `x-custom-header`), list the header when calling . +* Credentials set by client-side JavaScript code (`credentials` property set to `include`). + +```csharp +app.UseCors(policy => + policy.WithOrigins("http://localhost:5000", "https://localhost:5001") + .AllowAnyMethod() + .WithHeaders(HeaderNames.ContentType, HeaderNames.Authorization, "x-custom-header") + .AllowCredentials()); +``` + +A hosted Blazor solution based on the [Blazor WebAssembly project template](xref:blazor/project-structure) uses the same base address for the client and server apps. The client app's is set to a URI of `builder.HostEnvironment.BaseAddress` by default. CORS configuration is **not** required in the default configuration of a hosted Blazor solution. Additional client apps that aren't hosted by the server project and don't share the server app's base address **do** require CORS configuration in the server project. + +For more information, see and the sample app's HTTP Request Tester component (`Components/HTTPRequestTester.razor`). + +## Handle token request errors + +When a single-page application (SPA) authenticates a user using OpenID Connect (OIDC), the authentication state is maintained locally within the SPA and in the Identity Provider (IP) in the form of a session cookie that's set as a result of the user providing their credentials. + +The tokens that the IP emits for the user typically are valid for short periods of time, about one hour normally, so the client app must regularly fetch new tokens. Otherwise, the user would be logged-out after the granted tokens expire. In most cases, OIDC clients are able to provision new tokens without requiring the user to authenticate again thanks to the authentication state or "session" that is kept within the IP. + +There are some cases in which the client can't get a token without user interaction, for example, when for some reason the user explicitly logs out from the IP. This scenario occurs if a user visits `https://login.microsoftonline.com` and logs out. In these scenarios, the app doesn't know immediately that the user has logged out. Any token that the client holds might no longer be valid. Also, the client isn't able to provision a new token without user interaction after the current token expires. + +These scenarios aren't specific to token-based authentication. They are part of the nature of SPAs. An SPA using cookies also fails to call a server API if the authentication cookie is removed. + +When an app performs API calls to protected resources, you must be aware of the following: + +* To provision a new access token to call the API, the user might be required to authenticate again. +* Even if the client has a token that seems to be valid, the call to the server might fail because the token was revoked by the user. + +When the app requests a token, there are two possible outcomes: + +* The request succeeds, and the app has a valid token. +* The request fails, and the app must authenticate the user again to obtain a new token. + +When a token request fails, you need to decide whether you want to save any current state before you perform a redirection. Several approaches exist with increasing levels of complexity: + +* Store the current page state in session storage. During the [`OnInitializedAsync` lifecycle method](xref:blazor/components/lifecycle#component-initialization-oninitializedasync) (), check if state can be restored before continuing. +* Add a query string parameter and use that as a way to signal the app that it needs to re-hydrate the previously saved state. +* Add a query string parameter with a unique identifier to store data in session storage without risking collisions with other items. + +The following example shows how to: + +* Preserve state before redirecting to the login page. +* Recover the previous state afterward authentication using the query string parameter. + +```razor +... +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@inject IAccessTokenProvider TokenProvider +... + + + + + + +@code { + public class Profile + { + public string Name { get; set; } + public string LastName { get; set; } + } + + public Profile User { get; set; } = new Profile(); + + protected override async Task OnInitializedAsync() + { + var currentQuery = new Uri(Navigation.Uri).Query; + + if (currentQuery.Contains("state=resumeSavingProfile")) + { + User = await JS.InvokeAsync("sessionStorage.getState", + "resumeSavingProfile"); + } + } + + public async Task OnSaveAsync() + { + var http = new HttpClient(); + http.BaseAddress = new Uri(Navigation.BaseUri); + + var resumeUri = Navigation.Uri + $"?state=resumeSavingProfile"; + + var tokenResult = await TokenProvider.RequestAccessToken( + new AccessTokenRequestOptions + { + ReturnUrl = resumeUri + }); + + if (tokenResult.TryGetToken(out var token)) + { + http.DefaultRequestHeaders.Add("Authorization", + $"Bearer {token.Value}"); + await http.PostAsJsonAsync("Save", User); + } + else + { + await JS.InvokeVoidAsync("sessionStorage.setState", + "resumeSavingProfile", User); + Navigation.NavigateTo(tokenResult.InteractiveRequestUrl); + } + } +} +``` + +## Save app state before an authentication operation + +During an authentication operation, there are cases where you want to save the app state before the browser is redirected to the IP. This can be the case when you're using a state container and want to restore the state after the authentication succeeds. You can use a custom authentication state object to preserve app-specific state or a reference to it and restore that state after the authentication operation successfully completes. The following example demonstrates the approach. + +A state container class is created in the app with properties to hold the app's state values. In the following example, the container is used to maintain the counter value of the default [Blazor project template's](xref:blazor/project-structure) `Counter` component (`Pages/Counter.razor`). Methods for serializing and deserializing the container are based on . + +```csharp +using System.Text.Json; + +public class StateContainer +{ + public int CounterValue { get; set; } + + public string GetStateForLocalStorage() + { + return JsonSerializer.Serialize(this); + } + + public void SetStateFromLocalStorage(string locallyStoredState) + { + var deserializedState = + JsonSerializer.Deserialize(locallyStoredState); + + CounterValue = deserializedState.CounterValue; + } +} +``` + +The `Counter` component uses the state container to maintain the `currentCount` value outside of the component: + +```razor +@page "/counter" +@inject StateContainer State + +

Counter

+ +

Current count: @currentCount

+ + + +@code { + private int currentCount = 0; + + protected override void OnInitialized() + { + if (State.CounterValue > 0) + { + currentCount = State.CounterValue; + } + } + + private void IncrementCount() + { + currentCount++; + State.CounterValue = currentCount; + } +} +``` + +Create an `ApplicationAuthenticationState` from . Provide an `Id` property, which serves as an identifier for the locally-stored state. + +`ApplicationAuthenticationState.cs`: + +```csharp +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; + +public class ApplicationAuthenticationState : RemoteAuthenticationState +{ + public string Id { get; set; } +} +``` + +The `Authentication` component (`Pages/Authentication.razor`) saves and restores the app's state using local session storage with the `StateContainer` serialization and deserialization methods, `GetStateForLocalStorage` and `SetStateFromLocalStorage`: + +```razor +@page "/authentication/{action}" +@inject IJSRuntime JS +@inject StateContainer State +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication + + + +@code { + [Parameter] + public string Action { get; set; } + + public ApplicationAuthenticationState AuthenticationState { get; set; } = + new ApplicationAuthenticationState(); + + protected override async Task OnInitializedAsync() + { + if (RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogIn, + Action) || + RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogOut, + Action)) + { + AuthenticationState.Id = Guid.NewGuid().ToString(); + + await JS.InvokeVoidAsync("sessionStorage.setItem", + AuthenticationState.Id, State.GetStateForLocalStorage()); + } + } + + private async Task RestoreState(ApplicationAuthenticationState state) + { + if (state.Id != null) + { + var locallyStoredState = await JS.InvokeAsync( + "sessionStorage.getItem", state.Id); + + if (locallyStoredState != null) + { + State.SetStateFromLocalStorage(locallyStoredState); + await JS.InvokeVoidAsync("sessionStorage.removeItem", state.Id); + } + } + } +} +``` + +In the preceding example, `JS` is an injected instance. is registered by the Blazor framework. + +This example uses Azure Active Directory (AAD) for authentication. In `Program.cs`: + +* The `ApplicationAuthenticationState` is configured as the Microsoft Autentication Library (MSAL) `RemoteAuthenticationState` type. +* The state container is registered in the service container. + +```csharp +builder.Services.AddMsalAuthentication(options => +{ + builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication); +}); + +builder.Services.AddSingleton(); +``` + +## Customize app routes + +By default, the [`Microsoft.AspNetCore.Components.WebAssembly.Authentication`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.Authentication) library uses the routes shown in the following table for representing different authentication states. + +| Route | Purpose | +| -------------------------------- | ------- | +| `authentication/login` | Triggers a sign-in operation. | +| `authentication/login-callback` | Handles the result of any sign-in operation. | +| `authentication/login-failed` | Displays error messages when the sign-in operation fails for some reason. | +| `authentication/logout` | Triggers a sign-out operation. | +| `authentication/logout-callback` | Handles the result of a sign-out operation. | +| `authentication/logout-failed` | Displays error messages when the sign-out operation fails for some reason. | +| `authentication/logged-out` | Indicates that the user has successfully logout. | +| `authentication/profile` | Triggers an operation to edit the user profile. | +| `authentication/register` | Triggers an operation to register a new user. | + +The routes shown in the preceding table are configurable via . When setting options to provide custom routes, confirm that the app has a route that handles each path. + +In the following example, all the paths are prefixed with `/security`. + +`Authentication` component (`Pages/Authentication.razor`): + +```razor +@page "/security/{action}" +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication + + + +@code{ + [Parameter] + public string Action { get; set; } +} +``` + +In `Program.cs`: + +```csharp +builder.Services.AddApiAuthorization(options => { + options.AuthenticationPaths.LogInPath = "security/login"; + options.AuthenticationPaths.LogInCallbackPath = "security/login-callback"; + options.AuthenticationPaths.LogInFailedPath = "security/login-failed"; + options.AuthenticationPaths.LogOutPath = "security/logout"; + options.AuthenticationPaths.LogOutCallbackPath = "security/logout-callback"; + options.AuthenticationPaths.LogOutFailedPath = "security/logout-failed"; + options.AuthenticationPaths.LogOutSucceededPath = "security/logged-out"; + options.AuthenticationPaths.ProfilePath = "security/profile"; + options.AuthenticationPaths.RegisterPath = "security/register"; +}); +``` + +If the requirement calls for completely different paths, set the routes as described previously and render the with an explicit action parameter: + +```razor +@page "/register" + + +``` + +You're allowed to break the UI into different pages if you choose to do so. + +## Customize the authentication user interface + + includes a default set of UI pieces for each authentication state. Each state can be customized by passing in a custom . To customize the displayed text during the initial login process, can change the as follows. + +`Authentication` component (`Pages/Authentication.razor`): + +```razor +@page "/security/{action}" +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication + + + + You are about to be redirected to https://login.microsoftonline.com. + + + +@code{ + [Parameter] + public string Action { get; set; } +} +``` + +The has one fragment that can be used per authentication route shown in the following table. + +| Route | Fragment | +| -------------------------------- | ----------------------- | +| `authentication/login` | `` | +| `authentication/login-callback` | `` | +| `authentication/login-failed` | `` | +| `authentication/logout` | `` | +| `authentication/logout-callback` | `` | +| `authentication/logout-failed` | `` | +| `authentication/logged-out` | `` | +| `authentication/profile` | `` | +| `authentication/register` | `` | + +## Customize the user + +Users bound to the app can be customized. + +### Customize the user with a payload claim + +In the following example, the app's authenticated users receive an `amr` claim for each of the user's authentication methods. The `amr` claim identifies how the subject of the token was authenticated in Microsoft Identity Platform v1.0 [payload claims](/azure/active-directory/develop/access-tokens#the-amr-claim). The example uses a custom user account class based on . + +Create a class that extends the class. The following example sets the `AuthenticationMethod` property to the user's array of `amr` JSON property values. `AuthenticationMethod` is populated automatically by the framework when the user is authenticated. + +```csharp +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; + +public class CustomUserAccount : RemoteUserAccount +{ + [JsonPropertyName("amr")] + public string[] AuthenticationMethod { get; set; } +} +``` + +Create a factory that extends to create claims from the user's authentication methods stored in `CustomUserAccount.AuthenticationMethod`: + +```csharp +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; +using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal; + +public class CustomAccountFactory + : AccountClaimsPrincipalFactory +{ + public CustomAccountFactory(NavigationManager navigationManager, + IAccessTokenProviderAccessor accessor) : base(accessor) + { + } + + public override async ValueTask CreateUserAsync( + CustomUserAccount account, RemoteAuthenticationUserOptions options) + { + var initialUser = await base.CreateUserAsync(account, options); + + if (initialUser.Identity.IsAuthenticated) + { + foreach (var value in account.AuthenticationMethod) + { + ((ClaimsIdentity)initialUser.Identity) + .AddClaim(new Claim("amr", value)); + } + } + + return initialUser; + } +} +``` + +Register the `CustomAccountFactory` for the authentication provider in use. Any of the following registrations are valid: + +* : + + ```csharp + using Microsoft.AspNetCore.Components.WebAssembly.Authentication; + + ... + + builder.Services.AddOidcAuthentication(options => + { + ... + }) + .AddAccountClaimsPrincipalFactory(); + ``` + +* : + + ```csharp + using Microsoft.AspNetCore.Components.WebAssembly.Authentication; + + ... + + builder.Services.AddMsalAuthentication(options => + { + ... + }) + .AddAccountClaimsPrincipalFactory(); + ``` + +* : + + ```csharp + using Microsoft.AspNetCore.Components.WebAssembly.Authentication; + + ... + + builder.Services.AddApiAuthorization(options => + { + ... + }) + .AddAccountClaimsPrincipalFactory(); + ``` + +### AAD security groups and roles with a custom user account class + +For an additional example that works with AAD security groups and AAD Administrator Roles and a custom user account class, see . + +## Support prerendering with authentication + +Prerendering content that requires authentication and authorization isn't currently supported. After following the guidance in one of the Blazor WebAssembly security app topics, use the following instructions to create an app that: + +* Prerenders paths for which authorization isn't required. +* Doesn't prerender paths for which authorization is required. + +For the **`Client`** project's `Program.cs` file, factor common service registrations into a separate method (for example, create a `ConfigureCommonServices` method in the **`Client`** project). Common services are those that the developer registers for use by both the client and server projects. + +```csharp +public static void ConfigureCommonServices(IServiceCollection services) +{ + services.Add...; +} +``` + +`Program.cs`: + +```csharp +var builder = WebAssemblyHostBuilder.CreateDefault(args); +... + +builder.Services.AddScoped( ... ); + +ConfigureCommonServices(builder.Services); + +await builder.Build().RunAsync(); +``` + +In the **`Server`** project's `Program.cs` file, register the following additional services and call `ConfigureCommonServices`: + +```csharp +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Components.Server; +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; + +... + +builder.Services.AddRazorPages(); +builder.Services.AddScoped(); + +Client.Program.ConfigureCommonServices(services); +``` + +In the **`Server`** project's `Pages/_Host.cshtml` file, replace the `Component` Tag Helper (``) with the following: + +```cshtml +
+ @if (HttpContext.Request.Path.StartsWithSegments("/authentication")) + { + + } + else + { + + } +
+``` + +In the preceding example: + +* The placeholder `{CLIENT APP ASSEMBLY NAME}` is the client app's assembly name (for example `BlazorSample.Client`). +* The conditional check for the `/authentication` path segment: + * Avoids prerendering (`render-mode="WebAssembly"`) for authentication paths. + * Prerenders (`render-mode="WebAssemblyPrerendered"`) for non-authentication paths. + +## Options for hosted apps and third-party login providers + +When authenticating and authorizing a hosted Blazor WebAssembly app with a third-party provider, there are several options available for authenticating the user. Which one you choose depends on your scenario. + +For more information, see . + +### Authenticate users to only call protected third party APIs + +Authenticate the user with a client-side OAuth flow against the third-party API provider: + + ```csharp + builder.services.AddOidcAuthentication(options => { ... }); + ``` + + In this scenario: + +* The server hosting the app doesn't play a role. +* APIs on the server can't be protected. +* The app can only call protected third-party APIs. + +### Authenticate users with a third-party provider and call protected APIs on the host server and the third party + +Configure Identity with a third-party login provider. Obtain the tokens required for third-party API access and store them. + +When a user logs in, Identity collects access and refresh tokens as part of the authentication process. At that point, there are a couple of approaches available for making API calls to third-party APIs. + +#### Use a server access token to retrieve the third-party access token + +Use the access token generated on the server to retrieve the third-party access token from a server API endpoint. From there, use the third-party access token to call third-party API resources directly from Identity on the client. + +We don't recommend this approach. This approach requires treating the third-party access token as if it were generated for a public client. In OAuth terms, the public app doesn't have a client secret because it can't be trusted to store secrets safely, and the access token is produced for a confidential client. A confidential client is a client that has a client secret and is assumed to be able to safely store secrets. + +* The third-party access token might be granted additional scopes to perform sensitive operations based on the fact that the third-party emitted the token for a more trusted client. +* Similarly, refresh tokens shouldn't be issued to a client that isn't trusted, as doing so gives the client unlimited access unless other restrictions are put into place. + +#### Make API calls from the client to the server API in order to call third-party APIs + +Make an API call from the client to the server API. From the server, retrieve the access token for the third-party API resource and issue whatever call is necessary. + +While this approach requires an extra network hop through the server to call a third-party API, it ultimately results in a safer experience: + +* The server can store refresh tokens and ensure that the app doesn't lose access to third-party resources. +* The app can't leak access tokens from the server that might contain more sensitive permissions. + +## Use OpenID Connect (OIDC) v2.0 endpoints + +The authentication library and [Blazor project templates](xref:blazor/project-structure) use OpenID Connect (OIDC) v1.0 endpoints. To use a v2.0 endpoint, configure the JWT Bearer option. In the following example, AAD is configured for v2.0 by appending a `v2.0` segment to the property: + +```csharp +using Microsoft.AspNetCore.Authentication.JwtBearer; + +... + +builder.Services.Configure( + JwtBearerDefaults.AuthenticationScheme, + options => + { + options.Authority += "/v2.0"; + }); +``` + +Alternatively, the setting can be made in the app settings (`appsettings.json`) file: + +```json +{ + "Local": { + "Authority": "https://login.microsoftonline.com/common/oauth2/v2.0/", + ... + } +} +``` + +If tacking on a segment to the authority isn't appropriate for the app's OIDC provider, such as with non-AAD providers, set the property directly. Either set the property in or in the app settings file (`appsettings.json`) with the `Authority` key. + +The list of claims in the ID token changes for v2.0 endpoints. For more information, see [Why update to Microsoft identity platform (v2.0)?](/azure/active-directory/azuread-dev/azure-ad-endpoint-comparison). + +## Configure and use gRPC in components + +To configure a Blazor WebAssembly app to use the [ASP.NET Core gRPC framework](xref:grpc/index): + +* Enable gRPC-Web on the server. For more information, see . +* Register gRPC services for the app's message handler. The following example configures the app's authorization message handler to use the [`GreeterClient` service from the gRPC tutorial](xref:tutorials/grpc/grpc-start#create-a-grpc-service) (`Program.cs`): + +```csharp +using System.Net.Http; +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; +using Grpc.Net.Client; +using Grpc.Net.Client.Web; +using {APP ASSEMBLY}.Shared; + +... + +builder.Services.AddScoped(sp => +{ + var baseAddressMessageHandler = + sp.GetRequiredService(); + baseAddressMessageHandler.InnerHandler = new HttpClientHandler(); + var grpcWebHandler = + new GrpcWebHandler(GrpcWebMode.GrpcWeb, baseAddressMessageHandler); + var channel = GrpcChannel.ForAddress(builder.HostEnvironment.BaseAddress, + new GrpcChannelOptions { HttpHandler = grpcWebHandler }); + + return new Greeter.GreeterClient(channel); +}); +``` + +The placeholder `{APP ASSEMBLY}` is the app's assembly name (for example, `BlazorSample`). Place the `.proto` file in the `Shared` project of the hosted Blazor solution. + +A component in the client app can make gRPC calls using the gRPC client (`Pages/Grpc.razor`): + +```razor +@page "/grpc" +@using Microsoft.AspNetCore.Authorization +@using {APP ASSEMBLY}.Shared +@attribute [Authorize] +@inject Greeter.GreeterClient GreeterClient + +

Invoke gRPC service

+ +

+ + +

+ +Server response: @serverResponse + +@code { + private string name = "Bert"; + private string serverResponse; + + private async Task GetGreeting() + { + try + { + var request = new HelloRequest { Name = name }; + var reply = await GreeterClient.SayHelloAsync(request); + serverResponse = reply.Message; + } + catch (Grpc.Core.RpcException ex) + when (ex.Status.DebugException is + AccessTokenNotAvailableException tokenEx) + { + tokenEx.Redirect(); + } + } +} +``` + +The placeholder `{APP ASSEMBLY}` is the app's assembly name (for example, `BlazorSample`). To use the `Status.DebugException` property, use [`Grpc.Net.Client`](https://www.nuget.org/packages/Grpc.Net.Client) version 2.30.0 or later. + +For more information, see . + +## Replace the `AuthenticationService` implementation + +The following subsections explain how to replace: + +* Any JavaScript `AuthenticationService` implementation. +* The Microsoft Authentication Library for JavaScript (`MSAL.js`). + +### Replace any JavaScript `AuthenticationService` implementation + +Create a JavaScript library to handle your custom authentication details. + +> [!WARNING] +> The guidance in this section is an implementation detail of the default . The TypeScript code in this section applies specifically to ASP.NET Core 7.0 and is subject to change without notice in upcoming releases of ASP.NET Core. + +```typescript +// .NET makes calls to an AuthenticationService object in the Window. +declare global { + interface Window { AuthenticationService: AuthenticationService } +} + +export interface AuthenticationService { + // Init is called to initialize the AuthenticationService. + public static init(settings: UserManagerSettings & AuthorizeServiceSettings, logger: any) : Promise; + + // Gets the currently authenticated user. + public static getUser() : Promise<{[key: string] : string }>; + + // Tries to get an access token silently. + public static getAccessToken(options: AccessTokenRequestOptions) : Promise; + + // Tries to sign in the user or get an access token interactively. + public static signIn(context: AuthenticationContext) : Promise; + + // Handles the sign-in process when a redirect is used. + public static async completeSignIn(url: string) : Promise; + + // Signs the user out. + public static signOut(context: AuthenticationContext) : Promise; + + // Handles the signout callback when a redirect is used. + public static async completeSignOut(url: string) : Promise; +} + +// The rest of these interfaces match their C# definitions. + +export interface AccessTokenRequestOptions { + scopes: string[]; + returnUrl: string; +} + +export interface AccessTokenResult { + status: AccessTokenResultStatus; + token?: AccessToken; +} + +export interface AccessToken { + value: string; + expires: Date; + grantedScopes: string[]; +} + +export enum AccessTokenResultStatus { + Success = 'Success', + RequiresRedirect = 'RequiresRedirect' +} + +export enum AuthenticationResultStatus { + Redirect = 'Redirect', + Success = 'Success', + Failure = 'Failure', + OperationCompleted = 'OperationCompleted' +}; + +export interface AuthenticationResult { + status: AuthenticationResultStatus; + state?: unknown; + message?: string; +} + +export interface AuthenticationContext { + state?: unknown; + interactiveRequest: InteractiveAuthenticationRequest; +} + +export interface InteractiveAuthenticationRequest { + scopes?: string[]; + additionalRequestParameters?: { [key: string]: any }; +}; +``` + +You can import the library by removing the original ` ++ +``` + +For more information, see [`AuthenticationService.ts` in the `dotnet/aspnetcore` GitHub repository](https://github.com/dotnet/aspnetcore/blob/main/src/Components/WebAssembly/WebAssembly.Authentication/src/Interop/AuthenticationService.ts). + +[!INCLUDE[](~/includes/aspnetcore-repo-ref-source-links.md)] + +### Replace the Microsoft Authentication Library for JavaScript (`MSAL.js`) + +If an app requires a custom version of the [Microsoft Authentication Library for JavaScript (`MSAL.js`)](https://www.npmjs.com/package/@azure/msal-browser), perform the following steps: + +1. Confirm the system has the latest developer .NET SDK or obtain and install the latest developer SDK from [.NET Core SDK: Installers and Binaries](https://github.com/dotnet/installer#installers-and-binaries). Configuration of internal NuGet feeds isn't required for this scenario. +1. Set up the `dotnet/aspnetcore` GitHub repository for development per the docs at [Build ASP.NET Core from Source](https://github.com/dotnet/aspnetcore/blob/main/docs/BuildFromSource.md). Fork and clone or download a ZIP archive of the [dotnet/aspnetcore GitHub repository](https://github.com/dotnet/aspnetcore). +1. Open the `src/Components/WebAssembly/Authentication.Msal/src/Interop/package.json` file and set the desired version of `@azure/msal-browser`. For a list of released versions, visit the [`@azure/msal-browser` npm website](https://www.npmjs.com/package/@azure/msal-browser) and select the **Versions** tab. +1. Build the `Authentication.Msal` project in the `src/Components/WebAssembly/Authentication.Msal/src` folder with the `yarn build` command in a command shell. +1. If the app uses [compressed assets (Brotli/Gzip)](xref:blazor/host-and-deploy/webassembly#compression), compress the `Interop/dist/Release/AuthenticationService.js` file. +1. Copy the `AuthenticationService.js` file and compressed versions (`.br`/`.gz`) of the file, if produced, from the `Interop/dist/Release` folder into the app's `publish/wwwroot/_content/Microsoft.Authentication.WebAssembly.Msal` folder in the app's published assets. + +## Pass custom provider options + +Define a class for passing the data to the underlying JavaScript library. + +> [!IMPORTANT] +> The class's structure must match what the library expects when the JSON is serialized with . + +The following example demonstrates a `ProviderOptions` class with [`JsonPropertyName` attributes](xref:System.Text.Json.Serialization.JsonPropertyNameAttribute) matching a hypothetical custom provider library's expectations: + +```csharp +public class ProviderOptions +{ + public string? Authority { get; set; } + public string? MetadataUrl { get; set; } + + [JsonPropertyName("client_id")] + public string? ClientId { get; set; } + + public IList DefaultScopes { get; } = + new List { "openid", "profile" }; + + [JsonPropertyName("redirect_uri")] + public string? RedirectUri { get; set; } + + [JsonPropertyName("post_logout_redirect_uri")] + public string? PostLogoutRedirectUri { get; set; } + + [JsonPropertyName("response_type")] + public string? ResponseType { get; set; } + + [JsonPropertyName("response_mode")] + public string? ResponseMode { get; set; } +} +``` + +Register the provider options within the DI system and configure the appropriate values: + +```csharp +builder.Services.AddRemoteAuthentication(options => { + options.Authority = "..."; + options.MetadataUrl = "..."; + options.ClientId = "..."; + options.DefaultScopes = new List { "openid", "profile", "myApi" }; + options.RedirectUri = "https://localhost:5001/authentication/login-callback"; + options.PostLogoutRedirectUri = "https://localhost:5001/authentication/logout-callback"; + options.ResponseType = "..."; + options.ResponseMode = "..."; + }); +``` + +The preceding example sets redirect URIs with regular string literals. The following alternatives are available: + +* using : + + ```csharp + Uri.TryCreate($"{builder.HostEnvironment.BaseAddress}authentication/login-callback", UriKind.Absolute, out var redirectUri); + options.RedirectUri = redirectUri; + ``` + +* [Host builder configuration](xref:blazor/fundamentals/configuration#host-builder-configuration): + + ```csharp + options.RedirectUri = builder.Configuration["RedirectUri"]; + ``` + + `wwwroot/appsettings.json`: + + ```json + { + "RedirectUri": "https://localhost:5001/authentication/login-callback" + } + ``` + + +## Additional resources + +* +* [`HttpClient` and `HttpRequestMessage` with Fetch API request options](xref:blazor/call-web-api#httpclient-and-httprequestmessage-with-fetch-api-request-options) + +:::moniker-end diff --git a/aspnetcore/blazor/security/webassembly/hosted-with-identity-server.md b/aspnetcore/blazor/security/webassembly/hosted-with-identity-server.md index 10300e616e04..6807abf39164 100644 --- a/aspnetcore/blazor/security/webassembly/hosted-with-identity-server.md +++ b/aspnetcore/blazor/security/webassembly/hosted-with-identity-server.md @@ -232,33 +232,12 @@ The `LoginDisplay` component (`Shared/LoginDisplay.razor`) is rendered in the `M * Offers the option to register. * Offers the option to log in. -```razor -@using Microsoft.AspNetCore.Components.Authorization -@using Microsoft.AspNetCore.Components.WebAssembly.Authentication -@inject NavigationManager Navigation -@inject SignOutSessionStateManager SignOutManager - - - - Hello, @context.User.Identity.Name! - - - - Register - Log in - - - -@code { - private async Task BeginSignOut(MouseEventArgs args) - { - await SignOutManager.SetSignOutState(); - Navigation.NavigateTo("authentication/logout"); - } -} -``` +Due to changes in the framework across releases of ASP.NET Core, Razor markup for the `LoginDisplay` component isn't shown in this section. To inspect the markup of the component for a given release, use ***either*** of the following approaches: + +* Create an app provisioned for authentication from the default Blazor WebAssembly project template for the version of ASP.NET Core that you intend to use. Inspect the `LoginDisplay` component in the generated app. +* Inspect the `LoginDisplay` component in [reference source](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Shared/LoginDisplay.IndividualLocalAuth.razor). The templated content for `Hosted` equal to `true` is used. + + [!INCLUDE[](~/includes/aspnetcore-repo-ref-source-links.md)] ### Authentication component @@ -793,33 +772,12 @@ The `LoginDisplay` component (`Shared/LoginDisplay.razor`) is rendered in the `M * Offers the option to register. * Offers the option to log in. -```razor -@using Microsoft.AspNetCore.Components.Authorization -@using Microsoft.AspNetCore.Components.WebAssembly.Authentication -@inject NavigationManager Navigation -@inject SignOutSessionStateManager SignOutManager - - - - Hello, @context.User.Identity.Name! - - - - Register - Log in - - - -@code { - private async Task BeginSignOut(MouseEventArgs args) - { - await SignOutManager.SetSignOutState(); - Navigation.NavigateTo("authentication/logout"); - } -} -``` +Due to changes in the framework across releases of ASP.NET Core, Razor markup for the `LoginDisplay` component isn't shown in this section. To inspect the markup of the component for a given release, use ***either*** of the following approaches: + +* Create an app provisioned for authentication from the default Blazor WebAssembly project template for the version of ASP.NET Core that you intend to use. Inspect the `LoginDisplay` component in the generated app. +* Inspect the `LoginDisplay` component in [reference source](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Shared/LoginDisplay.IndividualLocalAuth.razor). The templated content for `Hosted` equal to `true` is used. + + [!INCLUDE[](~/includes/aspnetcore-repo-ref-source-links.md)] ### Authentication component @@ -1524,33 +1482,12 @@ The `LoginDisplay` component (`Shared/LoginDisplay.razor`) is rendered in the `M * Offers the option to register. * Offers the option to log in. -```razor -@using Microsoft.AspNetCore.Components.Authorization -@using Microsoft.AspNetCore.Components.WebAssembly.Authentication -@inject NavigationManager Navigation -@inject SignOutSessionStateManager SignOutManager - - - - Hello, @context.User.Identity.Name! - - - - Register - Log in - - - -@code { - private async Task BeginSignOut(MouseEventArgs args) - { - await SignOutManager.SetSignOutState(); - Navigation.NavigateTo("authentication/logout"); - } -} -``` +Due to changes in the framework across releases of ASP.NET Core, Razor markup for the `LoginDisplay` component isn't shown in this section. To inspect the markup of the component for a given release, use ***either*** of the following approaches: + +* Create an app provisioned for authentication from the default Blazor WebAssembly project template for the version of ASP.NET Core that you intend to use. Inspect the `LoginDisplay` component in the generated app. +* Inspect the `LoginDisplay` component in [reference source](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Shared/LoginDisplay.IndividualLocalAuth.razor). The templated content for `Hosted` equal to `true` is used. + + [!INCLUDE[](~/includes/aspnetcore-repo-ref-source-links.md)] ### Authentication component diff --git a/aspnetcore/blazor/security/webassembly/index.md b/aspnetcore/blazor/security/webassembly/index.md index ca41b58735a2..b2dc69647063 100644 --- a/aspnetcore/blazor/security/webassembly/index.md +++ b/aspnetcore/blazor/security/webassembly/index.md @@ -12,7 +12,7 @@ uid: blazor/security/webassembly/index Blazor WebAssembly apps are secured in the same manner as single-page applications (SPAs). There are several approaches for authenticating users to SPAs, but the most common and comprehensive approach is to use an implementation based on the [OAuth 2.0 protocol](https://oauth.net/), such as [OpenID Connect (OIDC)](https://openid.net/connect/). -:::moniker range=">= aspnetcore-6.0" +:::moniker range=">= aspnetcore-6.0 < aspnetcore-7.0" ## Authentication library @@ -497,3 +497,202 @@ For further configuration guidance, see [!IMPORTANT] +> [Duende Software](https://duendesoftware.com/) might require you to pay a license fee for production use of Duende Identity Server. For more information, see . + +The authentication support in Blazor WebAssembly is built on top of the `oidc-client.js` library, which is used to handle the underlying authentication protocol details. + +Other options for authenticating SPAs exist, such as the use of SameSite cookies. However, the engineering design of Blazor WebAssembly is settled on OAuth and OIDC as the best option for authentication in Blazor WebAssembly apps. [Token-based authentication](xref:security/anti-request-forgery#token-based-authentication) based on [JSON Web Tokens (JWTs)](https://self-issued.info/docs/draft-ietf-oauth-json-web-token.html) was chosen over [cookie-based authentication](xref:security/anti-request-forgery#cookie-based-authentication) for functional and security reasons: + +* Using a token-based protocol offers a smaller attack surface area, as the tokens aren't sent in all requests. +* Server endpoints don't require protection against [Cross-Site Request Forgery (CSRF)](xref:security/anti-request-forgery) because the tokens are sent explicitly. This allows you to host Blazor WebAssembly apps alongside MVC or Razor pages apps. +* Tokens have narrower permissions than cookies. For example, tokens can't be used to manage the user account or change a user's password unless such functionality is explicitly implemented. +* Tokens have a short lifetime, one hour by default, which limits the attack window. Tokens can also be revoked at any time. +* Self-contained JWTs offer guarantees to the client and server about the authentication process. For example, a client has the means to detect and validate that the tokens it receives are legitimate and were emitted as part of a given authentication process. If a third party attempts to switch a token in the middle of the authentication process, the client can detect the switched token and avoid using it. +* Tokens with OAuth and OIDC don't rely on the user agent behaving correctly to ensure that the app is secure. +* Token-based protocols, such as OAuth and OIDC, allow for authenticating and authorizing hosted and standalone apps with the same set of security characteristics. + +> [!IMPORTANT] +> [Prerendering](xref:blazor/components/prerendering-and-integration) isn't supported for authentication endpoints (`/authentication/` path segment). For more information, see . + +## Authentication process with OIDC + +The [`Microsoft.AspNetCore.Components.WebAssembly.Authentication`](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.Authentication) library offers several primitives to implement authentication and authorization using OIDC. In broad terms, authentication works as follows: + +* When an anonymous user selects the login button or requests a page with the [`[Authorize]` attribute](xref:Microsoft.AspNetCore.Authorization.AuthorizeAttribute) applied, the user is redirected to the app's login page (`/authentication/login`). +* In the login page, the authentication library prepares for a redirect to the authorization endpoint. The authorization endpoint is outside of the Blazor WebAssembly app and can be hosted at a separate origin. The endpoint is responsible for determining whether the user is authenticated and for issuing one or more tokens in response. The authentication library provides a login callback to receive the authentication response. + * If the user isn't authenticated, the user is redirected to the underlying authentication system, which is usually ASP.NET Core Identity. + * If the user was already authenticated, the authorization endpoint generates the appropriate tokens and redirects the browser back to the login callback endpoint (`/authentication/login-callback`). +* When the Blazor WebAssembly app loads the login callback endpoint (`/authentication/login-callback`), the authentication response is processed. + * If the authentication process completes successfully, the user is authenticated and optionally sent back to the original protected URL that the user requested. + * If the authentication process fails for any reason, the user is sent to the login failed page (`/authentication/login-failed`), and an error is displayed. + +## Customize authentication + +Blazor WebAssembly provides methods to add and retrieve additional parameters for the underlying Authentication library to conduct remote authentication operations with external identity providers. + +To pass additional parameters, supports passing and retreiving history entry state when performing external location changes. For more information, see the following resources: + +* [Navigation history state](xref:blazor/fundamentals/routing#navigation-history-state) +* [Navigation options](xref:blazor/fundamentals/routing#navigation-options) +* [History API (MDN documentation)](https://developer.mozilla.org/docs/Web/API/History_API) + +The state stored by the History API provides the following benefits for remote authentication: + +* The state passed to the secured app endpoint is tied to the navigation performed to authenticate the user at the `authentication/login` endpoint. +* Extra work encoding and decoding data is avoided. +* The surface attack area is reduced. Unlike using the query string to store navigation state, the state stored by the History API can't be set via a top-level navigation nor be influenced from a different origin. +* The history entry is replaced upon successful authentication, so the state attached to the history entry is removed and doesn't require clean up. + +`InteractiveRequestOptions` represents the request to the identity provider for logging in or provisioning an access token. + +`NavigationManagerExtensions` provides the `NavigateToLogin` method for a login operation and `NavigateToLogout` for a logout operation. The methods call , setting the history entry state with a passed `InteractiveRequestOptions` or a new `InteractiveRequestOptions` instance created by the method for: + +* A user signing in (`InteractionType.SignIn`) with the current URI for the return URL. +* A user signing out (`InteractionType.SignOut`) with the return URL. + +The following authentication scenarios are covered in the article: + +* Customize the login process +* Logout with a custom return URL +* Customize options before obtaining a token interactively +* Customize options when using an `IAccessTokenProvider` +* Obtain the login path from authentication options + +## `Authentication` component + +The `Authentication` component (`Pages/Authentication.razor`) handles remote authentication operations and permits the app to: + +* Configure app routes for authentication states. +* Set UI content for authentication states. +* Manage authentication state. + +Authentication actions, such as registering or signing in a user, are passed to the Blazor framework's component, which persists and controls state across authentication operations. + +For more information and examples, see . + +## Authorization + +In Blazor WebAssembly apps, authorization checks can be bypassed because all client-side code can be modified by users. The same is true for all client-side app technologies, including JavaScript SPA frameworks or native apps for any operating system. + +**Always perform authorization checks on the server within any API endpoints accessed by your client-side app.** + +## Require authorization for the entire app + +Apply the [`[Authorize]` attribute](xref:blazor/security/index#authorize-attribute) ([API documentation](xref:Microsoft.AspNetCore.Authorization.AuthorizeAttribute)) to each Razor component of the app using one of the following approaches: + +* In the app's Imports file, add an [`@using`](xref:mvc/views/razor#using) directive for the namespace with an [`@attribute`](xref:mvc/views/razor#attribute) directive for the [`[Authorize]` attribute](xref:blazor/security/index#authorize-attribute). + + `_Imports.razor`: + + ```razor + @using Microsoft.AspNetCore.Authorization + @attribute [Authorize] + ``` + + Allow anonymous access to the `Authentication` component to permit redirection to the Idenfity Provider. Add the following Razor code to the `Authentication` component under its [`@page`](xref:mvc/views/razor#page) directive. + + `Pages/Authentication.razor`: + + ```razor + @using Microsoft.AspNetCore.Components.WebAssembly.Authentication + @attribute [AllowAnonymous] + ``` + +* Add the attribute to each Razor component in the `Pages` folder. + +> [!NOTE] +> Setting an to a policy with is **not** supported. + +## Refresh tokens + +Refresh tokens can't be secured client-side in Blazor WebAssembly apps. Therefore, refresh tokens shouldn't be sent to the app for direct use. + +Refresh tokens can be maintained and used by the server-side app in a hosted Blazor WebAssembly [solution](xref:blazor/tooling#visual-studio-solution-file-sln) to access third-party APIs. For more information, see . + +## Establish claims for users + +Apps often require claims for users based on a web API call to a server. For example, claims are frequently used to [establish authorization](xref:blazor/security/index#authorization) in an app. In these scenarios, the app requests an access token to access the service and uses the token to obtain the user data for the claims. For examples, see the following resources: + +* [Additional scenarios: Customize the user](xref:blazor/security/webassembly/additional-scenarios#customize-the-user) +* + +## Azure App Service on Linux with Identity Server + +Specify the issuer explicitly when deploying to Azure App Service on Linux with Identity Server. For more information, see . + +## Windows Authentication + +We don't recommend using Windows Authentication with Blazor Webassembly or with any other SPA framework. We recommend using token-based protocols instead of Windows Authentication, such as OIDC with Active Directory Federation Services (ADFS). + +If Windows Authentication is used with Blazor Webassembly or with any other SPA framework, additional measures are required to protect the app from cross-site request forgery (CSRF) tokens. The same concerns that apply to cookies apply to Windows Authentication with the addition that Windows Authentication doesn't offer any mechanism to prevent sharing of the authentication context across origins. Apps using Windows Authentication without additional protection from CSRF should at least be restricted to an organization's intranet and not be used on the Internet. + +For more information, see . + +## Secure a SignalR hub + +To secure a SignalR hub: + +* In the **`Server`** project, apply the [`[Authorize]` attribute](xref:Microsoft.AspNetCore.Authorization.AuthorizeAttribute) to the hub class or to methods of the hub class that you want to secure. + +* In the **`Client`** project's component, supply an access token to the hub connection: + + ```razor + @using Microsoft.AspNetCore.Components.WebAssembly.Authentication + @inject IAccessTokenProvider TokenProvider + + ... + + var tokenResult = await TokenProvider.RequestAccessToken(); + + if (tokenResult.TryGetToken(out var token)) + { + hubConnection = new HubConnectionBuilder() + .WithUrl(NavigationManager.ToAbsoluteUri("/chathub"), + options => { options.AccessTokenProvider = () => Task.FromResult(token?.Value); }) + .Build(); + + ... + } + ``` + +For more information, see . + +## Implementation guidance + +Articles under this *Overview* provide information on authenticating users in Blazor WebAssembly apps against specific providers. + +Standalone Blazor WebAssembly apps: + +* [General guidance for OIDC providers and the WebAssembly Authentication Library](xref:blazor/security/webassembly/standalone-with-authentication-library) +* [Microsoft Accounts](xref:blazor/security/webassembly/standalone-with-microsoft-accounts) +* [Azure Active Directory (AAD)](xref:blazor/security/webassembly/standalone-with-azure-active-directory) +* [Azure Active Directory (AAD) B2C](xref:blazor/security/webassembly/standalone-with-azure-active-directory-b2c) + +Hosted Blazor WebAssembly apps: + +* [Azure Active Directory (AAD)](xref:blazor/security/webassembly/hosted-with-azure-active-directory) +* [Azure Active Directory (AAD) B2C](xref:blazor/security/webassembly/hosted-with-azure-active-directory-b2c) +* [Identity Server](xref:blazor/security/webassembly/hosted-with-identity-server) + +Further configuration guidance is found in the following articles: + +* +* + +## Additional resources + +* [Microsoft identity platform documentation](/azure/active-directory/develop/) +* : Includes guidance on: + * Using Forwarded Headers Middleware to preserve HTTPS scheme information across proxy servers and internal networks. + * Additional scenarios and use cases, including manual scheme configuration, request path changes for correct request routing, and forwarding the request scheme for Linux and non-IIS reverse proxies. +* [Support prerendering with authentication](xref:blazor/security/webassembly/additional-scenarios#support-prerendering-with-authentication) + +:::moniker-end diff --git a/aspnetcore/blazor/security/webassembly/standalone-with-authentication-library.md b/aspnetcore/blazor/security/webassembly/standalone-with-authentication-library.md index 7db623f89442..5b613809ed17 100644 --- a/aspnetcore/blazor/security/webassembly/standalone-with-authentication-library.md +++ b/aspnetcore/blazor/security/webassembly/standalone-with-authentication-library.md @@ -153,32 +153,12 @@ The `LoginDisplay` component (`Shared/LoginDisplay.razor`) is rendered in the `M * Offers a button to log out of the app. * For anonymous users, offers the option to log in. -```razor -@using Microsoft.AspNetCore.Components.Authorization -@using Microsoft.AspNetCore.Components.WebAssembly.Authentication -@inject NavigationManager Navigation -@inject SignOutSessionStateManager SignOutManager - - - - Hello, @context.User.Identity.Name! - - - - Log in - - - -@code { - private async Task BeginSignOut(MouseEventArgs args) - { - await SignOutManager.SetSignOutState(); - Navigation.NavigateTo("authentication/logout"); - } -} -``` +Due to changes in the framework across releases of ASP.NET Core, Razor markup for the `LoginDisplay` component isn't shown in this section. To inspect the markup of the component for a given release, use ***either*** of the following approaches: + +* Create an app provisioned for authentication from the default Blazor WebAssembly project template for the version of ASP.NET Core that you intend to use. Inspect the `LoginDisplay` component in the generated app. +* Inspect the `LoginDisplay` component in [reference source](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Shared/LoginDisplay.IndividualMsalAuth.razor). + + [!INCLUDE[](~/includes/aspnetcore-repo-ref-source-links.md)] ## Authentication component @@ -337,32 +317,12 @@ The `LoginDisplay` component (`Shared/LoginDisplay.razor`) is rendered in the `M * Offers a button to log out of the app. * For anonymous users, offers the option to log in. -```razor -@using Microsoft.AspNetCore.Components.Authorization -@using Microsoft.AspNetCore.Components.WebAssembly.Authentication -@inject NavigationManager Navigation -@inject SignOutSessionStateManager SignOutManager - - - - Hello, @context.User.Identity.Name! - - - - Log in - - - -@code { - private async Task BeginSignOut(MouseEventArgs args) - { - await SignOutManager.SetSignOutState(); - Navigation.NavigateTo("authentication/logout"); - } -} -``` +Due to changes in the framework across releases of ASP.NET Core, Razor markup for the `LoginDisplay` component isn't shown in this section. To inspect the markup of the component for a given release, use ***either*** of the following approaches: + +* Create an app provisioned for authentication from the default Blazor WebAssembly project template for the version of ASP.NET Core that you intend to use. Inspect the `LoginDisplay` component in the generated app. +* Inspect the `LoginDisplay` component in [reference source](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Shared/LoginDisplay.IndividualMsalAuth.razor). + + [!INCLUDE[](~/includes/aspnetcore-repo-ref-source-links.md)] ## Authentication component @@ -521,32 +481,12 @@ The `LoginDisplay` component (`Shared/LoginDisplay.razor`) is rendered in the `M * Offers a button to log out of the app. * For anonymous users, offers the option to log in. -```razor -@using Microsoft.AspNetCore.Components.Authorization -@using Microsoft.AspNetCore.Components.WebAssembly.Authentication -@inject NavigationManager Navigation -@inject SignOutSessionStateManager SignOutManager - - - - Hello, @context.User.Identity.Name! - - - - Log in - - - -@code { - private async Task BeginSignOut(MouseEventArgs args) - { - await SignOutManager.SetSignOutState(); - Navigation.NavigateTo("authentication/logout"); - } -} -``` +Due to changes in the framework across releases of ASP.NET Core, Razor markup for the `LoginDisplay` component isn't shown in this section. To inspect the markup of the component for a given release, use ***either*** of the following approaches: + +* Create an app provisioned for authentication from the default Blazor WebAssembly project template for the version of ASP.NET Core that you intend to use. Inspect the `LoginDisplay` component in the generated app. +* Inspect the `LoginDisplay` component in [reference source](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Shared/LoginDisplay.IndividualMsalAuth.razor). + + [!INCLUDE[](~/includes/aspnetcore-repo-ref-source-links.md)] ## Authentication component From 4f3dbefa8421438e059025e067796ee45fffd573 Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Thu, 15 Sep 2022 07:32:56 -0500 Subject: [PATCH 2/2] Updates --- aspnetcore/blazor/security/index.md | 16 +++++----- .../webassembly/additional-scenarios.md | 30 +++++++++++-------- .../blazor/security/webassembly/index.md | 2 +- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/aspnetcore/blazor/security/index.md b/aspnetcore/blazor/security/index.md index c00cb61efd9b..c6a75ddc38c1 100644 --- a/aspnetcore/blazor/security/index.md +++ b/aspnetcore/blazor/security/index.md @@ -246,7 +246,7 @@ Apps created from a [Blazor project template](xref:blazor/project-structure) tha @using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.WebAssembly.Authentication -@inject NavigationManager Navigation +@inject NavigationManager NavigationManager @inject SignOutSessionStateManager SignOutManager @@ -263,7 +263,7 @@ Apps created from a [Blazor project template](xref:blazor/project-structure) tha private async Task BeginLogout(MouseEventArgs args) { await SignOutManager.SetSignOutState(); - Navigation.NavigateTo("authentication/logout"); + NavigationManager.NavigateTo("authentication/logout"); } } ``` @@ -807,7 +807,7 @@ Apps created from a [Blazor project template](xref:blazor/project-structure) tha @using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.WebAssembly.Authentication -@inject NavigationManager Navigation +@inject NavigationManager NavigationManager @inject SignOutSessionStateManager SignOutManager @@ -824,7 +824,7 @@ Apps created from a [Blazor project template](xref:blazor/project-structure) tha private async Task BeginLogout(MouseEventArgs args) { await SignOutManager.SetSignOutState(); - Navigation.NavigateTo("authentication/logout"); + NavigationManager.NavigateTo("authentication/logout"); } } ``` @@ -1369,7 +1369,7 @@ Apps created from a [Blazor project template](xref:blazor/project-structure) tha @using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.WebAssembly.Authentication -@inject NavigationManager Navigation +@inject NavigationManager NavigationManager @inject SignOutSessionStateManager SignOutManager @@ -1386,7 +1386,7 @@ Apps created from a [Blazor project template](xref:blazor/project-structure) tha private async Task BeginLogout(MouseEventArgs args) { await SignOutManager.SetSignOutState(); - Navigation.NavigateTo("authentication/logout"); + NavigationManager.NavigateTo("authentication/logout"); } } ``` @@ -1847,7 +1847,7 @@ Apps created from a [Blazor project template](xref:blazor/project-structure) tha ```razor @using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.WebAssembly.Authentication -@inject NavigationManager Navigation +@inject NavigationManager NavigationManager @@ -1862,7 +1862,7 @@ Apps created from a [Blazor project template](xref:blazor/project-structure) tha @code{ public void BeginLogOut() { - Navigation.NavigateToLogout("authentication/logout"); + NavigationManager.NavigateToLogout("authentication/logout"); } } ``` diff --git a/aspnetcore/blazor/security/webassembly/additional-scenarios.md b/aspnetcore/blazor/security/webassembly/additional-scenarios.md index 86b9fa7ecdef..cac096160481 100644 --- a/aspnetcore/blazor/security/webassembly/additional-scenarios.md +++ b/aspnetcore/blazor/security/webassembly/additional-scenarios.md @@ -3752,18 +3752,18 @@ protected override async Task OnInitializedAsync() ## Custom authentication request scenarios -The following scenarios demonstrate how to customize authentication requests. +The following scenarios demonstrate how to customize authentication requests and how to obtain the login path from authentication options. ### Customize the login process Add additional parameters to a login request by calling `TryAddAdditionalParameter` one or more times on a new instance of `InteractiveRequestOptions`: ```csharp -var requestOptions = - new InteractiveRequestOptions +InteractiveRequestOptions requestOptions = + new() { - Interaction = InteractionType.SignIn, - ReturnUrl = Navigation.Uri + Interaction = InteractionType.SignIn, + ReturnUrl = NavigationManager.Uri, }; requestOptions.TryAddAdditionalParameter("prompt", "login"); @@ -3772,7 +3772,10 @@ requestOptions.TryAddAdditionalParameter("login_hint", "peter@example.com"); Navigation.NavigateToLogin("authentication/login", requestOptions); ``` -The preceding example assumes the presence of an `@using`/`using` statement for API in the namespace. +The preceding example assumes: + +* The presence of an `@using`/`using` statement for API in the namespace. +* injected as `NavigationManager`. Obtain an additional parameter by calling `TryGetAdditionalParameter` with the name of the parameter. Remove an additional parameter by calling `TryRemoveAdditionalParameter` with the name of the parameter. @@ -3818,7 +3821,7 @@ The preceding example assumes that: If obtaining a token fails when using an , attach additional parameters for the new identity provider access token request by calling `TryAddAdditionalParameter` one or more times: ```csharp -var accessTokenResult = await AuthorizationService.RequestAccessToken( +var accessTokenResult = await TokenProvider.RequestAccessToken( new AccessTokenRequestOptions { Scopes = new[] { ... } @@ -3826,19 +3829,20 @@ var accessTokenResult = await AuthorizationService.RequestAccessToken( if (!accessTokenResult.TryGetToken(out var token)) { - var requestOptions = result.InteractiveRequest; - - requestOptions.TryAddAdditionalParameter("prompt", "login"); - requestOptions.TryAddAdditionalParameter("login_hint", "peter@example.com"); + accessTokenResult.InteractionOptions + .TryAddAdditionalParameter("prompt", "login"); + accessTokenResult.InteractionOptions + .TryAddAdditionalParameter("login_hint", "peter@example.com"); - Navigation.NavigateToLogin(result.InteractiveRequestUrl, requestOptions); + Navigation.NavigateToLogin(accessTokenResult.InteractiveRequestUrl, + accessTokenResult.InteractionOptions); } ``` The preceding example assumes: * The presence of an `@using`/`using` statement for API in the namespace. -* injected as `AuthorizationService`. +* injected as `TokenProvider`. diff --git a/aspnetcore/blazor/security/webassembly/index.md b/aspnetcore/blazor/security/webassembly/index.md index b2dc69647063..7ffddc3d8c98 100644 --- a/aspnetcore/blazor/security/webassembly/index.md +++ b/aspnetcore/blazor/security/webassembly/index.md @@ -548,7 +548,7 @@ The state stored by the History API provides the following benefits for remote a * The state passed to the secured app endpoint is tied to the navigation performed to authenticate the user at the `authentication/login` endpoint. * Extra work encoding and decoding data is avoided. -* The surface attack area is reduced. Unlike using the query string to store navigation state, the state stored by the History API can't be set via a top-level navigation nor be influenced from a different origin. +* The attack surface area is reduced. Unlike using the query string to store navigation state, the state stored by the History API can't be set via a top-level navigation nor be influenced from a different origin. * The history entry is replaced upon successful authentication, so the state attached to the history entry is removed and doesn't require clean up. `InteractiveRequestOptions` represents the request to the identity provider for logging in or provisioning an access token.