Skip to content

Commit 42e6cc3

Browse files
committed
Add BFF pattern pivot
1 parent 69c3895 commit 42e6cc3

3 files changed

Lines changed: 207 additions & 3 deletions

File tree

aspnetcore/blazor/security/blazor-web-app-with-entra.md

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ ms.author: riande
77
ms.custom: mvc
88
ms.date: 11/12/2024
99
uid: blazor/security/blazor-web-app-entra
10+
zone_pivot_groups: blazor-web-app-entra-specification
1011
---
1112
# Secure an ASP.NET Core Blazor Web App with Microsoft Entra ID
1213

@@ -18,6 +19,10 @@ uid: blazor/security/blazor-web-app-entra
1819

1920
This article describes how to secure a Blazor Web App with [Microsoft identity platform](/entra/identity-platform/)/[Microsoft Identity Web packages](/entra/msal/dotnet/microsoft-identity-web/) for [Microsoft Entra ID](https://www.microsoft.com/security/business/microsoft-entra) using a sample app.
2021

22+
:::zone pivot="without-bff-pattern"
23+
24+
This version of the article covers implementing Entra without adopting the [Backend for Frontend (BFF) pattern](/azure/architecture/patterns/backends-for-frontends). The BFF pattern is useful for making authenticated requests to external services. Change the article version selector to **Entra with BFF pattern** if the app's specification calls for adopting the BFF pattern.
25+
2126
The following specification is covered:
2227

2328
* The Blazor Web App uses the [Auto render mode with global interactivity (`InteractiveAuto`)](xref:blazor/components/render-modes).
@@ -93,6 +98,139 @@ Example:
9398
},
9499
```
95100

101+
:::zone-end
102+
103+
:::zone pivot="with-bff-pattern"
104+
105+
This version of the article covers implementing Entra with the [Backend for Frontend (BFF) pattern](/azure/architecture/patterns/backends-for-frontends). Change the article version selector to **Entra without BFF pattern** if the app's specification doesn't call for adopting the BFF pattern.
106+
107+
The following specification is covered:
108+
109+
* The Blazor Web App uses the [Auto render mode with global interactivity (`InteractiveAuto`)](xref:blazor/components/render-modes).
110+
* The server project calls <xref:Microsoft.Extensions.DependencyInjection.WebAssemblyRazorComponentsBuilderExtensions.AddAuthenticationStateSerialization%2A> to add a server-side authentication state provider that uses <xref:Microsoft.AspNetCore.Components.PersistentComponentState> to flow the authentication state to the client. The client calls <xref:Microsoft.Extensions.DependencyInjection.WebAssemblyAuthenticationServiceCollectionExtensions.AddAuthenticationStateDeserialization%2A> to deserialize and use the authentication state passed by the server. The authentication state is fixed for the lifetime of the WebAssembly application.
111+
* The app uses [Microsoft Entra ID](https://www.microsoft.com/security/business/microsoft-entra), based on [Microsoft Identity Web](/entra/msal/dotnet/microsoft-identity-web/) packages.
112+
* Automatic non-interactive token refresh is managed by the framework.
113+
* The app uses server-side and client-side service abstractions to display generated weather data:
114+
* The [Backend for Frontend (BFF) pattern](/azure/architecture/patterns/backends-for-frontends) is adopted using [.NET Aspire](/dotnet/aspire/get-started/aspire-overview) for service discovery and [YARP](https://dotnet.github.io/yarp/) for proxying requests to a weather forecast endpoint on the backend app.
115+
* A backend web API uses JWT-bearer authentication to validate JWT tokens saved by the Blazor Web App in the sign-in cookie.
116+
* Aspire improves the experience of building .NET cloud-native apps. It provides a consistent, opinionated set of tools and patterns for building and running distributed apps.
117+
* YARP (Yet Another Reverse Proxy) is a library used to create a reverse proxy server.
118+
119+
<!-- UPDATE 10.0 Remove at 10.0 -->
120+
121+
For more information on .NET Aspire, see [General Availability of .NET Aspire: Simplifying .NET Cloud-Native Development (May, 2024)](https://devblogs.microsoft.com/dotnet/dotnet-aspire-general-availability/).
122+
123+
## Prerequisite
124+
125+
[.NET Aspire](/dotnet/aspire/get-started/aspire-overview) requires [Visual Studio](https://visualstudio.microsoft.com/) version 17.10 or later.
126+
127+
## Sample app
128+
129+
The sample app consists of five projects:
130+
131+
* .NET Aspire:
132+
* `Aspire.AppHost`: Used to manage the high level orchestration concerns of the app.
133+
* `Aspire.ServiceDefaults`: Contains default .NET Aspire app configurations that can be extended and customized as needed.
134+
* `MinimalApiJwt`: Backend web API, containing an example [Minimal API](xref:fundamentals/minimal-apis) endpoint for weather data.
135+
* `BlazorWebAppEntra`: Server-side project of the Blazor Web App.
136+
* `BlazorWebAppEntra.Client`: Client-side project of the Blazor Web App.
137+
138+
Access sample apps through the latest version folder from the repository's root with the following link. The projects are in the `BlazorWebAppEntraBff` folder for .NET 9 or later.
139+
140+
[View or download sample code](https://github.com/dotnet/blazor-samples) ([how to download](xref:blazor/fundamentals/index#sample-apps))
141+
142+
## .NET Aspire projects
143+
144+
For more information on using .NET Aspire and details on the `.AppHost` and `.ServiceDefaults` projects of the sample app, see the [.NET Aspire documentation](/dotnet/aspire/).
145+
146+
Confirm that you've met the prerequisites for .NET Aspire. For more information, see the *Prerequisites* section of [Quickstart: Build your first .NET Aspire app](/dotnet/aspire/get-started/build-your-first-aspire-app?tabs=visual-studio#prerequisites).
147+
148+
The sample app only configures an insecure HTTP launch profile (`http`) for use during development testing. For more information, including an example of insecure and secure launch settings profiles, see [Allow unsecure transport in .NET Aspire (.NET Aspire documentation)](/dotnet/aspire/troubleshooting/allow-unsecure-transport).
149+
150+
## Server-side Blazor Web App project (`BlazorWebAppEntra`)
151+
152+
The `BlazorWebAppEntra` project is the server-side project of the Blazor Web App.
153+
154+
## Client-side Blazor Web App project (`BlazorWebAppEntra.Client`)
155+
156+
The `BlazorWebAppEntra.Client` project is the client-side project of the Blazor Web App.
157+
158+
If the user needs to log in or out during client-side rendering, a full page reload is initiated.
159+
160+
## Backend web API project (`MinimalApiJwt`)
161+
162+
The `MinimalApiJwt` project is a backend web API for multiple frontend projects. The project configures a [Minimal API](xref:fundamentals/minimal-apis) endpoint for weather data. Requests from the Blazor Web App server-side project (`BlazorWebAppOidc`) are proxied to the `MinimalApiJwt` project.
163+
164+
The `MinimalApiJwt.http` file can be used for testing the weather data request. Note that the `MinimalApiJwt` project must be running to test the endpoint, and the endpoint is hardcoded into the file. For more information, see <xref:test/http-files>.
165+
166+
Secure weather forecast data endpoint in the project's `Program` file:
167+
168+
```csharp
169+
app.MapGet("/weather-forecast", () =>
170+
{
171+
var forecast = Enumerable.Range(1, 5).Select(index =>
172+
new WeatherForecast
173+
(
174+
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
175+
Random.Shared.Next(-20, 55),
176+
summaries[Random.Shared.Next(summaries.Length)]
177+
))
178+
.ToArray();
179+
return forecast;
180+
}).RequireAuthorization();
181+
```
182+
183+
The <xref:Microsoft.AspNetCore.Builder.AuthorizationEndpointConventionBuilderExtensions.RequireAuthorization%2A> extension method requires authorization for the route definition. For any controllers that you add to the project, add the [`[Authorize]` attribute](xref:Microsoft.AspNetCore.Authorization.AuthorizeAttribute) to the controller or action.
184+
185+
## Configuration
186+
187+
This section explains how to configure the sample app.
188+
189+
<xref:Microsoft.Identity.Web.AppBuilderExtension.AddMicrosoftIdentityWebApp%2A> from [Microsoft Identity Web](/entra/msal/dotnet/microsoft-identity-web/) ([`Microsoft.Identity.Web` NuGet package](https://www.nuget.org/packages/Microsoft.Identity.Web), [API documentation](<xref:Microsoft.Identity.Web?displayProperty=fullName>)) is configured by the `AzureAd` section of the server project's `appsettings.json` file.
190+
191+
In the app's registration in the Entra or Azure portal, use a **Web** platform configuration with a **Redirect URI** of `https://localhost/signin-oidc` (a port isn't required). Confirm that **ID tokens** and access tokens under **Implicit grant and hybrid flows** are **not** selected. The OpenID Connect handler automatically requests the appropriate tokens using the code returned from the authorization endpoint. The `Weather.Get` scope is configured in the Azure or Entra portal in **Expose an API**. In **API Permissions**, grant delegated permission with Admin consent to access to the **BlazorWebAppEntra** > **Weather.Get** API.
192+
193+
### Configure the server project
194+
195+
In the server project's app settings file (`appsettings.json`), provide the app's `AzureAd` section configuration. Obtain the application (client) ID, tenant (publisher) domain, and directory (tenant) ID from the app's registration in the Entra or Azure portal.
196+
197+
The App ID URI is obtained for the `Weather.Get` scope. Don't include the scope name, and there's no trailing slash.
198+
199+
```json
200+
"AzureAd": {
201+
"CallbackPath": "/signin-oidc",
202+
"ClientId": "{CLIENT ID}",
203+
"Domain": "{DOMAIN}",
204+
"Instance": "https://login.microsoftonline.com/",
205+
"ResponseType": "code",
206+
"TenantId": "{TENANT ID}",
207+
"AppIdUri": "{APP ID URI}"
208+
},
209+
```
210+
211+
Placeholders in the preceding example:
212+
213+
* `{CLIENT ID}`: The application (client) ID.
214+
* `{DOMAIN}`: The tenant (publisher) domain.
215+
* `{TENANT ID}`: The directory (tenant) ID.
216+
* `{APP ID URI}`: The Application ID URI.
217+
218+
Example:
219+
220+
```json
221+
"AzureAd": {
222+
"CallbackPath": "/signin-oidc",
223+
"ClientId": "00001111-aaaa-2222-bbbb-3333cccc4444",
224+
"Domain": "contoso.onmicrosoft.com",
225+
"Instance": "https://login.microsoftonline.com/",
226+
"ResponseType": "code",
227+
"TenantId": "aaaabbbb-0000-cccc-1111-dddd2222eeee",
228+
"AppIdUri": "api://00001111-aaaa-2222-bbbb-3333cccc4444"
229+
},
230+
```
231+
232+
:::zone-end
233+
96234
The callback path (`CallbackPath`) must match the redirect URI (login callback path) configured when registering the application in the Entra or Azure portal. Paths are configured in the **Authentication** blade of the app's registration. The default value of `CallbackPath` is `/signin-oidc` for a registered redirect URI of `https://localhost/signin-oidc` (a port isn't required).
97235

