| title | Secure an ASP.NET Core Blazor Web App with Microsoft Entra ID |
|---|---|
| ai-usage | ai-assisted |
| author | guardrex |
| description | Learn how to secure a Blazor Web App with Microsoft Entra ID. |
| monikerRange | >= aspnetcore-9.0 |
| ms.author | wpickett |
| ms.custom | mvc, sfi-ropc-nochange |
| ms.date | 12/17/2025 |
| uid | blazor/security/blazor-web-app-entra |
| zone_pivot_groups | blazor-web-app-entra-specification |
This article describes how to secure a Blazor Web App with Microsoft identity platform with Microsoft Identity Web packages for Microsoft Entra ID using a sample app.
:::zone pivot="with-yarp-and-aspire"
This version of the article covers implementing Entra with the Backend for Frontend (BFF) pattern with YARP and Aspire. Change the article version selector to Without YARP and Aspire if the app's specification doesn't call for adopting YARP and Aspire.
The following specification is covered:
- The Blazor Web App uses the Auto render mode with global interactivity (
InteractiveAuto). - 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.
- The app uses Microsoft Entra ID, based on Microsoft Identity Web packages.
- Automatic non-interactive token refresh is managed by the framework.
- The Backend for Frontend (BFF) pattern is adopted using Aspire for service discovery and YARP for proxying requests to a weather forecast endpoint on the backend app.
- A backend web API uses JWT-bearer authentication to validate JWT tokens saved by the Blazor Web App in the sign-in cookie.
- 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.
- YARP (Yet Another Reverse Proxy) is a library used to create a reverse proxy server.
- The app uses server-side and client-side service abstractions to display generated weather data.
- When rendering the
Weathercomponent on the server to display weather data, the component uses theServerWeatherForecaster. Microsoft Identity Web packages provide API to create a named downstream web service for making web API calls. xref:Microsoft.Identity.Abstractions.IDownstreamApi is injected into theServerWeatherForecaster, which is used to call xref:Microsoft.Identity.Abstractions.IDownstreamApi.CallApiForUserAsync%2A to obtain weather data from an external web API (MinimalApiJwtproject). - When the
Weathercomponent is rendered on the client, the component uses theClientWeatherForecasterservice implementation, which uses a preconfigured xref:System.Net.Http.HttpClient (in the client project'sProgramfile) to make a web API call to the server project's Minimal API (/weather-forecast) for weather data. The Minimal API endpoint obtains an access token for the user by calling xref:Microsoft.Identity.Web.ITokenAcquisition.GetAccessTokenForUserAsync%2A. Along with the correct scopes, a reverse proxy call is made to the external web API (MinimalApiJwtproject) to obtain and return weather data to the client for rendering by the component.
- When rendering the
Aspire requires Visual Studio version 17.10 or later.
Also, see the Prerequisites section of Quickstart: Build your first Aspire solution.
The sample solution consists of the following projects:
- Aspire:
Aspire.AppHost: Used to manage the high-level orchestration concerns of the app.Aspire.ServiceDefaults: Contains default Aspire app configurations that can be extended and customized as needed.
MinimalApiJwt: Backend web API, containing an example Minimal API endpoint for weather data.BlazorWebAppEntra: Server-side project of the Blazor Web App.BlazorWebAppEntra.Client: Client-side project of the Blazor Web App.
Access the sample through the latest version folder in the Blazor samples repository with the following link. The sample is in the BlazorWebAppEntraBffYarpAspire folder for .NET 9 or later.
Start the solution from the Aspire/Aspire.AppHost project.
View or download sample code (how to download)
We recommend using separate registrations for apps and web APIs, even when the apps and web APIs are in the same solution. The following guidance is for the BlazorWebAppEntra app and MinimalApiJwt web API of the sample solution, but the same guidance applies generally to any Entra-based registrations for apps and web APIs.
For app and web API registration guidance, see Register an application in Microsoft Entra ID.
Register the web API (MinimalApiJwt) first so that you can then grant access to the web API when registering the app. The web API's tenant ID and client ID are used to configure the web API in its Program file. After registering the web API, expose the web API in App registrations > Expose an API with a scope name of Weather.Get. Record the App ID URI for use in the app's configuration.
Next, register the app (BlazorWebAppEntra) with a Web platform configuration with two entries under Redirect URI: https://localhost/signin-oidc and https://localhost/signout-callback-oidc (ports aren't required on these URIs). Set the Front-channel logout URL to https://localhost/signout-callback-oidc (a port isn't required). The app's tenant ID, tenant domain, and client ID, along with the web API's base address, App ID URI, and weather scope name, are used to configure the app in its appsettings.json file. Grant API permission to access the web API in App registrations > API permissions. If the app's security specification calls for it, you can grant admin consent for the organization to access the web API. Authorized users and groups are assigned to the app's registration in App registrations > Enterprise applications.
In the Entra or Azure portal's Implicit grant and hybrid flows app registration configuration, don't select either checkbox for the authorization endpoint to return Access tokens or ID tokens. The OpenID Connect handler automatically requests the appropriate tokens using the code returned from the authorization endpoint.
Create a client secret in the app's registration in the Entra or Azure portal (Manage > Certificates & secrets > New client secret). Hold on to the client secret Value for use the next section.
Additional Entra configuration guidance for specific settings is provided later in this article.
For more information on using Aspire and details on the .AppHost and .ServiceDefaults projects of the sample app, see the Aspire documentation.
Confirm that you've met the prerequisites for Aspire. For more information, see the Prerequisites section of Quickstart: Build your first Aspire solution.
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 Aspire (Aspire documentation).
The BlazorWebAppEntra project is the server-side project of the Blazor Web App.
The BlazorWebAppEntra.Client project is the client-side project of the Blazor Web App.
If the user needs to log in or out during client-side rendering, a full page reload is initiated.
The MinimalApiJwt project is a backend web API for multiple frontend projects. The project configures a Minimal API endpoint for weather data. Requests from the Blazor Web App server-side project (BlazorWebAppEntra) are proxied to the MinimalApiJwt project.
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.
The project includes packages and configuration to produce OpenAPI documents.
A secure weather forecast data endpoint is in the project's Program file:
app.MapGet("/weather-forecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
}).RequireAuthorization();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 to the controller or action.
Configure the MinimalApiJwt 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.
For the web API app's registration, the Weather.Get scope is configured in the Entra or Azure portal in Expose an API.
xref:Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.Authority%2A sets the Authority for making OIDC calls.
jwtOptions.Authority = "{AUTHORITY}";The following examples use a Tenant ID of aaaabbbb-0000-cccc-1111-dddd2222eeee and a directory name of contoso.
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.
V1 STS token format:
jwtOptions.Authority = "https://sts.windows.net/aaaabbbb-0000-cccc-1111-dddd2222eeee";V2 STS token format:
jwtOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0";For more information on V2 STS tokens, see the STS token version section.
If the app is registered in a Microsoft Entra External ID tenant:
jwtOptions.Authority = "https://contoso.ciamlogin.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0";If the app is registered in an AAD B2C tenant:
jwtOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0";Note
Azure Active Directory B2C is no longer available as a service to new customers as of May 1, 2025. AAD B2C tenants are supported for customers with accounts established prior to May 1, 2025 until 2030. For more information, see Azure AD B2C: Frequently asked questions (FAQ).
xref:Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.Audience%2A sets the Audience for any received JWT access token.
jwtOptions.Audience = "{AUDIENCE}";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 Entra or Azure portal. Don't include the scope name, "Weather.Get," in the value.
The following examples use an Application (Client) Id of 11112222-bbbb-3333-cccc-4444dddd5555. The third example uses a tenant domain of contoso.onmicrosoft.com.
ME-ID tenant example:
jwtOptions.Audience = "api://11112222-bbbb-3333-cccc-4444dddd5555";Microsoft Entra External ID tenant:
jwtOptions.Audience = "11112222-bbbb-3333-cccc-4444dddd5555";AAD B2C tenant example:
jwtOptions.Audience = "https://contoso.onmicrosoft.com/11112222-bbbb-3333-cccc-4444dddd5555";xref:Microsoft.Identity.Web.AppBuilderExtension.AddMicrosoftIdentityWebApp%2A from Microsoft Identity Web (Microsoft.Identity.Web NuGet package, API documentation) is configured in the BlazorWebAppEntra project's Program file.
Obtain the application (client) ID, tenant (publisher) domain, and directory (tenant) ID from the app's registration in the Entra or Azure portal. The App ID URI is obtained for the Weather.Get scope from the web API's registration. Don't include the scope name when taking the App ID URI from the portal.
The authentication configuration depends on the type of tenant:
This section applies to an app registered in a Microsoft Entra ID or Azure AAD B2C tenant.
In the BlazorWebAppEntra project's Program file, provide the values for the following placeholders in Microsoft Identity Web configuration:
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(msIdentityOptions =>
{
msIdentityOptions.CallbackPath = "/signin-oidc";
msIdentityOptions.ClientId = "{CLIENT ID (BLAZOR APP)}";
msIdentityOptions.Domain = "{DIRECTORY NAME}.onmicrosoft.com";
msIdentityOptions.Instance = "https://login.microsoftonline.com/";
msIdentityOptions.ResponseType = "code";
msIdentityOptions.TenantId = "{TENANT ID}";
})
.EnableTokenAcquisitionToCallDownstreamApi()
.AddDownstreamApi("DownstreamApi", configOptions =>
{
configOptions.BaseUrl = "{BASE ADDRESS}";
configOptions.Scopes = ["{APP ID URI}/Weather.Get"];
})
.AddDistributedTokenCaches();Provide the same downstream API scope to the request transformer:
List<string> scopes = ["{APP ID URI}/Weather.Get"];Placeholders in the preceding configuration:
{CLIENT ID (BLAZOR APP)}: The application (client) ID.{DIRECTORY NAME}: The directory name of the tenant (publisher) domain.{TENANT ID}: The directory (tenant) ID.{BASE ADDRESS}: The web API's base address.{APP ID URI}: The App ID URI for web API scopes. Either of the following formats are used, where the{CLIENT ID (WEB API)}placeholder is the Client Id of the web API's Entra registration, and the{DIRECTORY NAME}placeholder is the directory name of the tenant (publishers) domain (example:contoso).- ME-ID tenant format:
api://{CLIENT ID (WEB API)} - B2C tenant format:
https://{DIRECTORY NAME}.onmicrosoft.com/{CLIENT ID (WEB API)}
- ME-ID tenant format:
Example:
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(msIdentityOptions =>
{
msIdentityOptions.CallbackPath = "/signin-oidc";
msIdentityOptions.ClientId = "00001111-aaaa-2222-bbbb-3333cccc4444";
msIdentityOptions.Domain = "contoso.onmicrosoft.com";
msIdentityOptions.Instance = "https://login.microsoftonline.com/";
msIdentityOptions.ResponseType = "code";
msIdentityOptions.TenantId = "aaaabbbb-0000-cccc-1111-dddd2222eeee";
})
.EnableTokenAcquisitionToCallDownstreamApi()
.AddDownstreamApi("DownstreamApi", configOptions =>
{
configOptions.BaseUrl = "https://localhost:7277";
configOptions.Scopes =
["api://11112222-bbbb-3333-cccc-4444dddd5555/Weather.Get"];
})
.AddDistributedTokenCaches();Example:
List<string> scopes = ["api://11112222-bbbb-3333-cccc-4444dddd5555/Weather.Get"];This section applies to an app registered in a Microsoft Entra External ID tenant.
In the BlazorWebAppEntra project's Program file, provide the values for the following placeholders in Microsoft Identity Web configuration:
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(msIdentityOptions =>
{
msIdentityOptions.CallbackPath = "/signin-oidc";
msIdentityOptions.Authority = "https://{DIRECTORY NAME}.ciamlogin.com/{TENANT ID}/v2.0";
msIdentityOptions.ClientId = "{CLIENT ID (BLAZOR APP)}";
msIdentityOptions.ResponseType = "code";
})
.EnableTokenAcquisitionToCallDownstreamApi()
.AddDownstreamApi("DownstreamApi", configOptions =>
{
configOptions.BaseUrl = "{BASE ADDRESS}";
configOptions.Scopes = ["{APP ID URI}/Weather.Get"];
})
.AddDistributedTokenCaches();Provide the same downstream API scope to the request transformer:
List<string> scopes = ["{APP ID URI}/Weather.Get"];Placeholders in the preceding configuration:
{DIRECTORY NAME}: The directory name of the tenant (publisher) domain.{CLIENT ID (BLAZOR APP)}: The application (client) ID.{BASE ADDRESS}: The web API's base address.{APP ID URI}: The App ID URI for web API scopes. Either of the following formats are used, where the{CLIENT ID (WEB API)}placeholder is the Client Id of the web API's Entra registration, and the{DIRECTORY NAME}placeholder is the directory name of the tenant (publishers) domain (example:contoso).- ME-ID or Microsoft Entra External ID tenant format:
api://{CLIENT ID (WEB API)} - B2C tenant format:
https://{DIRECTORY NAME}.onmicrosoft.com/{CLIENT ID (WEB API)}
- ME-ID or Microsoft Entra External ID tenant format:
Example:
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(msIdentityOptions =>
{
msIdentityOptions.CallbackPath = "/signin-oidc";
msIdentityOptions.Authority = "https://contoso.ciamlogin.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0";
msIdentityOptions.ClientId = "00001111-aaaa-2222-bbbb-3333cccc4444";
msIdentityOptions.ResponseType = "code";
})
.EnableTokenAcquisitionToCallDownstreamApi()
.AddDownstreamApi("DownstreamApi", configOptions =>
{
configOptions.BaseUrl = "https://localhost:7277";
configOptions.Scopes = ["api://11112222-bbbb-3333-cccc-4444dddd5555/Weather.Get"];
})
.AddDistributedTokenCaches();Example:
List<string> scopes = ["api://11112222-bbbb-3333-cccc-4444dddd5555/Weather.Get"];:::zone-end
:::zone pivot="without-yarp-and-aspire"
This version of the article covers implementing Entra with the Backend for Frontend (BFF) pattern without YARP and Aspire. Change the article version selector to With YARP and Aspire if the app's specification calls for adopting YARP and Aspire.
The following specification is covered:
- The Blazor Web App uses the Auto render mode with global interactivity (
InteractiveAuto). - 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.
- The app uses Microsoft Entra ID, based on Microsoft Identity Web packages.
- Automatic non-interactive token refresh is managed by the framework.
- The Backend for Frontend (BFF) pattern is adopted for proxying requests to a weather forecast endpoint on the backend app. A backend web API uses JWT-bearer authentication to validate JWT tokens saved by the Blazor Web App in the sign-in cookie.
- The app uses server-side and client-side service abstractions to display generated weather data:
- When rendering the
Weathercomponent on the server to display weather data, the component uses theServerWeatherForecaster. Microsoft Identity Web packages provide API to create a named downstream web service for making web API calls. xref:Microsoft.Identity.Abstractions.IDownstreamApi is injected into theServerWeatherForecaster, which is used to call xref:Microsoft.Identity.Abstractions.IDownstreamApi.CallApiForUserAsync%2A to obtain weather data from an external web API (MinimalApiJwtproject). - When the
Weathercomponent is rendered on the client, the component uses theClientWeatherForecasterservice implementation, which uses a preconfigured xref:System.Net.Http.HttpClient (in the client project'sProgramfile) to make a web API call to the server project's Minimal API (/weather-forecast) for weather data. The Minimal API endpoint obtains the weather data from theServerWeatherForecasterclass and returns it to the client for rendering by the component.
- When rendering the
The sample solution consists of the following projects:
BlazorWebAppEntra: Server-side project of the Blazor Web App, containing an example Minimal API endpoint for weather data.BlazorWebAppEntra.Client: Client-side project of the Blazor Web App.MinimalApiJwt: Backend web API, containing an example Minimal API endpoint for weather data.
Access the sample through the latest version folder in the Blazor samples repository with the following link. The sample is in the BlazorWebAppEntraBff folder for .NET 9 or later.
View or download sample code (how to download)
We recommend using separate registrations for apps and web APIs, even when the apps and web APIs are in the same solution. The following guidance is for the BlazorWebAppEntra app and MinimalApiJwt web API of the sample solution, but the same guidance applies generally to any Entra-based registrations for apps and web APIs.
For app and web API registration guidance, see Register an application in Microsoft Entra ID.
Register the web API (MinimalApiJwt) first so that you can then grant access to the web API when registering the app. The web API's tenant ID and client ID are used to configure the web API in its Program file. After registering the web API, expose the web API in App registrations > Expose an API with a scope name of Weather.Get. Record the App ID URI for use in the app's configuration.
Next, register the app (BlazorWebAppEntra) with a Web platform configuration with two entries under Redirect URI: https://localhost/signin-oidc and https://localhost/signout-callback-oidc (ports aren't required on these URIs). Set the Front-channel logout URL to https://localhost/signout-callback-oidc (a port isn't required). The app's tenant ID, tenant domain, and client ID, along with the web API's base address, App ID URI, and weather scope name, are used to configure the app in its appsettings.json file. Grant API permission to access the web API in App registrations > API permissions. If the app's security specification calls for it, you can grant admin consent for the organization to access the web API. Authorized users and groups are assigned to the app's registration in App registrations > Enterprise applications.
In the Entra or Azure portal's Implicit grant and hybrid flows app registration configuration, don't select either checkbox for the authorization endpoint to return Access tokens or ID tokens. The OpenID Connect handler automatically requests the appropriate tokens using the code returned from the authorization endpoint.
Create a client secret in the app's registration in the Entra or Azure portal (Manage > Certificates & secrets > New client secret). Hold on to the client secret Value for use the next section.
Additional Entra configuration guidance for specific settings is provided later in this article.
The BlazorWebAppEntra project is the server-side project of the Blazor Web App.
The BlazorWebAppEntra.Client project is the client-side project of the Blazor Web App.
If the user needs to log in or out during client-side rendering, a full page reload is initiated.
The MinimalApiJwt project is a backend web API for multiple frontend projects. The project configures a Minimal API endpoint for weather data.
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.
The project includes packages and configuration to produce OpenAPI documents.
A secure weather forecast data endpoint is in the project's Program file:
app.MapGet("/weather-forecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
}).RequireAuthorization();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 to the controller or action.
Configure the project in the xref:Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions of the xref:Microsoft.Extensions.DependencyInjection.JwtBearerExtensions.AddJwtBearer%2A call in the MinimalApiJwt project's Program file.
For the web API app's registration, the Weather.Get scope is configured in the Entra or Azure portal in Expose an API.
xref:Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.Authority%2A sets the Authority for making OIDC calls.
jwtOptions.Authority = "{AUTHORITY}";The following examples use a Tenant ID of aaaabbbb-0000-cccc-1111-dddd2222eeee and a directory name of contoso.
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.
V1 STS token format:
jwtOptions.Authority = "https://sts.windows.net/aaaabbbb-0000-cccc-1111-dddd2222eeee";V2 STS token format:
jwtOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0";For more information on V2 STS tokens, see the STS token version section.
If the app is registered in a Microsoft Entra External ID tenant:
jwtOptions.Authority = "https://contoso.ciamlogin.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0";If the app is registered in an AAD B2C tenant:
jwtOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0";Note
Azure Active Directory B2C is no longer available as a service to new customers as of May 1, 2025. AAD B2C tenants are supported for customers with accounts established prior to May 1, 2025 until 2030. For more information, see Azure AD B2C: Frequently asked questions (FAQ).
xref:Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.Audience%2A sets the Audience for any received JWT access token.
jwtOptions.Audience = "{AUDIENCE}";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 Entra or Azure portal. Don't include the scope name, "Weather.Get," in the value.
The following examples use an Application (Client) Id of 11112222-bbbb-3333-cccc-4444dddd5555. The third example uses a tenant domain of contoso.onmicrosoft.com.
ME-ID tenant example:
jwtOptions.Audience = "api://11112222-bbbb-3333-cccc-4444dddd5555";Microsoft Entra External ID tenant:
jwtOptions.Audience = "11112222-bbbb-3333-cccc-4444dddd5555";AAD B2C tenant example:
jwtOptions.Audience = "https://contoso.onmicrosoft.com/11112222-bbbb-3333-cccc-4444dddd5555";xref:Microsoft.Identity.Web.AppBuilderExtension.AddMicrosoftIdentityWebApp%2A from Microsoft Identity Web (Microsoft.Identity.Web NuGet package, API documentation) is configured in the BlazorWebAppEntra project's Program file.
Obtain the application (client) ID, tenant (publisher) domain, and directory (tenant) ID from the app's registration in the Entra or Azure portal. The App ID URI is obtained for the Weather.Get scope from the web API's registration. Don't include the scope name when taking the App ID URI from the portal.
The authentication configuration depends on the type of tenant:
This section applies to an app registered in a Microsoft Entra ID or Azure AAD B2C tenant.
In the BlazorWebAppEntra project's Program file, provide the values for the following placeholders in Microsoft Identity Web configuration:
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(msIdentityOptions =>
{
msIdentityOptions.CallbackPath = "/signin-oidc";
msIdentityOptions.ClientId = "{CLIENT ID (BLAZOR APP)}";
msIdentityOptions.Domain = "{DIRECTORY NAME}.onmicrosoft.com";
msIdentityOptions.Instance = "https://login.microsoftonline.com/";
msIdentityOptions.ResponseType = "code";
msIdentityOptions.TenantId = "{TENANT ID}";
})
.EnableTokenAcquisitionToCallDownstreamApi()
.AddDownstreamApi("DownstreamApi", configOptions =>
{
configOptions.BaseUrl = "{BASE ADDRESS}";
configOptions.Scopes = ["{APP ID URI}/Weather.Get"];
})
.AddDistributedTokenCaches();Placeholders in the preceding configuration:
{CLIENT ID (BLAZOR APP)}: The application (client) ID.{DIRECTORY NAME}: The directory name of the tenant (publisher) domain.{TENANT ID}: The directory (tenant) ID.{BASE ADDRESS}: The web API's base address.{APP ID URI}: The App ID URI for web API scopes. Either of the following formats are used, where the{CLIENT ID (WEB API)}placeholder is the Client Id of the web API's Entra registration, and the{DIRECTORY NAME}placeholder is the directory name of the tenant (publishers) domain (example:contoso).- ME-ID tenant format:
api://{CLIENT ID (WEB API)} - B2C tenant format:
https://{DIRECTORY NAME}.onmicrosoft.com/{CLIENT ID (WEB API)}
- ME-ID tenant format:
Example:
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(msIdentityOptions =>
{
msIdentityOptions.CallbackPath = "/signin-oidc";
msIdentityOptions.ClientId = "00001111-aaaa-2222-bbbb-3333cccc4444";
msIdentityOptions.Domain = "contoso.onmicrosoft.com";
msIdentityOptions.Instance = "https://login.microsoftonline.com/";
msIdentityOptions.ResponseType = "code";
msIdentityOptions.TenantId = "aaaabbbb-0000-cccc-1111-dddd2222eeee";
})
.EnableTokenAcquisitionToCallDownstreamApi()
.AddDownstreamApi("DownstreamApi", configOptions =>
{
configOptions.BaseUrl = "https://localhost:7277";
configOptions.Scopes = ["api://11112222-bbbb-3333-cccc-4444dddd5555/Weather.Get"];
})
.AddDistributedTokenCaches();This section applies to an app registered in a Microsoft Entra External ID tenant.
In the BlazorWebAppEntra project's Program file, provide the values for the following placeholders in Microsoft Identity Web configuration:
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(msIdentityOptions =>
{
msIdentityOptions.CallbackPath = "/signin-oidc";
msIdentityOptions.Authority = "https://{DIRECTORY NAME}.ciamlogin.com/{TENANT ID}/v2.0";
msIdentityOptions.ClientId = "{CLIENT ID (BLAZOR APP)}";
msIdentityOptions.ResponseType = "code";
})
.EnableTokenAcquisitionToCallDownstreamApi()
.AddDownstreamApi("DownstreamApi", configOptions =>
{
configOptions.BaseUrl = "{BASE ADDRESS}";
configOptions.Scopes = ["{APP ID URI}/Weather.Get"];
})
.AddDistributedTokenCaches();Placeholders in the preceding configuration:
{DIRECTORY NAME}: The directory name of the tenant (publisher) domain.{CLIENT ID (BLAZOR APP)}: The application (client) ID.{BASE ADDRESS}: The web API's base address.{APP ID URI}: The App ID URI for web API scopes. Either of the following formats are used, where the{CLIENT ID (WEB API)}placeholder is the Client Id of the web API's Entra registration, and the{DIRECTORY NAME}placeholder is the directory name of the tenant (publisher) domain (example:contoso).- ME-ID or Microsoft Entra External ID tenant format:
api://{CLIENT ID (WEB API)} - B2C tenant format:
https://{DIRECTORY NAME}.onmicrosoft.com/{CLIENT ID (WEB API)}
- ME-ID or Microsoft Entra External ID tenant format:
Example:
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(msIdentityOptions =>
{
msIdentityOptions.CallbackPath = "/signin-oidc";
msIdentityOptions.Authority = "https://contoso.ciamlogin.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0";
msIdentityOptions.ClientId = "00001111-aaaa-2222-bbbb-3333cccc4444";
msIdentityOptions.ResponseType = "code";
})
.EnableTokenAcquisitionToCallDownstreamApi()
.AddDownstreamApi("DownstreamApi", configOptions =>
{
configOptions.BaseUrl = "https://localhost:7277";
configOptions.Scopes = ["api://11112222-bbbb-3333-cccc-4444dddd5555/Weather.Get"];
})
.AddDistributedTokenCaches();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).
The xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.SignedOutCallbackPath%2A 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.
:::zone-end
Warning
Production apps should use a production distributed token cache provider. Otherwise, the app may have poor performance in some scenarios. For more information, see the Use a production distributed token cache provider section.
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).
The xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.SignedOutCallbackPath%2A 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.
This section only applies to the server project of the Blazor Web App.
Use either or both of the following approaches to supply the client secret to the app:
- Secret Manager tool: The Secret Manager tool stores private data on the local machine and is only used during local development.
- Azure Key Vault: You can store the client secret in a key vault for use in any environment, including for the
Developmentenvironment when working locally. Some developers prefer to use key vaults for staging and production deployments and use the Secret Manager tool for local development.
We strongly recommend that you avoid storing client secrets in project code or configuration files. Use secure authentication flows, such as either or both of the approaches in this section.
The Secret Manager tool can store the server app's client secret under the configuration key AzureAd:ClientSecret.
The Blazor server app hasn't been initialized for the Secret Manager tool. Use a command shell, such as the Developer PowerShell command shell in Visual Studio, to execute the following command. Before executing the command, change the directory with the cd command to the server project's directory. The command establishes a user secrets identifier (<UserSecretsId>) in the server app's project file, which is used internally by the tooling to track secrets for the app:
dotnet user-secrets init
Execute the following command to set the client secret. The {SECRET} placeholder is the client secret obtained from the app's Entra registration:
dotnet user-secrets set "AzureAd:ClientSecret" "{SECRET}"
If using Visual Studio, you can confirm that the secret is set by right-clicking the server project in Solution Explorer and selecting Manage User Secrets.
Azure Key Vault provides a safe approach for providing the app's client secret to the app.
To create a key vault and set a client secret, see About Azure Key Vault secrets (Azure documentation), which cross-links resources to get started with Azure Key Vault. To implement the code in this section, record the key vault URI and the secret name from Azure when you create the key vault and secret. For the example in this section, the secret name is "BlazorWebAppEntraClientSecret."
When establishing the key vault in the Entra or Azure portal:
-
Configure the key vault to use Azure role-based access control (RABC). If you aren't operating on an Azure Virtual Network, including for local development and testing, confirm that public access on the Networking step is enabled (checked). Enabling public access only exposes the key vault endpoint. Authenticated accounts are still required for access.
-
Create an Azure Managed Identity (or add a role to the existing Managed Identity that you plan to use) with the Key Vault Secrets User role. Assign the Managed Identity to the Azure App Service that's hosting the deployment: Settings > Identity > User assigned > Add.
[!NOTE] If you also plan to run an app locally with an authorized user for key vault access using the Azure CLI or Visual Studio's Azure Service Authentication, add your developer Azure user account in Access Control (IAM) with the Key Vault Secrets User role. If you want to use the Azure CLI through Visual Studio, execute the
az logincommand from the Developer PowerShell panel and follow the prompts to authenticate with the tenant.
To implement the code in this section, record the key vault URI (example: "https://contoso.vault.azure.net/", trailing slash required) and the secret name (example: "BlazorWebAppEntraClientSecret") from Azure when you create the key vault and secret.
Important
A key vault secret is created with an expiration date. Be sure to track when a key vault secret is going to expire and create a new secret for the app prior to that date passing.
Add the following AzureHelper class to the server project. The GetKeyVaultSecret method retrieves a secret from a key vault. Adjust the namespace (BlazorSample.Helpers) to match your project namespace scheme.
Helpers/AzureHelper.cs:
using Azure.Core;
using Azure.Security.KeyVault.Secrets;
namespace BlazorWebAppEntra.Helpers;
public static class AzureHelper
{
public static string GetKeyVaultSecret(string vaultUri,
TokenCredential credential, string secretName)
{
var client = new SecretClient(new Uri(vaultUri), credential);
var secret = client.GetSecretAsync(secretName).Result;
return secret.Value.Value;
}
}Note
The preceding example uses xref:Azure.Identity.DefaultAzureCredential to simplify authentication while developing apps that deploy to Azure by combining credentials used in Azure hosting environments with credentials used in local development. When moving to production, an alternative is a better choice, such as xref:Azure.Identity.ManagedIdentityCredential. For more information, see Authenticate Azure-hosted .NET apps to Azure resources using a system-assigned managed identity.
Where services are registered in the server project's Program file, obtain and apply the client secret using the following code:
TokenCredential? credential;
if (builder.Environment.IsProduction())
{
credential = new ManagedIdentityCredential("{MANAGED IDENTITY CLIENT ID}");
}
else
{
// Local development and testing only
DefaultAzureCredentialOptions options = new()
{
// Specify the tenant ID to use the dev credentials when running the app locally
// in Visual Studio.
VisualStudioTenantId = "{TENANT ID}",
SharedTokenCacheTenantId = "{TENANT ID}"
};
credential = new DefaultAzureCredential(options);
}Where xref:Microsoft.Identity.Web.MicrosoftIdentityOptions are set, call GetKeyVaultSecret to receive and assign the app's client secret:
msIdentityOptions.ClientSecret = AzureHelper.GetKeyVaultSecret("{VAULT URI}",
credential, "{SECRET NAME}");{MANAGED IDENTITY CLIENT ID}: The Azure Managed Identity Client ID (GUID).
{TENANT ID}: The directory (tenant) ID. Example: aaaabbbb-0000-cccc-1111-dddd2222eeee
{VAULT URI}: Key vault URI. Include the trailing slash on the URI. Example: https://contoso.vault.azure.net/
{SECRET NAME}: Secret name. Example: BlazorWebAppEntraClientSecret
Configuration is used to facilitate supplying dedicated key vaults and secret names based on the app's environmental configuration files. For example, you can supply different configuration values for appsettings.Development.json in development, appsettings.Staging.json when staging, and appsettings.Production.json for the production deployment. For more information, see xref:blazor/fundamentals/configuration.
In the Program file, all claims are serialized by setting xref:Microsoft.AspNetCore.Components.WebAssembly.Server.AuthenticationStateSerializationOptions.SerializeAllClaims%2A to true. If you only want the name and role claims serialized for CSR, remove the option or set it to false.
The sample solution projects configure Microsoft Identity Web and JWT bearer authentication in their Program files in order to make configuration settings discoverable using C# autocompletion. Professional apps usually use a configuration provider to configure OIDC options, such as the default JSON configuration provider. The JSON configuration provider loads configuration from app settings files appsettings.json/appsettings.{ENVIRONMENT}.json, where the {ENVIRONMENT} placeholder is the app's runtime environment. Follow the guidance in this section to use app settings files for configuration.
In the app settings file (appsettings.json) of the BlazorWebAppEntra project, add the following JSON configuration:
{
"AzureAd": {
"CallbackPath": "/signin-oidc",
"ClientId": "{CLIENT ID (BLAZOR APP)}",
"Domain": "{DIRECTORY NAME}.onmicrosoft.com",
"Instance": "https://login.microsoftonline.com/",
"ResponseType": "code",
"TenantId": "{TENANT ID}"
},
"DownstreamApi": {
"BaseUrl": "{BASE ADDRESS}",
"Scopes": ["{APP ID URI}/Weather.Get"]
}
}Update the placeholders in the preceding configuration to match the values that the app uses in the Program file:
{CLIENT ID (BLAZOR APP)}: The application (client) ID.{DIRECTORY NAME}: The directory name of the tenant (publisher) domain.{TENANT ID}: The directory (tenant) ID.{BASE ADDRESS}: The web API's base address.{APP ID URI}: The App ID URI for web API scopes. Either of the following formats are used, where the{CLIENT ID (WEB API)}placeholder is the Client Id of the web API's Entra registration, and the{DIRECTORY NAME}placeholder is the directory name of the tenant (publishers) domain (example:contoso).- ME-ID tenant format:
api://{CLIENT ID (WEB API)} - B2C tenant format:
https://{DIRECTORY NAME}.onmicrosoft.com/{CLIENT ID (WEB API)}
- ME-ID tenant format:
Example:
"AzureAd": {
"CallbackPath": "/signin-oidc",
"ClientId": "00001111-aaaa-2222-bbbb-3333cccc4444",
"Domain": "contoso.onmicrosoft.com",
"Instance": "https://login.microsoftonline.com/",
"ResponseType": "code",
"TenantId": "aaaabbbb-0000-cccc-1111-dddd2222eeee"
},
"DownstreamApi": {
"BaseUrl": "https://localhost:7277",
"Scopes": ["api://11112222-bbbb-3333-cccc-4444dddd5555/Weather.Get"]
}Update any other values in the preceding configuration to match custom/non-default values used in the Program file.
The configuration is automatically picked up by the authentication builder.
Make the following changes in the Program file:
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
- .AddMicrosoftIdentityWebApp(msIdentityOptions =>
- {
- msIdentityOptions.CallbackPath = "...";
- msIdentityOptions.ClientId = "...";
- msIdentityOptions.Domain = "...";
- msIdentityOptions.Instance = "...";
- msIdentityOptions.ResponseType = "...";
- msIdentityOptions.TenantId = "...";
- })
+ .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi()
- .AddDownstreamApi("DownstreamApi", configOptions =>
- {
- configOptions.BaseUrl = "...";
- configOptions.Scopes = ["..."];
- })
+ .AddDownstreamApi("DownstreamApi", builder.Configuration.GetSection("DownstreamApi"))
.AddDistributedTokenCaches();- List<string> scopes = ["{APP ID URI}/Weather.Get"];
- var accessToken = await tokenAcquisition.GetAccessTokenForUserAsync(scopes);
+ var configuration = transformContext.HttpContext.RequestServices.GetRequiredService<IConfiguration>();
+ var scopes = configuration.GetSection("DownstreamApi:Scopes").Get<IEnumerable<string>>();
+ var accessToken = await tokenAcquisition.GetAccessTokenForUserAsync(scopes ??
+ throw new InvalidOperationException("No downstream API scopes!"));Note
Production apps should use a production distributed token cache provider. Otherwise, the app may have poor performance in some scenarios. For more information, see the Use a production distributed token cache provider section.
In the MinimalApiJwt project, add the following app settings configuration to the appsettings.json file:
"Authentication": {
"Schemes": {
"Bearer": {
"Authority": "https://sts.windows.net/{TENANT ID (WEB API)}",
"ValidAudiences": ["{APP ID URI (WEB API)}"]
}
}
},The preceding example uses the V1 STS token URL format. For guidance on V2 STS tokens, see the STS token version section.
Update the placeholders in the preceding configuration to match the values that the app uses in the Program file:
{TENANT ID (WEB API)}: The Tenant Id of the web API.{APP ID URI (WEB API)}: The App ID URI of the web API.
Authority formats adopt the following patterns:
- ME-ID tenant type:
https://sts.windows.net/{TENANT ID} - Microsoft Entra External ID:
https://{DIRECTORY NAME}.ciamlogin.com/{TENANT ID}/v2.0 - B2C tenant type:
https://login.microsoftonline.com/{TENANT ID}/v2.0
The preceding example uses the V1 STS token URL format. For guidance on V2 STS tokens, see the STS token version section.
Audience formats adopt the following patterns ({CLIENT ID} is the Client Id of the web API; {DIRECTORY NAME} is the directory name, for example, contoso):
- ME-ID tenant type:
api://{CLIENT ID} - Microsoft Entra External ID:
{CLIENT ID} - B2C tenant type:
https://{DIRECTORY NAME}.onmicrosoft.com/{CLIENT ID}
The configuration is automatically picked up by the JWT bearer authentication builder.
Remove the following lines from the Program file:
- jwtOptions.Authority = "...";
- jwtOptions.Audience = "...";For more information on configuration, see the following resources:
- xref:fundamentals/configuration/index
- xref:blazor/fundamentals/configuration
In-memory distributed token caches are created when calling xref:Microsoft.Identity.Web.TokenCacheProviders.Distributed.DistributedTokenCacheAdapterExtension.AddDistributedTokenCaches%2A to ensure that there's a base implementation available for distributed token caching.
Production web apps and web APIs should use a production distributed token cache (for example: Redis, Microsoft SQL Server, Microsoft Azure Cosmos DB).
Note
For local development and testing on a single machine, you can use in-memory token caches instead of distributed token caches:
builder.Services.AddInMemoryTokenCaches();Later in the development and testing period, adopt a production distributed token cache provider.
xref:Microsoft.Extensions.DependencyInjection.MemoryCacheServiceCollectionExtensions.AddDistributedMemoryCache%2A adds a default implementation of xref:Microsoft.Extensions.Caching.Distributed.IDistributedCache that stores cache items in memory, which is used by Microsoft Identity Web for token caching.
The distributed token cache is configured by xref:Microsoft.Identity.Web.TokenCacheProviders.Distributed.MsalDistributedTokenCacheAdapterOptions:
- In development for debugging purposes, you can disable the L1 cache by setting xref:Microsoft.Identity.Web.TokenCacheProviders.Distributed.MsalDistributedTokenCacheAdapterOptions.DisableL1Cache%2A to
true. Be sure to reset it back tofalsefor production. - Set the maximum size of your L1 cache with
L1CacheOptions.SizeLimitto prevent the cache from overrunning the server's memory. The default value is 500 MB. - In development for debugging purposes, you can disable token encryption at rest by setting xref:Microsoft.Identity.Web.TokenCacheProviders.Distributed.MsalDistributedTokenCacheAdapterOptions.Encrypt%2A to
false, which is the default value. Be sure to reset it back totruefor production. - Set token eviction from the cache with xref:Microsoft.Extensions.Caching.Distributed.DistributedCacheEntryOptions.SlidingExpiration%2A. The default value is 1 hour.
- For more information, including guidance on the callback for L2 cache failures (xref:Microsoft.Identity.Web.TokenCacheProviders.Distributed.MsalDistributedTokenCacheAdapterOptions.OnL2CacheFailure%2A) and asynchronous L2 cache writes (xref:Microsoft.Identity.Web.TokenCacheProviders.Distributed.MsalDistributedTokenCacheAdapterOptions.EnableAsyncL2Write%2A), see xref:Microsoft.Identity.Web.TokenCacheProviders.Distributed.MsalDistributedTokenCacheAdapterOptions and Token cache serialization: Distributed token caches.
builder.Services.AddDistributedMemoryCache();
builder.Services.Configure<MsalDistributedTokenCacheAdapterOptions>(
options =>
{
// The following lines that are commented out reflect
// default values. We recommend overriding the default
// value of Encrypt to encrypt tokens at rest.
//options.DisableL1Cache = false;
//options.L1CacheOptions.SizeLimit = 500 * 1024 * 1024;
options.Encrypt = true;
//options.SlidingExpiration = TimeSpan.FromHours(1);
});xref:Microsoft.Extensions.DependencyInjection.MemoryCacheServiceCollectionExtensions.AddDistributedMemoryCache%2A requires a package reference to the Microsoft.Extensions.Caching.Memory NuGet package.
To configure a production distributed cache provider, see xref:performance/caching/distributed.
Warning
Always replace the in-memory distributed token caches with a real token cache provider when deploying the app to a production environment. If you fail to adopt a production distributed token cache provider, the app may suffer significantly degraded performance.
For more information, see Token cache serialization: Distributed caches. However, the code examples shown don't apply to ASP.NET Core apps, which configure distributed caches via xref:Microsoft.Extensions.DependencyInjection.MemoryCacheServiceCollectionExtensions.AddDistributedMemoryCache%2A, not xref:Microsoft.Identity.Web.TokenCacheExtensions.AddDistributedTokenCache%2A.
Use a shared Data Protection key ring in production so that instances of the app across servers in a web farm can decrypt tokens when xref:Microsoft.Identity.Web.TokenCacheProviders.Distributed.MsalDistributedTokenCacheAdapterOptions.Encrypt%2A?displayProperty=nameWithType is set to true.
Note
For early development and local testing on a single machine, you can set xref:Microsoft.Identity.Web.TokenCacheProviders.Distributed.MsalDistributedTokenCacheAdapterOptions.Encrypt%2A to false and configure a shared Data Protection key ring later:
options.Encrypt = false;Later in the development and testing period, enable token encryption and adopt a shared Data Protection key ring.
The following example shows how to use Azure Blob Storage and Azure Key Vault (PersistKeysToAzureBlobStorage/ProtectKeysWithAzureKeyVault) for the shared key ring. The service configurations are base case scenarios for demonstration purposes. Before deploying production apps, familiarize yourself with the Azure services and adopt best practices using the Azure services' dedicated documentation sets, which are linked at the end of this section.
Confirm the presence of the following packages in the server project of the Blazor Web App:
Note
Before proceeding with the following steps, confirm that the app is registered with Microsoft Entra.
The following code is typically implemented at the same time that a production distributed token cache provider is implemented. Other options, both within Azure and outside of Azure, are available for managing data protection keys across multiple app instances, but the sample app demonstrates how to use Azure services.
Configure Azure Blob Storage to maintain data protection keys. Follow the guidance in xref:security/data-protection/implementation/key-storage-providers#azure-storage.
Configure Azure Key Vault to encrypt the data protection keys at rest. Follow the guidance in xref:security/data-protection/configuration/overview#protect-keys-with-azure-key-vault-protectkeyswithazurekeyvault.
Use the following code in the Program file where services are registered:
TokenCredential? credential;
if (builder.Environment.IsProduction())
{
credential = new ManagedIdentityCredential("{MANAGED IDENTITY CLIENT ID}");
}
else
{
// Local development and testing only
DefaultAzureCredentialOptions options = new()
{
// Specify the tenant ID to use the dev credentials when running the app locally
// in Visual Studio.
VisualStudioTenantId = "{TENANT ID}",
SharedTokenCacheTenantId = "{TENANT ID}"
};
credential = new DefaultAzureCredential(options);
}
builder.Services.AddDataProtection()
.SetApplicationName("BlazorWebAppEntra")
.PersistKeysToAzureBlobStorage(new Uri("{BLOB URI}"), credential)
.ProtectKeysWithAzureKeyVault(new Uri("{KEY IDENTIFIER}"), credential);You can pass any app name to xref:Microsoft.AspNetCore.DataProtection.DataProtectionBuilderExtensions.SetApplicationName%2A. Just confirm that all app deployments use the same value.
{MANAGED IDENTITY CLIENT ID}: The Azure Managed Identity Client ID (GUID).
{TENANT ID}: Tenant ID.
{BLOB URI}: Full URI to the key file. The URI is generated by Azure Storage when you create the key file. Do not use a SAS.
{KEY IDENTIFIER}: Azure Key Vault key identifier used for key encryption. An access policy allows the application to access the key vault with Get, Unwrap Key, and Wrap Key permissions. The version of the key is obtained from the key in the Entra or Azure portal after it's created. If you enable autorotation of the key vault key, make sure that you use a versionless key identifier in the app's key vault configuration, where no key GUID is placed at the end of the identifier (example: https://contoso.vault.azure.net/keys/data-protection).
Note
In non-Production environments, the preceding example uses xref:Azure.Identity.DefaultAzureCredential to simplify authentication while developing apps that deploy to Azure by combining credentials used in Azure hosting environments with credentials used in local development. For more information, see Authenticate Azure-hosted .NET apps to Azure resources using a system-assigned managed identity.
Alternatively, you can configure the app to supply the values from app settings files using the JSON Configuration Provider. Add the following to the app settings file:
"DistributedTokenCache": {
"DisableL1Cache": false,
"L1CacheSizeLimit": 524288000,
"Encrypt": true,
"SlidingExpirationInHours": 1
},
"DataProtection": {
"BlobUri": "{BLOB URI}",
"KeyIdentifier": "{KEY IDENTIFIER}"
}Example DataProtection section:
"DataProtection": {
"BlobUri": "https://contoso.blob.core.windows.net/data-protection/keys.xml",
"KeyIdentifier": "https://contoso.vault.azure.net/keys/data-protection"
}Note
The key identifier in the preceding example is versionless. There's no GUID key version on the end of the identifier. This is particularly important if you opt to configure automatic key rotation for the key. For more information, see Configure cryptographic key auto-rotation in Azure Key Vault: Key rotation policy.
Make the following changes in the Program file:
builder.Services.Configure<MsalDistributedTokenCacheAdapterOptions>(
options =>
{
+ var config = builder.Configuration.GetSection("DistributedTokenCache");
- options.DisableL1Cache = false;
+ options.DisableL1Cache = config.GetValue<bool>("DisableL1Cache");
- options.L1CacheOptions.SizeLimit = 500 * 1024 * 1024;
+ options.L1CacheOptions.SizeLimit = config.GetValue<long>("L1CacheSizeLimit");
- options.Encrypt = true;
+ options.Encrypt = config.GetValue<bool>("Encrypt");
- options.SlidingExpiration = TimeSpan.FromHours(1);
+ options.SlidingExpiration =
+ TimeSpan.FromHours(config.GetValue<int>("SlidingExpirationInHours"));
});
- builder.Services.AddDataProtection()
- .SetApplicationName("BlazorWebAppEntra")
- .PersistKeysToAzureBlobStorage(new Uri("{BLOB URI}"), credential)
- .ProtectKeysWithAzureKeyVault(new Uri("{KEY IDENTIFIER}"), credential);Add the following code where services are configured in the Program file:
var config = builder.Configuration.GetSection("DataProtection");
builder.Services.AddDataProtection()
.SetApplicationName("BlazorWebAppEntra")
.PersistKeysToAzureBlobStorage(
new Uri(config.GetValue<string>("BlobUri") ??
throw new Exception("Missing Blob URI")),
credential)
.ProtectKeysWithAzureKeyVault(
new Uri(config.GetValue<string>("KeyIdentifier") ??
throw new Exception("Missing Key Identifier")),
credential);For more information on using a shared Data Protection key ring and key storage providers, see the following resources:
- xref:security/data-protection/implementation/key-storage-providers#azure-storage
- xref:security/data-protection/configuration/overview#protect-keys-with-azure-key-vault-protectkeyswithazurekeyvault
- Use the Azure SDK for .NET in ASP.NET Core apps
- Host ASP.NET Core in a web farm: Data Protection
- xref:security/data-protection/configuration/overview
- xref:security/data-protection/implementation/key-storage-providers
- Azure Key Vault documentation
- Azure Storage documentation
- Provide access to Key Vault keys, certificates, and secrets with Azure role-based access control
:::zone pivot="with-yarp-and-aspire"
The Blazor Web App server project's YARP forwarder, where the user's access token is attached to the MinimalApiJwt web API call, specifies a destination prefix of https://weatherapi. This value matches the project name passed to xref:Aspire.Hosting.ProjectResourceBuilderExtensions.AddProject%2A in the Program file of the Aspire.AppHost project.
Forwarder in the Blazor Web App server project (BlazorWebAppEntra):
app.MapForwarder("/weather-forecast", "https://weatherapi", transformBuilder =>
{
...
}).RequireAuthorization();Matching project name in the Program file of the Aspire App Host project (Aspire.AppHost):
var weatherApi = builder.AddProject<Projects.MinimalApiJwt>("weatherapi");There's no need to change the destination prefix of the YARP forwarder when deploying the Blazor Web App to production. The Microsoft Identity Web Downstream API package uses the base URI passed via configuration to make the web API call from the ServerWeatherForecaster, not the destination prefix of the YARP forwarder. In production, the YARP forwarder merely transforms the request, adding the user's access token.
:::zone-end
The LogInOrOut component (Layout/LogInOrOut.razor) sets a hidden field for the return URL (ReturnUrl) to the current URL (currentURL). When the user signs out of the app, the identity provider returns the user to the page from which they logged out. If the user logs out from a secure page, they're returned to the same secure page and sent back through the authentication process. This authentication flow is reasonable when users need to change accounts regularly.
Alternatively, use the following LogInOrOut component, which doesn't supply a return URL when logging out.
Layout/LogInOrOut.razor:
<div class="nav-item px-3">
<AuthorizeView>
<Authorized>
<form action="authentication/logout" method="post">
<AntiforgeryToken />
<button type="submit" class="nav-link">
<span class="bi bi-arrow-bar-left-nav-menu" aria-hidden="true">
</span> Logout
</button>
</form>
</Authorized>
<NotAuthorized>
<a class="nav-link" href="authentication/login">
<span class="bi bi-person-badge-nav-menu" aria-hidden="true"></span>
Login
</a>
</NotAuthorized>
</AuthorizeView>
</div>For more information on how this app secures its weather data, see Secure data in Blazor Web Apps with Interactive Auto rendering.
Server-side Blazor Web Apps hosted in a web farm or cluster of machines must adopt session affinity to maintain Blazor circuits for users of the app.
We also recommend using a shared Data Protection key ring in production, even when the app uses the Interactive WebAssembly render mode exclusively for client-side rendering (no Blazor circuits).
There are two types of token formats, named Version 1 (V1) and Version 2 (V2). In Azure's security token services (STS), the V1 format uses the sts.windows.net domain as the issuer, while the V2 format uses the login.microsoftonline.com domain as issuer. V2 supports additional features, such as authenticating personal accounts and OpenID protocols.
This article and its accompanying sample apps adopt V1 STS tokens. To adopt V2 tokens, make the following changes:
-
The STS version must be changed in the apps' registrations in the Azure portal. Set the value of
requestedAccessTokenVersionto2in the apps' manifests, both in the app's registration and the web API's (MinimalApiJwt) registration. -
Use the V2 authority URL format (example:
https://login.microsoftonline.com/{TENANT ID}/v2.0, where the{TENANT ID}placeholder is the tenant ID). -
In the web API (
MinimalApiJwt), explicitly validate the issuer:jwtOptions.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, // Ensure the issuer ends with /v2.0 if using the V2 endpoint ValidIssuer = "https://login.microsoftonline.com/{TENANT ID}/v2.0", ValidateAudience = true, ValidAudience = "{WEB API CLIENT ID}", ValidateLifetime = true };
The
{WEB API CLIENT ID}placeholder in the preceding example is only the client ID, not the full value passed to theAudienceproperty.
For more information, see Access tokens in the Microsoft identity platform: Token formats.
- Call a web API from an ASP.NET Core Blazor app: Microsoft identity platform for web API calls
- Microsoft identity platform documentation
- Web API documentation | Microsoft identity platform
- A web API that calls web APIs: Call an API: Option 2: Call a downstream web API with the helper class
AzureAD/microsoft-identity-webGitHub repository: Helpful guidance on implementing Microsoft Identity Web for Microsoft Entra ID for ASP.NET Core apps, including links to sample apps and related Azure documentation. Currently, Blazor Web Apps aren't explicitly addressed by the Azure documentation, but the setup and configuration of a Blazor Web App for ME-ID and Azure hosting is the same as it is for any ASP.NET Core web app.AuthenticationStateProviderservice- Manage authentication state in Blazor Web Apps
- Service abstractions in Blazor Web Apps
- Data Protection resources
- xref:security/data-protection/configuration/overview
- xref:security/data-protection/implementation/key-storage-providers
- xref:security/data-protection/implementation/key-encryption-at-rest