| title | Secure an ASP.NET Core Blazor Web App with OpenID Connect (OIDC) |
|---|---|
| author | guardrex |
| description | Learn how to secure a Blazor Web App with OpenID Connect (OIDC). |
| monikerRange | >= aspnetcore-8.0 |
| ms.author | wpickett |
| ms.custom | mvc |
| ms.date | 12/18/2025 |
| uid | blazor/security/blazor-web-app-oidc |
| zone_pivot_groups | blazor-web-app-oidc-specification |
This article describes how to secure a Blazor Web App with OpenID Connect (OIDC) using a sample app in the dotnet/blazor-samples GitHub repository (.NET 8 or later) (how to download).
:::zone pivot="with-yarp-and-aspire"
:::moniker range=">= aspnetcore-9.0"
For Microsoft Entra ID or Azure AD B2C, you can use xref:Microsoft.Identity.Web.AppBuilderExtension.AddMicrosoftIdentityWebApp%2A from Microsoft Identity Web (Microsoft.Identity.Web NuGet package, API documentation), which adds both the OIDC and Cookie authentication handlers with the appropriate defaults. The sample app and the guidance in this article don't use Microsoft Identity Web. The guidance demonstrates how to configure the OIDC handler manually for any OIDC provider. For more information on implementing Microsoft Identity Web, see xref:blazor/security/blazor-web-app-entra.
:::moniker-end
This version of the article covers implementing OIDC with the Backend for Frontend (BFF) pattern with YARP and Aspire. Change the article version selector to either Without YARP and Aspire (Interactive Auto) (Interactive Auto rendering) or Without YARP and Aspire (Interactive Server) (Interactive Server rendering) if the app's specification doesn't call for adopting YARP and Aspire.
Aspire requires Visual Studio version 17.10 or later.
Also, see the Prerequisites section of Quickstart: Build your first Aspire solution.
The sample app 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.BlazorWebAppOidc: Server-side project of the Blazor Web App. The project uses YARP to proxy requests to a weather forecast endpoint in the backend web API project (MinimalApiJwt) with theaccess_tokenstored in the authentication cookie.BlazorWebAppOidc.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 BlazorWebAppOidcBffAutoYarpAspire folder for .NET 8 or later.
Start the solution from the Aspire/Aspire.AppHost project.
View or download sample code (how to download)
The Blazor Web App uses the Auto render mode with global interactivity.
:::moniker range=">= aspnetcore-9.0"
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.
:::moniker-end
:::moniker range="< aspnetcore-9.0"
The PersistingAuthenticationStateProvider class (PersistingAuthenticationStateProvider.cs) is a server-side xref:Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider that uses xref:Microsoft.AspNetCore.Components.PersistentComponentState to flow the authentication state to the client, which is then fixed for the lifetime of the WebAssembly application.
:::moniker-end
This app is a starting point for any OIDC authentication flow. OIDC is configured manually in the app and doesn't rely upon Microsoft Entra ID or Microsoft Identity Web packages, nor does the sample app require Microsoft Azure hosting. However, the sample app can be used with Entra, Microsoft Identity Web, and hosted in Azure.
Automatic non-interactive token refresh with the help of a custom cookie refresher (CookieOidcRefresher.cs).
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.
The backend web API (MinimalApiJwt) 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. MapForwarder in the Program file of the server project adds direct forwarding of HTTP requests that match the specified pattern to a specific destination using default configuration for the outgoing request, customized transforms, and default HTTP client:
- When rendering the
Weathercomponent on the server, the component uses theServerWeatherForecasterclass to proxy the request for weather data with the user's access token. xref:Microsoft.AspNetCore.Http.IHttpContextAccessor.HttpContext?displayProperty=nameWithType determines if an xref:Microsoft.AspNetCore.Http.HttpContext is available for use by theGetWeatherForecastAsyncmethod. For more information, see xref:blazor/components/index#ihttpcontextaccessorhttpcontext. - When the component is rendered on the client, the component uses the
ClientWeatherForecasterservice 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. A Minimal API endpoint (/weather-forecast) defined in the server project'sProgramfile transforms the request with the user's access token to obtain the weather data.
For more information on (web) API calls using a service abstractions in Blazor Web Apps, see xref:blazor/call-web-api#service-abstractions-for-web-api-calls.
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 BlazorWebAppOidc 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 (BlazorWebAppOidc/BlazorWebAppOidc.Client) with a Web platform configuration and a Redirect URI of https://localhost/signin-oidc (a port isn't required). The app's tenant ID 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 Program 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.
This section only applies to the server project of the Blazor Web App (BlazorWebAppOidc project).
For local development testing, use the Secret Manager tool to store the Blazor server project's client secret under the configuration key Authentication:Schemes:MicrosoftOidc:ClientSecret.
The Blazor server project 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):
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 registration:
dotnet user-secrets set "Authentication:Schemes:MicrosoftOidc:ClientSecret" "{SECRET}"
If using Visual Studio, you can confirm the secret is set by right-clicking the server project in Solution Explorer and selecting Manage User Secrets.
For more information 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 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 (BlazorWebAppOidc) 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.
:::moniker range=">= aspnetcore-9.0"
The project includes packages and configuration to produce OpenAPI documents.
:::moniker-end
:::moniker range="< aspnetcore-9.0"
The project includes packages and configuration to produce OpenAPI documents and the Swagger UI in the Development environment. For more information, see xref:fundamentals/openapi/using-openapi-documents#use-swagger-ui-for-local-ad-hoc-testing.
:::moniker-end
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 project's Program file.
The xref:Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.Authority%2A sets the Authority for making OIDC calls. We recommend using a separate app registration for the MinimalApiJwt project. The authority matches the issurer (iss) of the JWT returned by the identity provider.
jwtOptions.Authority = "{AUTHORITY}";The format of the Authority depends on the type of tenant in use. The following examples for Microsoft Entra ID use a Tenant ID of aaaabbbb-0000-cccc-1111-dddd2222eeee.
ME-ID tenant Authority example:
jwtOptions.Authority = "https://sts.windows.net/aaaabbbb-0000-cccc-1111-dddd2222eeee";AAD B2C tenant Authority example:
jwtOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0";The xref:Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.Audience%2A sets the Audience for any received OIDC token.
jwtOptions.Audience = "{APP ID URI}";Note
When using Microsoft Entra ID, 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 format of the Audience depends on the type of tenant in use. The following examples for Microsoft Entra ID use a Tenant ID of contoso and a Client ID of 11112222-bbbb-3333-cccc-4444dddd5555.
ME-ID tenant App ID URI example:
jwtOptions.Audience = "api://11112222-bbbb-3333-cccc-4444dddd5555";AAD B2C tenant App ID URI example:
jwtOptions.Audience = "https://contoso.onmicrosoft.com/11112222-bbbb-3333-cccc-4444dddd5555";This section explains how to configure the server-side Blazor project.
The following xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions configuration is found in the project's Program file on the call to xref:Microsoft.Extensions.DependencyInjection.OpenIdConnectExtensions.AddOpenIdConnect%2A.
:::moniker range=">= aspnetcore-9.0"
xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.PushedAuthorizationBehavior%2A: Controls Pushed Authorization Requests (PAR) support. By default, the setting is to use PAR if the identity provider's discovery document (usually found at .well-known/openid-configuration) advertises support for PAR. If you wish to require PAR support for the app, you can assign a value of PushedAuthorizationBehavior.Require. PAR isn't supported by Microsoft Entra, and there are no plans for Entra to ever support it in the future.
oidcOptions.PushedAuthorizationBehavior = PushedAuthorizationBehavior.UseIfAvailable;:::moniker-end
xref:Microsoft.AspNetCore.Builder.RemoteAuthenticationOptions.SignInScheme%2A: Sets the authentication scheme corresponding to the middleware responsible of persisting user's identity after a successful authentication. The OIDC handler needs to use a sign-in scheme that's capable of persisting user credentials across requests. The following line is present merely for demonstration purposes. If omitted, xref:Microsoft.AspNetCore.Authentication.AuthenticationOptions.DefaultSignInScheme%2A is used as a fallback value.
oidcOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;Scopes for openid and profile (xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.Scope%2A) (Optional): The openid and profile scopes are also configured by default because they're required for the OIDC handler to work, but these may need to be re-added if scopes are included in the Authentication:Schemes:MicrosoftOidc:Scope configuration. For general configuration guidance, see xref:fundamentals/configuration/index and xref:blazor/fundamentals/configuration.
oidcOptions.Scope.Add(OpenIdConnectScope.OpenIdProfile);xref:Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions.SaveTokens%2A: Defines whether access and refresh tokens should be stored in the xref:Microsoft.AspNetCore.Authentication.AuthenticationProperties after a successful authorization. This property is set to true so the refresh token gets stored for non-interactive token refresh.
oidcOptions.SaveTokens = true;Scope for offline access (xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.Scope%2A): The offline_access scope is required for the refresh token.
oidcOptions.Scope.Add(OpenIdConnectScope.OfflineAccess);Scopes for obtaining weather data from the web API (xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.Scope%2A): Configure the Weather.Get scope for accessing the external web API for weather data. In the following example, the {APP ID URI} placeholder is found in the Entra or Azure portal where the web API is exposed. For any other identity provider, use the appropriate scope.
oidcOptions.Scope.Add("{APP ID URI}/Weather.Get");The format of the scope depends on the type of tenant in use. In the following examples, the Tenant Domain is contoso.onmicrosoft.com, and the Client ID is 11112222-bbbb-3333-cccc-4444dddd5555.
ME-ID tenant App ID URI example:
oidcOptions.Scope.Add("api://11112222-bbbb-3333-cccc-4444dddd5555/Weather.Get");AAD B2C tenant App ID URI example:
oidcOptions.Scope.Add("https://contoso.onmicrosoft.com/11112222-bbbb-3333-cccc-4444dddd5555/Weather.Get");xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.Authority%2A and xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.ClientId%2A: Sets the Authority and Client ID for OIDC calls.
oidcOptions.Authority = "{AUTHORITY}";
oidcOptions.ClientId = "{CLIENT ID}";The following example uses a Tenant ID of aaaabbbb-0000-cccc-1111-dddd2222eeee and a Client ID of 00001111-aaaa-2222-bbbb-3333cccc4444:
oidcOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0";
oidcOptions.ClientId = "00001111-aaaa-2222-bbbb-3333cccc4444";For multi-tenant apps, the "common" authority should be used. You can also use the "common" authority for single-tenant apps, but a custom xref:Microsoft.IdentityModel.Tokens.TokenValidationParameters.IssuerValidator%2A is required, as shown later in this section.
oidcOptions.Authority = "https://login.microsoftonline.com/common/v2.0";xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.ResponseType%2A: Configures the OIDC handler to only perform authorization code flow. Implicit grants and hybrid flows are unnecessary in this mode. The OIDC handler automatically requests the appropriate tokens using the code returned from the authorization endpoint.
oidcOptions.ResponseType = OpenIdConnectResponseType.Code;xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.MapInboundClaims%2A and configuration of xref:Microsoft.IdentityModel.Tokens.TokenValidationParameters.NameClaimType%2A and xref:Microsoft.IdentityModel.Tokens.TokenValidationParameters.RoleClaimType%2A: Many OIDC servers use "name" and "role" rather than the SOAP/WS-Fed defaults in xref:System.Security.Claims.ClaimTypes. When xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.MapInboundClaims%2A is set to false, the handler doesn't perform claims mappings, and the claim names from the JWT are used directly by the app. The following example sets the role claim type to "roles," which is appropriate for Microsoft Entra ID (ME-ID). Consult your identity provider's documentation for more information.
Note
xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.MapInboundClaims%2A must be set to false for most OIDC providers, which prevents renaming claims.
oidcOptions.MapInboundClaims = false;
oidcOptions.TokenValidationParameters.NameClaimType = "name";
oidcOptions.TokenValidationParameters.RoleClaimType = "roles";Path configuration: Paths must match the redirect URI (login callback path) and post logout redirect (signed-out callback path) paths configured when registering the application with the OIDC provider. In the Azure portal, paths are configured in the Authentication blade of the app's registration. Both the sign-in and sign-out paths must be registered as redirect URIs. The default values are /signin-oidc and /signout-callback-oidc.
Configure the signed-out callback path in the app's OIDC provider registration. In the following example, the {PORT} placeholder is the app's port:
:::no-loc text="https://localhost:{PORT}/signin-oidc":::
Note
A port isn't required for localhost addresses when using Microsoft Entra ID. Most other OIDC providers require the correct port.
xref:Microsoft.AspNetCore.Builder.OpenIdConnectOptions.SignedOutCallbackPath%2A (configuration key: "SignedOutCallbackPath"): The request path within the app's base path intercepted by the OIDC handler where the user agent is first returned after signing out from the identity provider. 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 OIDC handler redirects to the xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.SignedOutRedirectUri%2A or xref:Microsoft.AspNetCore.Authentication.AuthenticationProperties.RedirectUri%2A, if specified.
Configure the signed-out callback path in the app's OIDC provider registration. In the following example, the {PORT} placeholder is the app's port:
:::no-loc text="https://localhost:{PORT}/signout-callback-oidc":::
Note
When using Microsoft Entra ID, set the path in the Web platform configuration's Redirect URI entries in the Entra or Azure portal. A port isn't required for localhost addresses when using Entra. Most other OIDC providers require the correct port. If you don't add the signed-out callback path URI to the app's registration in Entra, Entra refuses to redirect the user back to the app and merely asks them to close their browser window.
xref:Microsoft.AspNetCore.Builder.OpenIdConnectOptions.RemoteSignOutPath%2A: Requests received on this path cause the handler to invoke sign-out using the sign-out scheme.
In the following example, the {PORT} placeholder is the app's port:
:::no-loc text="https://localhost/signout-oidc":::
Note
When using Microsoft Entra ID, set the Front-channel logout URL in the Entra or Azure portal. A port isn't required for localhost addresses when using Entra. Most other OIDC providers require the correct port.
oidcOptions.CallbackPath = new PathString("{PATH}");
oidcOptions.SignedOutCallbackPath = new PathString("{PATH}");
oidcOptions.RemoteSignOutPath = new PathString("{PATH}");Examples (default values):
oidcOptions.CallbackPath = new PathString("/signin-oidc");
oidcOptions.SignedOutCallbackPath = new PathString("/signout-callback-oidc");
oidcOptions.RemoteSignOutPath = new PathString("/signout-oidc");(Microsoft Azure only with the "common" endpoint) xref:Microsoft.IdentityModel.Tokens.TokenValidationParameters.IssuerValidator%2A?displayProperty=nameWithType: Many OIDC providers work with the default issuer validator, but we need to account for the issuer parameterized with the Tenant ID ({TENANT ID}) returned by https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration. For more information, see SecurityTokenInvalidIssuerException with OpenID Connect and the Azure AD "common" endpoint (AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet #1731).
Only for apps using Microsoft Entra ID or Azure AD B2C with the "common" endpoint:
var microsoftIssuerValidator = AadIssuerValidator.GetAadIssuerValidator(oidcOptions.Authority);
oidcOptions.TokenValidationParameters.IssuerValidator = microsoftIssuerValidator.Validate;The BlazorWebAppOidc.Client project is the client-side project of the Blazor Web App.
:::moniker range=">= aspnetcore-9.0"
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.
:::moniker-end
:::moniker range="< aspnetcore-9.0"
The PersistentAuthenticationStateProvider class (PersistentAuthenticationStateProvider.cs) is a client-side xref:Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider that determines the user's authentication state by looking for data persisted in the page when it was rendered on the server. The authentication state is fixed for the lifetime of the WebAssembly application.
:::moniker-end
If the user needs to log in or out, a full page reload is required.
The sample app only provides a user name and email for display purposes.
:::zone-end
:::zone pivot="without-yarp-and-aspire"
:::moniker range=">= aspnetcore-9.0"
For Microsoft Entra ID or Azure AD B2C, you can use xref:Microsoft.Identity.Web.AppBuilderExtension.AddMicrosoftIdentityWebApp%2A from Microsoft Identity Web (Microsoft.Identity.Web NuGet package, API documentation), which adds both the OIDC and Cookie authentication handlers with the appropriate defaults. The sample app and the guidance in this article don't use Microsoft Identity Web. The guidance demonstrates how to configure the OIDC handler manually for any OIDC provider. For more information on implementing Microsoft Identity Web, see xref:blazor/security/blazor-web-app-entra.
:::moniker-end
This version of the article covers implementing OIDC with the Backend for Frontend (BFF) pattern without YARP and Aspire. Change the article version selector to With YARP and Aspire (Interactive Auto) if the app's specification calls for adopting YARP and Aspire.
The following specification is adopted:
- The Blazor Web App uses the Auto render mode with global interactivity.
- Custom auth state provider services are used by the server and client apps to capture the user's authentication state and flow it between the server and client.
- This app is a starting point for any OIDC authentication flow. OIDC is configured manually in the app and doesn't rely upon Microsoft Entra ID or Microsoft Identity Web packages, nor does the sample app require Microsoft Azure hosting. However, the sample app can be used with Entra, Microsoft Identity Web, and hosted in Azure.
- Automatic non-interactive token refresh.
- A separate web API project demonstrates a secure web API call for weather data.
For an alternative experience using Microsoft Authentication Library for .NET, Microsoft Identity Web, and Microsoft Entra ID, see xref:blazor/security/blazor-web-app-entra.
The sample app consists of the following projects:
BlazorWebAppOidc: Server-side project of the Blazor Web App, containing an example Minimal API endpoint for weather data.BlazorWebAppOidc.Client: Client-side project of the Blazor Web App.MinimalApiJwt: Backend web API with a 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 BlazorWebAppOidcBffAuto folder for .NET 8 or later.
View or download sample code (how to download)
Sample solution features:
-
Automatic non-interactive token refresh with the help of a custom cookie refresher (
CookieOidcRefresher.cs). -
Weather data is handled by a Minimal API endpoint (
/weather-forecast) in theProgramfile (Program.cs) of theMinimalApiJwtproject. The endpoint requires authorization by calling xref:Microsoft.AspNetCore.Builder.AuthorizationEndpointConventionBuilderExtensions.RequireAuthorization%2A. For any controllers that you add to the project, add the[Authorize]attribute to the controller or action. For more information on requiring authorization across the app via an authorization policy and opting out of authorization at a subset of public endpoints, see the Razor Pages OIDC guidance. -
The app securely calls a web API for weather data:
- When rendering the
Weathercomponent on the server, the component uses theServerWeatherForecasteron the server to obtain weather data from the web API in theMinimalApiJwtproject using a xref:System.Net.Http.DelegatingHandler (TokenHandler) that attaches the access token from the xref:Microsoft.AspNetCore.Http.HttpContext to the request. - When the component is rendered on the client, the component uses the
ClientWeatherForecasterservice implementation, which uses a preconfigured xref:System.Net.Http.HttpClient (in the client project'sProgramfile) to make the web API call from the server project'sServerWeatherForecaster.
- When rendering the
:::moniker range=">= aspnetcore-9.0"
- 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.
:::moniker-end
:::moniker range="< aspnetcore-9.0"
- The
PersistingAuthenticationStateProviderclass (PersistingAuthenticationStateProvider.cs) is a server-side xref:Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider that uses xref:Microsoft.AspNetCore.Components.PersistentComponentState to flow the authentication state to the client, which is then fixed for the lifetime of the WebAssembly application.
:::moniker-end
For more information on (web) API calls using a service abstractions in Blazor Web Apps, see xref:blazor/call-web-api#service-abstractions-for-web-api-calls.
Although you aren't required to adopt Microsoft Entra (ME-ID) as the OIDC provider to use the sample app and the guidance in this article, this article describes settings for ME-ID using names that are found in Microsoft documentation and the Azure/Entra portals. OIDC settings have similar naming across OIDC providers. When using a third-party OIDC provider, use the provider's documentation in conjunction with the guidance in this article for app and web API registrations.
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 BlazorWebAppOidc 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 (BlazorWebAppOidc/BlazorWebAppOidc.Client) with a Web platform configuration and a Redirect URI of https://localhost/signin-oidc (a port isn't required). The app's tenant ID 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 Program 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.
This section only applies to the server project of the Blazor Web App (BlazorWebAppOidc project).
For local development testing, use the Secret Manager tool to store the Blazor server project's client secret under the configuration key Authentication:Schemes:MicrosoftOidc:ClientSecret.
The Blazor server project 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):
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 registration:
dotnet user-secrets set "Authentication:Schemes:MicrosoftOidc:ClientSecret" "{SECRET}"
If using Visual Studio, you can confirm the secret is set by right-clicking the server project in Solution Explorer and selecting Manage User Secrets.
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.
:::moniker range=">= aspnetcore-9.0"
The project includes packages and configuration to produce OpenAPI documents.
:::moniker-end
:::moniker range="< aspnetcore-9.0"
The project includes packages and configuration to produce OpenAPI documents and the Swagger UI in the Development environment. For more information, see xref:fundamentals/openapi/using-openapi-documents#use-swagger-ui-for-local-ad-hoc-testing.
:::moniker-end
The project creates a Minimal API endpoint for weather data:
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();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.
The xref:Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.Authority%2A sets the Authority for making OIDC calls. We recommend using a separate app registration for the MinimalApiJwt project. The authority matches the issurer (iss) of the JWT returned by the identity provider.
jwtOptions.Authority = "{AUTHORITY}";The format of the Authority depends on the type of tenant in use. The following examples for Microsoft Entra ID use a Tenant ID of aaaabbbb-0000-cccc-1111-dddd2222eeee.
ME-ID tenant Authority example:
jwtOptions.Authority = "https://sts.windows.net/aaaabbbb-0000-cccc-1111-dddd2222eeee";AAD B2C tenant Authority example:
jwtOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0";The xref:Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.Audience%2A sets the Audience for any received OIDC token.
jwtOptions.Audience = "{APP ID URI}";Note
When using Microsoft Entra ID, 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 format of the Audience depends on the type of tenant in use. The following examples for Microsoft Entra ID use a Tenant ID of contoso and a Client ID of 11112222-bbbb-3333-cccc-4444dddd5555.
ME-ID tenant App ID URI example:
jwtOptions.Audience = "api://11112222-bbbb-3333-cccc-4444dddd5555";AAD B2C tenant App ID URI example:
jwtOptions.Audience = "https://contoso.onmicrosoft.com/11112222-bbbb-3333-cccc-4444dddd5555";The BlazorWebAppOidc project is the server-side project of the Blazor Web App.
A xref:System.Net.Http.DelegatingHandler (TokenHandler) manages attaching a user's access token to outgoing requests. The token handler only executes during static server-side rendering (static SSR), so using xref:Microsoft.AspNetCore.Http.HttpContext is safe in this scenario. For more information, see xref:blazor/components/httpcontext and xref:blazor/security/additional-scenarios#use-a-token-handler-for-web-api-calls.
TokenHandler.cs:
public class TokenHandler(IHttpContextAccessor httpContextAccessor) :
DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
if (httpContextAccessor.HttpContext is null)
{
throw new Exception("HttpContext not available");
}
var accessToken = await httpContextAccessor.HttpContext
.GetTokenAsync("access_token");
request.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);
return await base.SendAsync(request, cancellationToken);
}
}In the project's Program file, the token handler (TokenHandler) is registered as a service and specified as the message handler with xref:Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler%2A for making secure requests to the backend MinimalApiJwt web API using a named HTTP client ("ExternalApi").
builder.Services.AddScoped<TokenHandler>();
builder.Services.AddHttpClient("ExternalApi",
client => client.BaseAddress = new Uri(builder.Configuration["ExternalApiUri"] ??
throw new Exception("Missing base address!")))
.AddHttpMessageHandler<TokenHandler>();In the project's appsettings.json file, configure the external API URI:
"ExternalApiUri": "{BASE ADDRESS}"Example:
"ExternalApiUri": "https://localhost:7277"The following xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions configuration is found in the project's Program file on the call to xref:Microsoft.Extensions.DependencyInjection.OpenIdConnectExtensions.AddOpenIdConnect%2A:
:::moniker range=">= aspnetcore-9.0"
xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.PushedAuthorizationBehavior%2A: Controls Pushed Authorization Requests (PAR) support. By default, the setting is to use PAR if the identity provider's discovery document (usually found at .well-known/openid-configuration) advertises support for PAR. If you wish to require PAR support for the app, you can assign a value of PushedAuthorizationBehavior.Require. PAR isn't supported by Microsoft Entra, and there are no plans for Entra to ever support it in the future.
oidcOptions.PushedAuthorizationBehavior = PushedAuthorizationBehavior.UseIfAvailable;:::moniker-end
xref:Microsoft.AspNetCore.Builder.RemoteAuthenticationOptions.SignInScheme%2A: Sets the authentication scheme corresponding to the middleware responsible of persisting user's identity after a successful authentication. The OIDC handler needs to use a sign-in scheme that's capable of persisting user credentials across requests. The following line is present merely for demonstration purposes. If omitted, xref:Microsoft.AspNetCore.Authentication.AuthenticationOptions.DefaultSignInScheme%2A is used as a fallback value.
oidcOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;Scopes for openid and profile (xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.Scope%2A) (Optional): The openid and profile scopes are also configured by default because they're required for the OIDC handler to work, but these may need to be re-added if scopes are included in the Authentication:Schemes:MicrosoftOidc:Scope configuration. For general configuration guidance, see xref:fundamentals/configuration/index and xref:blazor/fundamentals/configuration.
oidcOptions.Scope.Add(OpenIdConnectScope.OpenIdProfile);xref:Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions.SaveTokens%2A: Defines whether access and refresh tokens should be stored in the xref:Microsoft.AspNetCore.Authentication.AuthenticationProperties after a successful authorization. This property is set to true so the refresh token gets stored for non-interactive token refresh.
oidcOptions.SaveTokens = true;Scope for offline access (xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.Scope%2A): The offline_access scope is required for the refresh token.
oidcOptions.Scope.Add(OpenIdConnectScope.OfflineAccess);xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.Authority%2A and xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.ClientId%2A: Sets the Authority and Client ID for OIDC calls.
oidcOptions.Authority = "{AUTHORITY}";
oidcOptions.ClientId = "{CLIENT ID}";The following example uses a Tenant ID of aaaabbbb-0000-cccc-1111-dddd2222eeee and a Client ID of 00001111-aaaa-2222-bbbb-3333cccc4444:
oidcOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0";
oidcOptions.ClientId = "00001111-aaaa-2222-bbbb-3333cccc4444";For multi-tenant apps, the "common" authority should be used. You can also use the "common" authority for single-tenant apps, but a custom xref:Microsoft.IdentityModel.Tokens.TokenValidationParameters.IssuerValidator%2A is required, as shown later in this section.
oidcOptions.Authority = "https://login.microsoftonline.com/common/v2.0";xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.ResponseType%2A: Configures the OIDC handler to only perform authorization code flow. Implicit grants and hybrid flows are unnecessary in this mode. The OIDC handler automatically requests the appropriate tokens using the code returned from the authorization endpoint.
oidcOptions.ResponseType = OpenIdConnectResponseType.Code;xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.MapInboundClaims%2A and configuration of xref:Microsoft.IdentityModel.Tokens.TokenValidationParameters.NameClaimType%2A and xref:Microsoft.IdentityModel.Tokens.TokenValidationParameters.RoleClaimType%2A: Many OIDC servers use "name" and "role" rather than the SOAP/WS-Fed defaults in xref:System.Security.Claims.ClaimTypes. When xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.MapInboundClaims%2A is set to false, the handler doesn't perform claims mappings, and the claim names from the JWT are used directly by the app. The following example sets the role claim type to "roles," which is appropriate for Microsoft Entra ID (ME-ID). Consult your identity provider's documentation for more information.
Note
xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.MapInboundClaims%2A must be set to false for most OIDC providers, which prevents renaming claims.
oidcOptions.MapInboundClaims = false;
oidcOptions.TokenValidationParameters.NameClaimType = "name";
oidcOptions.TokenValidationParameters.RoleClaimType = "roles";Path configuration: Paths must match the redirect URI (login callback path) and post logout redirect (signed-out callback path) paths configured when registering the application with the OIDC provider. In the Azure portal, paths are configured in the Authentication blade of the app's registration. Both the sign-in and sign-out paths must be registered as redirect URIs. The default values are /signin-oidc and /signout-callback-oidc.
xref:Microsoft.AspNetCore.Builder.RemoteAuthenticationOptions.CallbackPath: The request path within the app's base path where the user-agent is returned.
Configure the signed-out callback path in the app's OIDC provider registration. In the following example, the {PORT} placeholder is the app's port:
:::no-loc text="https://localhost:{PORT}/signin-oidc":::
Note
A port isn't required for localhost addresses when using Microsoft Entra ID. Most other OIDC providers require the correct port.
xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.SignedOutCallbackPath%2A (configuration key: "SignedOutCallbackPath"): The request path within the app's base path intercepted by the OIDC handler where the user agent is first returned after signing out from the identity provider. 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 OIDC handler redirects to the xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.SignedOutRedirectUri%2A or xref:Microsoft.AspNetCore.Authentication.AuthenticationProperties.RedirectUri%2A, if specified.
Configure the signed-out callback path in the app's OIDC provider registration. In the following example, the {PORT} placeholder is the app's port:
:::no-loc text="https://localhost:{PORT}/signout-callback-oidc":::
Note
When using Microsoft Entra ID, set the path in the Web platform configuration's Redirect URI entries in the Entra or Azure portal. A port isn't required for localhost addresses when using Entra. Most other OIDC providers require the correct port. If you don't add the signed-out callback path URI to the app's registration in Entra, Entra refuses to redirect the user back to the app and merely asks them to close their browser window.
xref:Microsoft.AspNetCore.Builder.OpenIdConnectOptions.RemoteSignOutPath%2A: Requests received on this path cause the handler to invoke sign-out using the sign-out scheme.
In the following example, the {PORT} placeholder is the app's port:
:::no-loc text="https://localhost/signout-oidc":::
Note
When using Microsoft Entra ID, set the Front-channel logout URL in the Entra or Azure portal. A port isn't required for localhost addresses when using Entra. Most other OIDC providers require the correct port.
oidcOptions.CallbackPath = new PathString("{PATH}");
oidcOptions.SignedOutCallbackPath = new PathString("{PATH}");
oidcOptions.RemoteSignOutPath = new PathString("{PATH}");Examples (default values):
oidcOptions.CallbackPath = new PathString("/signin-oidc");
oidcOptions.SignedOutCallbackPath = new PathString("/signout-callback-oidc");
oidcOptions.RemoteSignOutPath = new PathString("/signout-oidc");(Microsoft Azure only with the "common" endpoint) xref:Microsoft.IdentityModel.Tokens.TokenValidationParameters.IssuerValidator%2A?displayProperty=nameWithType: Many OIDC providers work with the default issuer validator, but we need to account for the issuer parameterized with the Tenant ID ({TENANT ID}) returned by https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration. For more information, see SecurityTokenInvalidIssuerException with OpenID Connect and the Azure AD "common" endpoint (AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet #1731).
Only for apps using Microsoft Entra ID or Azure AD B2C with the "common" endpoint:
var microsoftIssuerValidator = AadIssuerValidator.GetAadIssuerValidator(oidcOptions.Authority);
oidcOptions.TokenValidationParameters.IssuerValidator = microsoftIssuerValidator.Validate;The BlazorWebAppOidc.Client project is the client-side project of the Blazor Web App.
:::moniker range=">= aspnetcore-9.0"
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.
:::moniker-end
:::moniker range="< aspnetcore-9.0"
The PersistentAuthenticationStateProvider class (PersistentAuthenticationStateProvider.cs) is a client-side xref:Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider that determines the user's authentication state by looking for data persisted in the page when it was rendered on the server. The authentication state is fixed for the lifetime of the WebAssembly application.
:::moniker-end
If the user needs to log in or out, a full page reload is required.
The sample app only provides a user name and email for display purposes.
:::zone-end
:::zone pivot="without-yarp-and-aspire-server"
:::moniker range=">= aspnetcore-9.0"
For Microsoft Entra ID or Azure AD B2C, you can use xref:Microsoft.Identity.Web.AppBuilderExtension.AddMicrosoftIdentityWebApp%2A from Microsoft Identity Web (Microsoft.Identity.Web NuGet package, API documentation), which adds both the OIDC and Cookie authentication handlers with the appropriate defaults. The sample app and the guidance in this article don't use Microsoft Identity Web. The guidance demonstrates how to configure the OIDC handler manually for any OIDC provider. For more information on implementing Microsoft Identity Web, see xref:blazor/security/blazor-web-app-entra.
:::moniker-end
This version of the article covers implementing OIDC with the Backend for Frontend (BFF) pattern without YARP and Aspire. Change the article version selector to With YARP and Aspire (Interactive Auto) if the app's specification calls for adopting YARP and Aspire.
The following specification is adopted:
- The Blazor Web App uses the Server render mode with global interactivity.
- This app is a starting point for any OIDC authentication flow. OIDC is configured manually in the app and doesn't rely upon Microsoft Entra ID or Microsoft Identity Web packages, nor does the sample app require Microsoft Azure hosting. However, the sample app can be used with Entra, Microsoft Identity Web, and hosted in Azure.
- Automatic non-interactive token refresh.
- A separate web API project demonstrates a secure web API call for weather data.
For an alternative experience using Microsoft Authentication Library for .NET, Microsoft Identity Web, and Microsoft Entra ID, see xref:blazor/security/blazor-web-app-entra.
The sample app consists of the following projects:
BlazorWebAppOidcServer: Blazor Web App server-side project (global Interactive Server rendering).MinimalApiJwt: Backend web API with a 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 BlazorWebAppOidcBffServer folder for .NET 8 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 BlazorWebAppOidcServer 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.
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 (BlazorWebAppOidcServer) with a Web platform configuration and a Redirect URI of https://localhost/signin-oidc (a port isn't required). The app's tenant ID 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 Program 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.
This section only applies to the server project of the Blazor Web App (BlazorWebAppOidcServer project).
For local development testing, use the Secret Manager tool to store the Blazor server project's client secret under the configuration key Authentication:Schemes:MicrosoftOidc:ClientSecret.
The Blazor server project 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 app's project file):
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 registration:
dotnet user-secrets set "Authentication:Schemes:MicrosoftOidc:ClientSecret" "{SECRET}"
If using Visual Studio, you can confirm the secret is set by right-clicking the project in Solution Explorer and selecting Manage User Secrets.
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.
:::moniker range=">= aspnetcore-9.0"
The project includes packages and configuration to produce OpenAPI documents.
:::moniker-end
:::moniker range="< aspnetcore-9.0"
The project includes packages and configuration to produce OpenAPI documents and the Swagger UI in the Development environment. For more information, see xref:fundamentals/openapi/using-openapi-documents#use-swagger-ui-for-local-ad-hoc-testing.
:::moniker-end
The project creates a Minimal API endpoint for weather data:
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();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.
The xref:Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.Authority%2A sets the Authority for making OIDC calls. We recommend using a separate app registration for the MinimalApiJwt project. The authority matches the issurer (iss) of the JWT returned by the identity provider.
jwtOptions.Authority = "{AUTHORITY}";The format of the Authority depends on the type of tenant in use. The following examples for Microsoft Entra ID use a Tenant ID of aaaabbbb-0000-cccc-1111-dddd2222eeee.
ME-ID tenant Authority example:
jwtOptions.Authority = "https://sts.windows.net/aaaabbbb-0000-cccc-1111-dddd2222eeee";AAD B2C tenant Authority example:
jwtOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0";The xref:Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions.Audience%2A sets the Audience for any received OIDC token.
jwtOptions.Audience = "{APP ID URI}";Note
When using Microsoft Entra ID, 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 format of the Audience depends on the type of tenant in use. The following examples for Microsoft Entra ID use a Tenant ID of contoso and a Client ID of 11112222-bbbb-3333-cccc-4444dddd5555.
ME-ID tenant App ID URI example:
jwtOptions.Audience = "api://11112222-bbbb-3333-cccc-4444dddd5555";AAD B2C tenant App ID URI example:
jwtOptions.Audience = "https://contoso.onmicrosoft.com/11112222-bbbb-3333-cccc-4444dddd5555";Automatic non-interactive token refresh is managed by a custom cookie refresher (CookieOidcRefresher.cs).
A xref:System.Net.Http.DelegatingHandler (TokenHandler) manages attaching a user's access token to outgoing requests. The token handler only executes during static server-side rendering (static SSR), so using xref:Microsoft.AspNetCore.Http.HttpContext is safe in this scenario. For more information, see xref:blazor/components/httpcontext and xref:blazor/security/additional-scenarios#use-a-token-handler-for-web-api-calls.
TokenHandler.cs:
public class TokenHandler(IHttpContextAccessor httpContextAccessor) :
DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
if (httpContextAccessor.HttpContext is null)
{
throw new Exception("HttpContext not available");
}
var accessToken = await httpContextAccessor.HttpContext
.GetTokenAsync("access_token");
request.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);
return await base.SendAsync(request, cancellationToken);
}
}In the project's Program file, the token handler (TokenHandler) is registered as a service and specified as the message handler with xref:Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler%2A for making secure requests to the backend MinimalApiJwt web API using a named HTTP client ("ExternalApi").
builder.Services.AddScoped<TokenHandler>();
builder.Services.AddHttpClient("ExternalApi",
client => client.BaseAddress = new Uri(builder.Configuration["ExternalApiUri"] ??
throw new Exception("Missing base address!")))
.AddHttpMessageHandler<TokenHandler>();The Weather component uses the [Authorize] attribute to prevent unauthorized access. For more information on requiring authorization across the app via an authorization policy and opting out of authorization at a subset of public endpoints, see the Razor Pages OIDC guidance.
The ExternalApi HTTP client is used to make a request for weather data to the secure web API. In the OnInitializedAsync lifecycle event of Weather.razor:
using var request = new HttpRequestMessage(HttpMethod.Get, "/weather-forecast");
var client = ClientFactory.CreateClient("ExternalApi");
using var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
forecasts = await response.Content.ReadFromJsonAsync<WeatherForecast[]>() ??
throw new IOException("No weather forecast!");In the project's appsettings.json file, configure the external API URI:
"ExternalApiUri": "{BASE ADDRESS}"Example:
"ExternalApiUri": "https://localhost:7277"The following xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions configuration is found in the project's Program file on the call to xref:Microsoft.Extensions.DependencyInjection.OpenIdConnectExtensions.AddOpenIdConnect%2A:
:::moniker range=">= aspnetcore-9.0"
xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.PushedAuthorizationBehavior%2A: Controls Pushed Authorization Requests (PAR) support. By default, the setting is to use PAR if the identity provider's discovery document (usually found at .well-known/openid-configuration) advertises support for PAR. If you wish to require PAR support for the app, you can assign a value of PushedAuthorizationBehavior.Require. PAR isn't supported by Microsoft Entra, and there are no plans for Entra to ever support it in the future.
oidcOptions.PushedAuthorizationBehavior = PushedAuthorizationBehavior.UseIfAvailable;:::moniker-end
xref:Microsoft.AspNetCore.Builder.RemoteAuthenticationOptions.SignInScheme%2A: Sets the authentication scheme corresponding to the middleware responsible of persisting user's identity after a successful authentication. The OIDC handler needs to use a sign-in scheme that's capable of persisting user credentials across requests. The following line is present merely for demonstration purposes. If omitted, xref:Microsoft.AspNetCore.Authentication.AuthenticationOptions.DefaultSignInScheme%2A is used as a fallback value.
oidcOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;Scopes for openid and profile (xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.Scope%2A) (Optional): The openid and profile scopes are also configured by default because they're required for the OIDC handler to work, but these may need to be re-added if scopes are included in the Authentication:Schemes:MicrosoftOidc:Scope configuration. For general configuration guidance, see xref:fundamentals/configuration/index and xref:blazor/fundamentals/configuration.
oidcOptions.Scope.Add(OpenIdConnectScope.OpenIdProfile);Configure the Weather.Get scope for accessing the external web API for weather data. The following example is based on using Entra ID in an ME-ID tenant domain. In the following example, the {APP ID URI} placeholder is found in the Entra or Azure portal where the web API is exposed. For any other identity provider, use the appropriate scope.
oidcOptions.Scope.Add("{APP ID URI}/Weather.Get");The format of the scope depends on the type of tenant in use. In the following examples, the Tenant Domain is contoso.onmicrosoft.com, and the Client ID is 11112222-bbbb-3333-cccc-4444dddd5555.
ME-ID tenant App ID URI example:
oidcOptions.Scope.Add("api://11112222-bbbb-3333-cccc-4444dddd5555/Weather.Get");AAD B2C tenant App ID URI example:
oidcOptions.Scope.Add("https://contoso.onmicrosoft.com/11112222-bbbb-3333-cccc-4444dddd5555/Weather.Get");xref:Microsoft.AspNetCore.Authentication.RemoteAuthenticationOptions.SaveTokens%2A: Defines whether access and refresh tokens should be stored in the xref:Microsoft.AspNetCore.Authentication.AuthenticationProperties after a successful authorization. This property is set to true so the refresh token gets stored for non-interactive token refresh.
oidcOptions.SaveTokens = true;Scope for offline access (xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.Scope%2A): The offline_access scope is required for the refresh token.
oidcOptions.Scope.Add(OpenIdConnectScope.OfflineAccess);xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.Authority%2A and xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.ClientId%2A: Sets the Authority and Client ID for OIDC calls.
oidcOptions.Authority = "{AUTHORITY}";
oidcOptions.ClientId = "{CLIENT ID}";The following example uses a Tenant ID of aaaabbbb-0000-cccc-1111-dddd2222eeee and a Client ID of 00001111-aaaa-2222-bbbb-3333cccc4444:
oidcOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0";
oidcOptions.ClientId = "00001111-aaaa-2222-bbbb-3333cccc4444";For multi-tenant apps, the "common" authority should be used. You can also use the "common" authority for single-tenant apps, but a custom xref:Microsoft.IdentityModel.Tokens.TokenValidationParameters.IssuerValidator%2A is required, as shown later in this section.
oidcOptions.Authority = "https://login.microsoftonline.com/common/v2.0";xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.ResponseType%2A: Configures the OIDC handler to only perform authorization code flow. Implicit grants and hybrid flows are unnecessary in this mode. The OIDC handler automatically requests the appropriate tokens using the code returned from the authorization endpoint.
oidcOptions.ResponseType = OpenIdConnectResponseType.Code;xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.MapInboundClaims%2A and configuration of xref:Microsoft.IdentityModel.Tokens.TokenValidationParameters.NameClaimType%2A and xref:Microsoft.IdentityModel.Tokens.TokenValidationParameters.RoleClaimType%2A: Many OIDC servers use "name" and "role" rather than the SOAP/WS-Fed defaults in xref:System.Security.Claims.ClaimTypes. When xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.MapInboundClaims%2A is set to false, the handler doesn't perform claims mappings, and the claim names from the JWT are used directly by the app. The following example sets the role claim type to "roles," which is appropriate for Microsoft Entra ID (ME-ID). Consult your identity provider's documentation for more information.
Note
xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.MapInboundClaims%2A must be set to false for most OIDC providers, which prevents renaming claims.
oidcOptions.MapInboundClaims = false;
oidcOptions.TokenValidationParameters.NameClaimType = "name";
oidcOptions.TokenValidationParameters.RoleClaimType = "roles";Path configuration: Paths must match the redirect URI (login callback path) and post logout redirect (signed-out callback path) paths configured when registering the application with the OIDC provider. In the Azure portal, paths are configured in the Authentication blade of the app's registration. Both the sign-in and sign-out paths must be registered as redirect URIs. The default values are /signin-oidc and /signout-callback-oidc.
xref:Microsoft.AspNetCore.Builder.RemoteAuthenticationOptions.CallbackPath: The request path within the app's base path where the user-agent is returned.
Configure the signed-out callback path in the app's OIDC provider registration. In the following example, the {PORT} placeholder is the app's port:
:::no-loc text="https://localhost:{PORT}/signin-oidc":::
Note
A port isn't required for localhost addresses when using Microsoft Entra ID. Most other OIDC providers require the correct port.
xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.SignedOutCallbackPath%2A (configuration key: "SignedOutCallbackPath"): The request path within the app's base path intercepted by the OIDC handler where the user agent is first returned after signing out from the identity provider. 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 OIDC handler redirects to the xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.SignedOutRedirectUri%2A or xref:Microsoft.AspNetCore.Authentication.AuthenticationProperties.RedirectUri%2A, if specified.
Configure the signed-out callback path in the app's OIDC provider registration. In the following example, the {PORT} placeholder is the app's port:
:::no-loc text="https://localhost:{PORT}/signout-callback-oidc":::
Note
When using Microsoft Entra ID, set the path in the Web platform configuration's Redirect URI entries in the Entra or Azure portal. A port isn't required for localhost addresses when using Entra. Most other OIDC providers require the correct port. If you don't add the signed-out callback path URI to the app's registration in Entra, Entra refuses to redirect the user back to the app and merely asks them to close their browser window.
xref:Microsoft.AspNetCore.Builder.OpenIdConnectOptions.RemoteSignOutPath%2A: Requests received on this path cause the handler to invoke sign-out using the sign-out scheme.
In the following example, the {PORT} placeholder is the app's port:
:::no-loc text="https://localhost/signout-oidc":::
Note
When using Microsoft Entra ID, set the Front-channel logout URL in the Entra or Azure portal. A port isn't required for localhost addresses when using Entra. Most other OIDC providers require the correct port.
oidcOptions.CallbackPath = new PathString("{PATH}");
oidcOptions.SignedOutCallbackPath = new PathString("{PATH}");
oidcOptions.RemoteSignOutPath = new PathString("{PATH}");Examples (default values):
oidcOptions.CallbackPath = new PathString("/signin-oidc");
oidcOptions.SignedOutCallbackPath = new PathString("/signout-callback-oidc");
oidcOptions.RemoteSignOutPath = new PathString("/signout-oidc");(Microsoft Azure only with the "common" endpoint) xref:Microsoft.IdentityModel.Tokens.TokenValidationParameters.IssuerValidator%2A?displayProperty=nameWithType: Many OIDC providers work with the default issuer validator, but we need to account for the issuer parameterized with the Tenant ID ({TENANT ID}) returned by https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration. For more information, see SecurityTokenInvalidIssuerException with OpenID Connect and the Azure AD "common" endpoint (AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet #1731).
Only for apps using Microsoft Entra ID or Azure AD B2C with the "common" endpoint:
var microsoftIssuerValidator = AadIssuerValidator.GetAadIssuerValidator(oidcOptions.Authority);
oidcOptions.TokenValidationParameters.IssuerValidator = microsoftIssuerValidator.Validate;:::zone-end
:::moniker range=">= aspnetcore-9.0"
This section only applies to the Interactive Auto render mode sample apps.
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.
:::moniker-end
The sample solution projects configure OIDC 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 BlazorWebAppOidc or BlazorWebAppOidcServer project, add the following JSON configuration:
"Authentication": {
"Schemes": {
"MicrosoftOidc": {
"Authority": "https://login.microsoftonline.com/{TENANT ID (BLAZOR APP)}/v2.0",
"ClientId": "{CLIENT ID (BLAZOR APP)}",
"CallbackPath": "/signin-oidc",
"SignedOutCallbackPath": "/signout-callback-oidc",
"RemoteSignOutPath": "/signout-oidc",
"SignedOutRedirectUri": "/",
"Scope": [
"openid",
"profile",
"offline_access",
"{APP ID URI (WEB API)}/Weather.Get"
]
}
}
},Update the placeholders in the preceding configuration to match the values that the app uses in the Program file:
{TENANT ID (BLAZOR APP)}: The Tenant Id of the Blazor app.{CLIENT ID (BLAZOR APP)}: The Client Id of the Blazor app.{APP ID URI (WEB API)}: The App ID URI of the web API.
The "common" Authority (https://login.microsoftonline.com/common/v2.0) should be used for multi-tenant apps. To use the "common" Authority for single-tenant apps, see the Use the "common" Authority for single-tenant apps section.
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.
Remove the following lines from the Program file:
- oidcOptions.Scope.Add(OpenIdConnectScope.OpenIdProfile);
- oidcOptions.Scope.Add("...");
- oidcOptions.CallbackPath = new PathString("...");
- oidcOptions.SignedOutCallbackPath = new PathString("...");
- oidcOptions.RemoteSignOutPath = new PathString("...");
- oidcOptions.Authority = "...";
- oidcOptions.ClientId = "...";In the ConfigureCookieOidc method of CookieOidcServiceCollectionExtensions.cs, remove the following line:
- oidcOptions.Scope.Add(OpenIdConnectScope.OfflineAccess);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)}" ]
}
}
},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
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
You can use the "common" Authority for single-tenant apps, but you must take the following steps to implement a custom issuer validator.
Add the Microsoft.IdentityModel.Validators NuGet package to the server project.
At the top of the Program file, make the xref:Microsoft.IdentityModel.Validators?displayProperty=fullName namespace available:
using Microsoft.IdentityModel.Validators;Use the following code in the Program file where OIDC options are configured:
var microsoftIssuerValidator =
AadIssuerValidator.GetAadIssuerValidator(oidcOptions.Authority);
oidcOptions.TokenValidationParameters.IssuerValidator =
microsoftIssuerValidator.Validate;For more information, see SecurityTokenInvalidIssuerException with OpenID Connect and the Azure AD "common" endpoint (AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet #1731).
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>:::moniker range="< aspnetcore-10.0"
The custom cookie refresher (CookieOidcRefresher.cs) implementation updates the user's claims automatically when they expire. The current implementation expects to receive an ID token from the token endpoint in exchange for the refresh token. The claims in this ID token are then used to overwrite the user's claims.
The sample implementation doesn't include code for requesting claims from the UserInfo endpoint on token refresh. For more information, see BlazorWebAppOidc AddOpenIdConnect with GetClaimsFromUserInfoEndpoint = true doesn't propogate [sic] role claims to client (dotnet/aspnetcore #58826).
Note
Some identity providers only return an access token when using a refresh token. The CookieOidcRefresher can be updated with additional logic to continue to use the prior set of claims stored in the authentication cookie or use the access token to request claims from the UserInfo endpoint.
:::moniker-end
A nonce is a string value that associates a client's session with an ID token to mitigate replay attacks.
If you receive a nonce error during authentication development and testing, use a new InPrivate/incognito browser session for each test run, no matter how small the change made to the app or test user because stale cookie data can lead to a nonce error. For more information, see the Cookies and site data section.
A nonce isn't required or used when a refresh token is exchanged for a new access token. In the sample app, the CookieOidcRefresher (CookieOidcRefresher.cs) deliberately sets xref:Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectProtocolValidator.RequireNonce?displayProperty=nameWithType to false.
This section pertains to apps that don't use Microsoft Entra ID (ME-ID) as the identity provider. For apps registered with ME-ID, see the Application roles for apps registered with Microsoft Entra (ME-ID) section.
Configure the role claim type (xref:Microsoft.IdentityModel.Tokens.TokenValidationParameters.RoleClaimType?displayProperty=nameWithType) in the xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions of Program.cs:
oidcOptions.TokenValidationParameters.RoleClaimType = "{ROLE CLAIM TYPE}";For many OIDC identity providers, the role claim type is role. Check your identity provider's documentation for the correct value.
Replace the UserInfo class in the BlazorWebAppOidc.Client project with the following class.
UserInfo.cs:
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using System.Security.Claims;
namespace BlazorWebAppOidc.Client;
// Add properties to this class and update the server and client
// AuthenticationStateProviders to expose more information about
// the authenticated user to the client.
public sealed class UserInfo
{
public required string UserId { get; init; }
public required string Name { get; init; }
public required string[] Roles { get; init; }
public const string UserIdClaimType = "sub";
public const string NameClaimType = "name";
private const string RoleClaimType = "role";
public static UserInfo FromClaimsPrincipal(ClaimsPrincipal principal) =>
new()
{
UserId = GetRequiredClaim(principal, UserIdClaimType),
Name = GetRequiredClaim(principal, NameClaimType),
Roles = principal.FindAll(RoleClaimType).Select(c => c.Value)
.ToArray(),
};
public ClaimsPrincipal ToClaimsPrincipal() =>
new(new ClaimsIdentity(
Roles.Select(role => new Claim(RoleClaimType, role))
.Concat([
new Claim(UserIdClaimType, UserId),
new Claim(NameClaimType, Name),
]),
authenticationType: nameof(UserInfo),
nameType: NameClaimType,
roleType: RoleClaimType));
private static string GetRequiredClaim(ClaimsPrincipal principal,
string claimType) =>
principal.FindFirst(claimType)?.Value ??
throw new InvalidOperationException(
$"Could not find required '{claimType}' claim.");
}At this point, Razor components can adopt role-based and policy-based authorization. Application roles appear in role claims, one claim per role.
Use the guidance in this section to implement application roles, ME-ID security groups, and ME-ID built-in administrator roles for apps using Microsoft Entra ID (ME-ID).
The approach described in this section configures ME-ID to send groups and roles in the authentication cookie header. When users are only a member of a few security groups and roles, the following approach should work for most hosting platforms without running into a problem where headers are too long, for example with IIS hosting that has a default header length limit of 16 KB (MaxRequestBytes). If header length is a problem due to high group or role membership, we recommend not following the guidance in this section in favor of implementing Microsoft Graph to obtain a user's groups and roles from ME-ID separately, an approach that doesn't inflate the size of the authentication cookie. For more information, see Bad Request - Request Too Long - IIS Server (dotnet/aspnetcore #57545).
Configure the role claim type (xref:Microsoft.IdentityModel.Tokens.TokenValidationParameters.RoleClaimType?displayProperty=nameWithType) in xref:Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions of Program.cs. Set the value to roles:
oidcOptions.TokenValidationParameters.RoleClaimType = "roles";Although you can't assign roles to groups without an ME-ID Premium account, you can assign roles to users and receive role claims for users with a standard Azure account. The guidance in this section doesn't require an ME-ID Premium account.
When working with the default directory, follow the guidance in Add app roles to your application and receive them in the token (ME-ID documentation) to configure and assign roles. If you aren't working with the default directory, edit the app's manifest in the Azure portal to establish the app's roles manually in the appRoles entry of the manifest file. For more information, see Configure the role claim (ME-ID documentation).
A user's Azure security groups arrive in groups claims, and a user's built-in ME-ID administrator role assignments arrive in well-known IDs (wids) claims. Values for both claim types are GUIDs. When received by the app, these claims can be used to establish role and policy authorization in Razor components.
In the app's manifest in the Azure portal, set the groupMembershipClaims attribute to All. A value of All results in ME-ID sending all of the security/distribution groups (groups claims) and roles (wids claims) of the signed-in user. To set the groupMembershipClaims attribute:
- Open the app's registration in the Azure portal.
- Select Manage > Manifest in the sidebar.
- Find the
groupMembershipClaimsattribute. - Set the value to
All("groupMembershipClaims": "All"). - Select the Save button.
Replace the UserInfo class in the BlazorWebAppOidc.Client project with the following class.
UserInfo.cs:
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using System.Security.Claims;
namespace BlazorWebAppOidc.Client;
// Add properties to this class and update the server and client
// AuthenticationStateProviders to expose more information about
// the authenticated user to the client.
public sealed class UserInfo
{
public required string UserId { get; init; }
public required string Name { get; init; }
public required string[] Roles { get; init; }
public required string[] Groups { get; init; }
public required string[] Wids { get; init; }
public const string UserIdClaimType = "sub";
public const string NameClaimType = "name";
private const string RoleClaimType = "roles";
private const string GroupsClaimType = "groups";
private const string WidsClaimType = "wids";
public static UserInfo FromClaimsPrincipal(ClaimsPrincipal principal) =>
new()
{
UserId = GetRequiredClaim(principal, UserIdClaimType),
Name = GetRequiredClaim(principal, NameClaimType),
Roles = principal.FindAll(RoleClaimType).Select(c => c.Value)
.ToArray(),
Groups = principal.FindAll(GroupsClaimType).Select(c => c.Value)
.ToArray(),
Wids = principal.FindAll(WidsClaimType).Select(c => c.Value)
.ToArray(),
};
public ClaimsPrincipal ToClaimsPrincipal() =>
new(new ClaimsIdentity(
Roles.Select(role => new Claim(RoleClaimType, role))
.Concat(Groups.Select(role => new Claim(GroupsClaimType, role)))
.Concat(Wids.Select(role => new Claim(WidsClaimType, role)))
.Concat([
new Claim(UserIdClaimType, UserId),
new Claim(NameClaimType, Name),
]),
authenticationType: nameof(UserInfo),
nameType: NameClaimType,
roleType: RoleClaimType));
private static string GetRequiredClaim(ClaimsPrincipal principal,
string claimType) =>
principal.FindFirst(claimType)?.Value ??
throw new InvalidOperationException(
$"Could not find required '{claimType}' claim.");
}At this point, Razor components can adopt role-based and policy-based authorization:
- Application roles appear in
rolesclaims, one claim per role. - Security groups appear in
groupsclaims, one claim per group. The security group GUIDs appear in the Azure portal when you create a security group and are listed when selecting Identity > Overview > Groups > View. - Built-in ME-ID administrator roles appear in
widsclaims, one claim per role. Thewidsclaim with a value ofb79fbf4d-3ef9-4689-8143-76b194e85509is always sent by ME-ID for non-guest accounts of the tenant and doesn't refer to an administrator role. Administrator role GUIDs (role template IDs) appear in the Azure portal when selecting Roles & admins, followed by the ellipsis (…) > Description for the listed role. The role template IDs are also listed in Microsoft Entra built-in roles (Entra documentation).
xref:Microsoft.Extensions.DependencyInjection.OpenIdConnectExtensions.AddOpenIdConnect%2A supports opaque tokens because it doesn't perform access token validation when configured for Proof Key for Code Exchange (PKCE) authorization code flow. It relies on the ASP.NET Core server's HTTPS backchannel to the OIDC authentication service to obtain the ID token using the authorization code received when the user redirects back to the ASP.NET Core app after signing in. If the app is only required to log a user in with OIDC to get a valid authentication cookie, opaque access tokens are supported without modifying the app.
Only if the opaque token acquired by xref:Microsoft.Extensions.DependencyInjection.OpenIdConnectExtensions.AddOpenIdConnect%2A is passed to another service that attempts to validate it with xref:Microsoft.Extensions.DependencyInjection.JwtBearerExtensions.AddJwtBearer%2A is there a failure to authenticate the user. Unlike self-contained JWTs, opaque tokens require a request to an authorization server to validate their status and retrieve claims. To work around this limitation, either use a third-party API, such as the Duende Introspection Authentication Handler, or create a custom AuthenticationHandler to validate the token.
Important
Duende Software isn't owned or controlled by Microsoft and might require you to pay a license fee for production use of the Duende Introspection Authentication Handler.
The following xref:Microsoft.AspNetCore.Authentication.AuthenticationHandler%601 and associated configuration and helper code is provided as a starting point for further development. The handler extracts the opaque token from the Authorization header for an HTTP call to an authorization server's introspection endpoint and creates an xref:Microsoft.AspNetCore.Authentication.AuthenticationTicket containing the user's claims.
HttpContextExtensions.cs:
namespace MinimalApiJwt.Extensions;
public static class HttpContextExtensions
{
public static string? ExtractBearerToken(this HttpRequest request)
{
var authorizationHeader = request.Headers["Authorization"].ToString();
if (!string.IsNullOrEmpty(authorizationHeader) &&
authorizationHeader.StartsWith("Bearer ",
StringComparison.OrdinalIgnoreCase))
{
var token = authorizationHeader["Bearer ".Length..].Trim();
if (!string.IsNullOrEmpty(token))
{
return token;
}
}
return null;
}
}OpaqueTokenAuthenticationOptions.cs:
using Microsoft.AspNetCore.Authentication;
namespace MinimalApiJwt.Authentication;
public class OpaqueTokenAuthenticationOptions : AuthenticationSchemeOptions
{
public const string DefaultScheme = "OpaqueTokenAuthentication";
public string? IntrospectionEndpoint { get; set; }
public string? ClientId { get; set; }
}OpaqueTokenAuthenticationHandler.cs:
using System.Security.Claims;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Options;
using MinimalApiJwt.Authentication;
using MinimalApiJwt.Extensions;
namespace MinimalApiJwt.Services;
public class OpaqueTokenAuthenticationHandler(
IOptionsMonitor<OpaqueTokenAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder)
: AuthenticationHandler<OpaqueTokenAuthenticationOptions>(options, logger, encoder)
{
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var token = Request.ExtractBearerToken();
if (token is null)
{
var failedResult = AuthenticateResult.Fail("Authorization failed.");
return Task.FromResult(failedResult);
}
/* Validate the opaque (reference) access token
Make an HTTP call to the authorization server's introspection endpoint
with the token and the API's credentials, process the response to
determine if the token is valid.
If the token is invalid, return a failed authorization result.
If the token is valid, create an AuthenticationTicket containing the
user's claims.
*/
var claims = new[] { new Claim(ClaimTypes.Name, "user_id") };
var identity = new ClaimsIdentity(claims,
OpaqueTokenAuthenticationOptions.DefaultScheme);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal,
OpaqueTokenAuthenticationOptions.DefaultScheme);
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}In the Program file:
builder.Services.AddHttpClient();
builder.Services.AddAuthentication()
.AddScheme<OpaqueTokenAuthenticationOptions, OpaqueTokenAuthenticationHandler>(
OpaqueTokenAuthenticationOptions.DefaultScheme,
options =>
{
options.IntrospectionEndpoint = "{AUTH SERVER URI}";
options.ClientId = "{API CLIENT ID}";
});The preceding example's placeholders:
{AUTH SERVER URI}: Authentication server URI{API CLIENT ID}: API Client ID
Built-in opaque access token support is under consideration for a future release of .NET. For more information, see Opaque - reference token validation (dotnet/aspnetcore #46026).
In the sample app, a custom cookie refresher (CookieOidcRefresher.cs) implementation is used to perform automatic non-interactive token refresh. An alternative solution can be found in the open source Duende.AccessTokenManagement.OpenIdConnect package.
Duende Access Token Management provides automatic access token management features for .NET worker and ASP.NET Core web apps, including Blazor, without the need to add a custom cookie refresher.
After the package is installed, remove the CookieOidcRefresher and add access token management for the currently logged-in user in the Program file:
// Add services for token management
builder.Services.AddOpenIdConnectAccessTokenManagement();
// Register a typed HTTP client with token management support
builder.Services.AddHttpClient<InvoiceClient>(client =>
{
client.BaseAddress = new Uri("https://api.example.com/invoices/");
})
.AddUserAccessTokenHandler();The typed HTTP client (or named HTTP client, if implemented) has automatic access token lifetime management on behalf of the currently logged-in user, including transparent refresh token management.
For more information, see the Duende Access Token Management documentation for Blazor.
AzureAD/microsoft-identity-webGitHub repository: Helpful guidance on implementing Microsoft Identity Web for Microsoft Entra ID and Azure Active Directory B2C 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
- Refresh token during http request in Blazor Interactive Server with OIDC (
dotnet/aspnetcore#55213) - Secure data in Blazor Web Apps with Interactive Auto rendering
- How to access an
AuthenticationStateProviderfrom aDelegatingHandler