98236
The <xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.SignedOutCallbackPath%2A> (configuration key: "`SignedOutCallbackPath`") is the request path within the app's base path intercepted by the OpenID Connect handler where the user agent is first returned after signing out from Entra. The sample app doesn't set a value for the path because the default value of "`/signout-callback-oidc`" is used. After intercepting the request, the OpenID Connect handler redirects to the <xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.SignedOutRedirectUri%2A> or <xref:Microsoft.AspNetCore.Authentication.AuthenticationProperties.RedirectUri%2A>, if specified.
@@ -114,6 +252,64 @@ If you don't add the signed-out callback path URI to the app's registration in E
114252
> Entra doesn't redirect a primary admin user (root account) or external user back to the Blazor application. Instead, Entra logs the user out of the app and recommends that they close all of their browser windows. For more information, see [postLogoutRedirectUri not working when authority url contains a tenant ID (`AzureAD/microsoft-authentication-library-for-js` #5783)](https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/5783#issuecomment-1465217522).
115253
116254
[!INCLUDE[](~/blazor/security/includes/secure-authentication-flows.md)]
255+
256+
:::zone pivot="with-bff-pattern"
257+
258+
### Configure the backend web API project (`MinimalApiJwt`)
259+
260+
Configure the project in the <xref:Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions> of the <xref:Microsoft.Extensions.DependencyInjection.JwtBearerExtensions.AddJwtBearer%2A> call in the project's `Program` file.
261+
262+
#### Authority
263+
264+
<xref:Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.Authority%2A> sets the Authority for making OIDC calls.
265+
266+
```csharp
267+
jwtOptions.Authority = "{AUTHORITY}";
268+
```
269+
270+
The following example use a Tenant ID of `aaaabbbb-0000-cccc-1111-dddd2222eeee`.
271+
272+
If the app is registered in an ME-ID tenant, the authority should match the issurer (`iss`) of the JWT returned by the identity provider:
273+
274+
```csharp
275+
jwtOptions.Authority = "https://sts.windows.net/aaaabbbb-0000-cccc-1111-dddd2222eeee/";
276+
```
277+
278+
If the app is registered in an AAD B2C tenant:
279+
280+
```csharp
281+
jwtOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0/";
282+
```
283+
284+
#### Audience
285+
286+
<xref:Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.Audience%2A> sets the Audience for any received OIDC token.
287+
288+
```csharp
289+
jwtOptions.Audience = "{AUDIENCE}";
290+
```
291+
292+
Match the value to just the path of the **Application ID URI** configured when adding the `Weather.Get` scope under **Expose an API** in the Azure or Entra portal.
293+
294+
The following examples use an Application (Client) Id (`{CLIENT ID}`) of `00001111-aaaa-2222-bbbb-3333cccc4444`. The second example uses a directory name (`{DIRECTORY NAME}`) of `contoso`.
295+
296+
If the app is registered in an ME-ID tenant:
297+
298+
App ID URI (`{APP ID URI}`): `api://{CLIENT ID}`
299+
300+
```csharp
301+
jwtOptions.Audience = "api://00001111-aaaa-2222-bbbb-3333cccc4444";
302+
```
303+
304+
If the app is registered in an AAD B2C tenant:
305+
306+
App ID URI (`{APP ID URI}`): `https://{DIRECTORY NAME}.onmicrosoft.com/{CLIENT ID}`
307+
308+
```csharp
309+
jwtOptions.Audience = "https://contoso.onmicrosoft.com/00001111-aaaa-2222-bbbb-3333cccc4444";
310+
```
311+
312+
:::zone-end
117313
118314
### Establish the client secret
119315

aspnetcore/blazor/security/blazor-web-app-with-oidc.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -484,8 +484,6 @@ The sample app only configures an insecure HTTP launch profile (`http`) for use
484484

485485
The `BlazorWebAppOidc` project is the server-side project of the Blazor Web App. The project uses [YARP](https://dotnet.github.io/yarp/) to proxy requests to a weather forecast endpoint in the backend web API project (`MinimalApiJwt`) with the `access_token` stored in the authentication cookie.
486486

487-
The `BlazorWebAppOidc.http` file can be used for testing the weather data request. Note that the `BlazorWebAppOidc` project must be running to test the endpoint, and the endpoint is hardcoded into the file. For more information, see <xref:test/http-files>.
488-
489487
### Configuration
490488

491489
This section explains how to configure the sample app.
@@ -551,7 +549,7 @@ The following <xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConn
551549
```
552550

553551
> [!NOTE]
554-
> When using Microsoft Entra ID, the `Weather.Get` scope is configured in the Azure or Entra portal under **Expose an API**.
552+
> When using Microsoft Entra ID, the `Weather.Get` scope is configured in the Azure or Entra portal in **Expose an API**.
555553
556554
Example:
557555

@@ -720,6 +718,8 @@ The sample app only provides a user name and email for display purposes. It does
720718

721719
The `MinimalApiJwt` project is a backend web API for multiple frontend projects. The project configures a [Minimal API](xref:fundamentals/minimal-apis) endpoint for weather data. Requests from the Blazor Web App server-side project (`BlazorWebAppOidc`) are proxied to the `MinimalApiJwt` project.
722720

721+
The `MinimalApiJwt.http` file can be used for testing the weather data request. Note that the `MinimalApiJwt` project must be running to test the endpoint, and the endpoint is hardcoded into the file. For more information, see <xref:test/http-files>.
722+
723723
### Configuration
724724

725725
Configure the project in the <xref:Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions> of the <xref:Microsoft.Extensions.DependencyInjection.JwtBearerExtensions.AddJwtBearer%2A> call in the project's `Program` file:

aspnetcore/zone-pivot-groups.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,11 @@ groups:
100100
title: Visual Studio Code
101101
- id: cli
102102
title: .NET CLI
103+
- id: blazor-web-app-entra-specification
104+
title: Specification
105+
prompt: Choose the app specification
106+
pivots:
107+
- id: without-bff-pattern
108+
title: Entra without BFF pattern
109+
- id: with-bff-pattern
110+
title: Entra with BFF pattern

0 commit comments

Comments
 (0)