Secure an ASP.NET Core Blazor Web App with OpenID Connect (OIDC)
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).
This version of the article covers implementing OIDC without adopting the Backend for Frontend (BFF) pattern. The BFF pattern is useful for making authenticated requests to external services. Change the article version selector to OIDC with BFF pattern if the app's specification calls for adopting the BFF pattern.
The following specification is covered:
- 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.
- Securely calls a (web) API in the server project for data.
Sample app
The sample app consists of two 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.
Access the sample apps through the latest version folder from the repository's root with the following link. The projects are in the BlazorWebAppOidc
folder for .NET 8 or later.
View or download sample code (how to download)
Server-side Blazor Web App project (BlazorWebAppOidc
)
The BlazorWebAppOidc
project is the server-side project of the Blazor Web App.
The BlazorWebAppOidc.http
file can be used for testing the weather data request. Note that the BlazorWebAppOidc
project must be running to test the endpoint, and the endpoint is hardcoded into the file. For more information, see Use .http files in Visual Studio 2022.
Note
The server project uses IHttpContextAccessor/HttpContext, but never for interactively-rendered components. For more information, see Threat mitigation guidance for ASP.NET Core Blazor interactive server-side rendering.
Configuration
This section explains how to configure the sample app.
Note
For Microsoft Entra ID or Azure AD B2C, you can use AddMicrosoftIdentityWebApp 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 section doesn'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 the linked resources.
Establish the client secret
Warning
Don't store app secrets, connection strings, credentials, passwords, personal identification numbers (PINs), private C#/.NET code, or private keys/tokens in client-side code, which is always insecure. In test/staging and production environments, server-side Blazor code and web APIs should use secure authentication flows that avoid maintaining credentials within project code or configuration files. Outside of local development testing, we recommend avoiding the use of environment variables to store sensitive data, as environment variables aren't the most secure approach. For local development testing, the Secret Manager tool is recommended for securing sensitive data. For more information, see Securely maintain sensitive data and credentials.
For local development testing, use the Secret Manager tool to store the server app's client secret under the configuration key Authentication:Schemes:MicrosoftOidc:ClientSecret
.
Note
If the app uses Microsoft Entra ID or Azure AD B2C, create a client secret in the app's registration in the Entra or Azure portal (Manage > Certificates & secrets > New client secret). Use the Value of the new secret in the following guidance.
The sample 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):
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.
Configure the app
The following OpenIdConnectOptions configuration is found in the project's Program
file on the call to AddOpenIdConnect:
SignInScheme: 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, DefaultSignInScheme is used as a fallback value.
oidcOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
Scopes for
openid
andprofile
(Scope) (Optional): Theopenid
andprofile
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 theAuthentication:Schemes:MicrosoftOidc:Scope
configuration. For general configuration guidance, see Configuration in ASP.NET Core and ASP.NET Core Blazor configuration.oidcOptions.Scope.Add(OpenIdConnectScope.OpenIdProfile);
SaveTokens: Defines whether access and refresh tokens should be stored in the AuthenticationProperties after a successful authorization. This property is set to
false
to reduce the size of the final authentication cookie.oidcOptions.SaveTokens = false;
Scope for offline access (Scope): The
offline_access
scope is required for the refresh token.oidcOptions.Scope.Add(OpenIdConnectScope.OfflineAccess);
Authority and ClientId: Sets the Authority and Client ID for OIDC calls.
oidcOptions.Authority = "{AUTHORITY}"; oidcOptions.ClientId = "{CLIENT ID}";
Example:
- Authority (
{AUTHORITY}
):https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0/
(uses Tenant IDaaaabbbb-0000-cccc-1111-dddd2222eeee
) - Client Id (
{CLIENT ID}
):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";
Example for Microsoft Azure "common" authority:
The "common" authority should be used for multi-tenant apps. You can also use the "common" authority for single-tenant apps, but a custom IssuerValidator is required, as shown later in this section.
oidcOptions.Authority = "https://login.microsoftonline.com/common/v2.0/";
- Authority (
ResponseType: Configures the OIDC handler to only perform authorization code flow. Implicit grants and hybrid flows are unnecessary in this mode.
In the Entra or Azure portal's Implicit grant and hybrid flows app registration configuration, do not select either checkbox for the authorization endpoint to return Access tokens or ID tokens. The OIDC handler automatically requests the appropriate tokens using the code returned from the authorization endpoint.
oidcOptions.ResponseType = OpenIdConnectResponseType.Code;
MapInboundClaims and configuration of NameClaimType and RoleClaimType: Many OIDC servers use "
name
" and "role
" rather than the SOAP/WS-Fed defaults in ClaimTypes. When MapInboundClaims is set tofalse
, 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
MapInboundClaims 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
.CallbackPath: The request path within the app's base path where the user-agent is returned.
In the Entra or Azure portal, set the path in the Web platform configuration's Redirect URI:
https://localhost/signin-oidc
Note
A port isn't required for
localhost
addresses when using Microsoft Entra ID. Most other OIDC providers require a correct port.SignedOutCallbackPath: The request path within the app's base path where the user agent is returned after sign out from the identity provider.
In the Entra or Azure portal, set the path in the Web platform configuration's Redirect URI:
https://localhost/signout-callback-oidc
Note
A port isn't required for
localhost
addresses when using Microsoft Entra ID. Most other OIDC providers require a correct port.Note
If using Microsoft Identity Web, the provider currently only redirects back to the SignedOutCallbackPath if the
microsoftonline.com
Authority (https://login.microsoftonline.com/{TENANT ID}/v2.0/
) is used. This limitation doesn't exist if you can use the "common" Authority with Microsoft Identity Web. For more information, see postLogoutRedirectUri not working when authority url contains a tenant ID (AzureAD/microsoft-authentication-library-for-js
#5783).RemoteSignOutPath: Requests received on this path cause the handler to invoke sign-out using the sign-out scheme.
In the Entra or Azure portal, set the Front-channel logout URL:
https://localhost/signout-oidc
Note
A port isn't required for
localhost
addresses when using Microsoft Entra ID. Most other OIDC providers require a 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) TokenValidationParameters.IssuerValidator: 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 byhttps://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;
Sample app code
Inspect the sample app for the following features:
- Automatic non-interactive token refresh with the help of a custom cookie refresher (
CookieOidcRefresher.cs
). - The server project calls
AddAuthenticationStateSerialization
to add a server-side authentication state provider that uses PersistentComponentState to flow the authentication state to the client. The client callsAddAuthenticationStateDeserialization
to deserialize and use the authentication state passed by the server. The authentication state is fixed for the lifetime of the WebAssembly application. - An example requests to the Blazor Web App for weather data is handled by a Minimal API endpoint (
/weather-forecast
) in theProgram
file (Program.cs
). The endpoint requires authorization by calling RequireAuthorization. For any controllers that you add to the project, add the[Authorize]
attribute to the controller or action. - The app securely calls a (web) API in the server project for weather data:
- When rendering the
Weather
component on the server, the component uses theServerWeatherForecaster
on the server to obtain weather data directly (not via a web API call). - When the component is rendered on the client, the component uses the
ClientWeatherForecaster
service implementation, which uses a preconfigured HttpClient (in the client project'sProgram
file) to make a web API call to the server project. A Minimal API endpoint (/weather-forecast
) defined in the server project'sProgram
file obtains the weather data from theServerWeatherForecaster
and returns the data to the client.
- When rendering the
- Automatic non-interactive token refresh with the help of a custom cookie refresher (
CookieOidcRefresher.cs
). - The
PersistingAuthenticationStateProvider
class (PersistingAuthenticationStateProvider.cs
) is a server-side AuthenticationStateProvider that uses PersistentComponentState to flow the authentication state to the client, which is then fixed for the lifetime of the WebAssembly application. - An example requests to the Blazor Web App for weather data is handled by a Minimal API endpoint (
/weather-forecast
) in theProgram
file (Program.cs
). The endpoint requires authorization by calling RequireAuthorization. For any controllers that you add to the project, add the[Authorize]
attribute to the controller or action. - The app securely calls a (web) API in the server project for weather data:
- When rendering the
Weather
component on the server, the component uses theServerWeatherForecaster
on the server to obtain weather data directly (not via a web API call). - When the component is rendered on the client, the component uses the
ClientWeatherForecaster
service implementation, which uses a preconfigured HttpClient (in the client project'sProgram
file) to make a web API call to the server project. A Minimal API endpoint (/weather-forecast
) defined in the server project'sProgram
file obtains the weather data from theServerWeatherForecaster
and returns the data to the client.
- When rendering the
For more information on (web) API calls using a service abstractions in Blazor Web Apps, see Call a web API from an ASP.NET Core Blazor app.
Client-side Blazor Web App project (BlazorWebAppOidc.Client
)
The BlazorWebAppOidc.Client
project is the client-side project of the Blazor Web App.
The client calls AddAuthenticationStateDeserialization
to deserialize and use the authentication state passed by the server. The authentication state is fixed for the lifetime of the WebAssembly application.
The PersistentAuthenticationStateProvider
class (PersistentAuthenticationStateProvider.cs
) is a client-side 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.
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. It doesn't include tokens that authenticate to the server when making subsequent requests, which works separately using a cookie that's included on HttpClient requests to the server.
This version of the article covers implementing OIDC with the Backend for Frontend (BFF) pattern. Change the article version selector to OIDC without BFF pattern if the app's specification doesn't call for adopting the BFF pattern.
The following specification is covered:
- 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.
- The Backend for Frontend (BFF) pattern is adopted using .NET 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.
For more information on .NET Aspire, see General Availability of .NET Aspire: Simplifying .NET Cloud-Native Development (May, 2024).
Prerequisite
.NET Aspire requires Visual Studio version 17.10 or later.
Sample app
The sample app consists of five projects:
- .NET Aspire:
Aspire.AppHost
: Used to manage the high level orchestration concerns of the app.Aspire.ServiceDefaults
: Contains default .NET 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.BlazorWebAppOidc.Client
: Client-side project of the Blazor Web App.
Access the sample apps through the latest version folder from the repository's root with the following link. The projects are in the BlazorWebAppOidcBff
folder for .NET 8 or later.
View or download sample code (how to download)
.NET Aspire projects
For more information on using .NET Aspire and details on the .AppHost
and .ServiceDefaults
projects of the sample app, see the .NET Aspire documentation.
Confirm that you've met the prerequisites for .NET Aspire. For more information, see the Prerequisites section of Quickstart: Build your first .NET Aspire app.
The sample app only configures an insecure HTTP launch profile (http
) for use during development testing. For more information, including an example of insecure and secure launch settings profiles, see Allow unsecure transport in .NET Aspire (.NET Aspire documentation).
Server-side Blazor Web App project (BlazorWebAppOidc
)
The BlazorWebAppOidc
project is the 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 the access_token
stored in the authentication cookie.
The BlazorWebAppOidc.http
file can be used for testing the weather data request. Note that the BlazorWebAppOidc
project must be running to test the endpoint, and the endpoint is hardcoded into the file. For more information, see Use .http files in Visual Studio 2022.
Note
The server project uses IHttpContextAccessor/HttpContext, but never for interactively-rendered components. For more information, see Threat mitigation guidance for ASP.NET Core Blazor interactive server-side rendering.
Configuration
This section explains how to configure the sample app.
Note
For Microsoft Entra ID or Azure AD B2C, you can use AddMicrosoftIdentityWebApp 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 section doesn'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 the linked resources.
Establish the client secret
Warning
Don't store app secrets, connection strings, credentials, passwords, personal identification numbers (PINs), private C#/.NET code, or private keys/tokens in client-side code, which is always insecure. In test/staging and production environments, server-side Blazor code and web APIs should use secure authentication flows that avoid maintaining credentials within project code or configuration files. Outside of local development testing, we recommend avoiding the use of environment variables to store sensitive data, as environment variables aren't the most secure approach. For local development testing, the Secret Manager tool is recommended for securing sensitive data. For more information, see Securely maintain sensitive data and credentials.
For local development testing, use the Secret Manager tool to store the server app's client secret under the configuration key Authentication:Schemes:MicrosoftOidc:ClientSecret
.
Note
If the app uses Microsoft Entra ID or Azure AD B2C, create a client secret in the app's registration in the Entra or Azure portal (Manage > Certificates & secrets > New client secret). Use the Value of the new secret in the following guidance.
The sample 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):
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.
Configure the app
The following OpenIdConnectOptions configuration is found in the project's Program
file on the call to AddOpenIdConnect:
SignInScheme: 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, DefaultSignInScheme is used as a fallback value.
oidcOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
Scopes for
openid
andprofile
(Scope) (Optional): Theopenid
andprofile
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 theAuthentication:Schemes:MicrosoftOidc:Scope
configuration. For general configuration guidance, see Configuration in ASP.NET Core and ASP.NET Core Blazor configuration.oidcOptions.Scope.Add(OpenIdConnectScope.OpenIdProfile);
SaveTokens: Defines whether access and refresh tokens should be stored in the AuthenticationProperties after a successful authorization. The value is set to
true
to authenticate requests for weather data from the backend web API project (MinimalApiJwt
).oidcOptions.SaveTokens = true;
Scope for offline access (Scope): The
offline_access
scope is required for the refresh token.oidcOptions.Scope.Add(OpenIdConnectScope.OfflineAccess);
Scopes for obtaining weather data from the web API (Scope): The
Weather.Get
scope is configured in the Azure or Entra portal under Expose an API. This is necessary for backend web API project (MinimalApiJwt
) to validate the access token with bearer JWT.oidcOptions.Scope.Add("{APP ID URI}/{API NAME}");
Example:
- App ID URI (
{APP ID URI}
):https://{DIRECTORY NAME}.onmicrosoft.com/{CLIENT ID}
- Directory Name (
{DIRECTORY NAME}
):contoso
- Application (Client) Id (
{CLIENT ID}
):00001111-aaaa-2222-bbbb-3333cccc4444
- Directory Name (
- Scope configured for weather data from
MinimalApiJwt
({API NAME}
):Weather.Get
oidcOptions.Scope.Add("https://contoso.onmicrosoft.com/00001111-aaaa-2222-bbbb-3333cccc4444/Weather.Get");
The preceding example pertains to an app registered in a tenant with an AAD B2C tenant type. If the app is registered in an ME-ID tenant, the App ID URI is different, thus the scope is different.
Example:
- App ID URI (
{APP ID URI}
):api://{CLIENT ID}
with Application (Client) Id ({CLIENT ID}
):00001111-aaaa-2222-bbbb-3333cccc4444
- Scope configured for weather data from
MinimalApiJwt
({API NAME}
):Weather.Get
oidcOptions.Scope.Add("api://00001111-aaaa-2222-bbbb-3333cccc4444/Weather.Get");
- App ID URI (
Authority and ClientId: Sets the Authority and Client ID for OIDC calls.
oidcOptions.Authority = "{AUTHORITY}"; oidcOptions.ClientId = "{CLIENT ID}";
Example:
- Authority (
{AUTHORITY}
):https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0/
(uses Tenant IDaaaabbbb-0000-cccc-1111-dddd2222eeee
) - Client Id (
{CLIENT ID}
):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";
Example for Microsoft Azure "common" authority:
The "common" authority should be used for multi-tenant apps. You can also use the "common" authority for single-tenant apps, but a custom IssuerValidator is required, as shown later in this section.
oidcOptions.Authority = "https://login.microsoftonline.com/common/v2.0/";
- Authority (
ResponseType: Configures the OIDC handler to only perform authorization code flow. Implicit grants and hybrid flows are unnecessary in this mode.
In the Entra or Azure portal's Implicit grant and hybrid flows app registration configuration, do not select either checkbox for the authorization endpoint to return Access tokens or ID tokens. The OIDC handler automatically requests the appropriate tokens using the code returned from the authorization endpoint.
oidcOptions.ResponseType = OpenIdConnectResponseType.Code;
MapInboundClaims and configuration of NameClaimType and RoleClaimType: Many OIDC servers use "
name
" and "role
" rather than the SOAP/WS-Fed defaults in ClaimTypes. When MapInboundClaims is set tofalse
, 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
MapInboundClaims 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
.CallbackPath: The request path within the app's base path where the user-agent is returned.
In the Entra or Azure portal, set the path in the Web platform configuration's Redirect URI:
https://localhost/signin-oidc
Note
A port isn't required for
localhost
addresses.SignedOutCallbackPath: The request path within the app's base path where the user agent is returned after sign out from the identity provider.
In the Entra or Azure portal, set the path in the Web platform configuration's Redirect URI:
https://localhost/signout-callback-oidc
Note
A port isn't required for
localhost
addresses.Note
If using Microsoft Identity Web, the provider currently only redirects back to the SignedOutCallbackPath if the
microsoftonline.com
Authority (https://login.microsoftonline.com/{TENANT ID}/v2.0/
) is used. This limitation doesn't exist if you can use the "common" Authority with Microsoft Identity Web. For more information, see postLogoutRedirectUri not working when authority url contains a tenant ID (AzureAD/microsoft-authentication-library-for-js
#5783).RemoteSignOutPath: Requests received on this path cause the handler to invoke sign-out using the sign-out scheme.
In the Entra or Azure portal, set the Front-channel logout URL:
https://localhost/signout-oidc
Note
A port isn't required for
localhost
addresses.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) TokenValidationParameters.IssuerValidator: 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 byhttps://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;
Sample app code
Inspect the sample app for the following features:
- Automatic non-interactive token refresh with the help of a custom cookie refresher (
CookieOidcRefresher.cs
). - The server project calls
AddAuthenticationStateSerialization
to add a server-side authentication state provider that uses PersistentComponentState to flow the authentication state to the client. The client callsAddAuthenticationStateDeserialization
to deserialize and use the authentication state passed by the server. The authentication state is fixed for the lifetime of the WebAssembly application. - Requests to the Blazor Web App are proxied to the backend web API project (
MinimalApiJwt
).MapForwarder
in theProgram
file 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
Weather
component on the server, the component uses theServerWeatherForecaster
to proxy the request for weather data with the user's access token. - When the component is rendered on the client, the component uses the
ClientWeatherForecaster
service implementation, which uses a preconfigured HttpClient (in the client project'sProgram
file) to make a web API call to the server project. A Minimal API endpoint (/weather-forecast
) defined in the server project'sProgram
file transforms the request with the user's access token to obtain the weather data.
- When rendering the
- Automatic non-interactive token refresh with the help of a custom cookie refresher (
CookieOidcRefresher.cs
). - The
PersistingAuthenticationStateProvider
class (PersistingAuthenticationStateProvider.cs
) is a server-side AuthenticationStateProvider that uses PersistentComponentState to flow the authentication state to the client, which is then fixed for the lifetime of the WebAssembly application. - Requests to the Blazor Web App are proxied to the backend web API project (
MinimalApiJwt
).MapForwarder
in theProgram
file 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
Weather
component on the server, the component uses theServerWeatherForecaster
to proxy the request for weather data with the user's access token. - When the component is rendered on the client, the component uses the
ClientWeatherForecaster
service implementation, which uses a preconfigured HttpClient (in the client project'sProgram
file) to make a web API call to the server project. A Minimal API endpoint (/weather-forecast
) defined in the server project'sProgram
file transforms the request with the user's access token to obtain the weather data.
- When rendering the
For more information on (web) API calls using a service abstractions in Blazor Web Apps, see Call a web API from an ASP.NET Core Blazor app.
Client-side Blazor Web App project (BlazorWebAppOidc.Client
)
The BlazorWebAppOidc.Client
project is the client-side project of the Blazor Web App.
The client calls AddAuthenticationStateDeserialization
to deserialize and use the authentication state passed by the server. The authentication state is fixed for the lifetime of the WebAssembly application.
The PersistentAuthenticationStateProvider
class (PersistentAuthenticationStateProvider.cs
) is a client-side 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.
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. It doesn't include tokens that authenticate to the server when making subsequent requests, which works separately using a cookie that's included on HttpClient requests to the server.
Backend web API project (MinimalApiJwt
)
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.
Configuration
Configure the project in the JwtBearerOptions of the AddJwtBearer call in the project's Program
file:
Audience: Sets the Audience for any received OpenID Connect token.
In the Azure or Entra portal: Match the value to just the path of the Application ID URI configured when adding the
Weather.Get
scope under Expose an API:jwtOptions.Audience = "{APP ID URI}";
Example:
App ID URI (
{APP ID URI}
):https://{DIRECTORY NAME}.onmicrosoft.com/{CLIENT ID}
:- Directory Name (
{DIRECTORY NAME}
):contoso
- Application (Client) Id (
{CLIENT ID}
):00001111-aaaa-2222-bbbb-3333cccc4444
jwtOptions.Audience = "https://contoso.onmicrosoft.com/00001111-aaaa-2222-bbbb-3333cccc4444";
The preceding example pertains to an app registered in a tenant with an AAD B2C tenant type. If the app is registered in an ME-ID tenant, the App ID URI is different, thus the audience is different.
Example:
App ID URI (
{APP ID URI}
):api://{CLIENT ID}
with Application (Client) Id ({CLIENT ID}
):00001111-aaaa-2222-bbbb-3333cccc4444
jwtOptions.Audience = "api://00001111-aaaa-2222-bbbb-3333cccc4444";
- Directory Name (
Authority: Sets the Authority for making OpenID Connect calls. Match the value to the Authority configured for the OIDC handler in
BlazorWebAppOidc/Program.cs
:jwtOptions.Authority = "{AUTHORITY}";
Example:
Authority (
{AUTHORITY}
):https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0/
(uses Tenant IDaaaabbbb-0000-cccc-1111-dddd2222eeee
)jwtOptions.Authority = "https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/v2.0/";
The preceding example pertains to an app registered in a tenant with an AAD B2C tenant type. 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:jwtOptions.Authority = "https://sts.windows.net/aaaabbbb-0000-cccc-1111-dddd2222eeee/";
Minimal API for weather data
Secure weather forecast data endpoint 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 RequireAuthorization 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.
Adding components that adopt interactive server-side rendering
Because the app uses global Interactive Auto rendering via the Routes
component, individual components that specify interactive server-side rendering (interactive SSR, @rendermode InteractiveServer
) in their component definition file (.razor
) are placed in the .Client
project's Pages
folder.
Placing interactive SSR components in the .Client
project is counter-intuitive because such components are only rendered on the server.
If you place an interactive SSR component in the server project's Components/Pages
folder, the component is prerendered normally and briefly displayed in the user's browser. However, the client-side router isn't able to find the component, ultimately resulting in a 404 - Not Found in the browser.
Therefore, place interactive SSR components in the .Client
project's Pages
folder.
Redirect to the home page on signout
When a user navigates around the app, the LogInOrOut
component (Layout/LogInOrOut.razor
) sets a hidden field for the return URL (ReturnUrl
) to the value of the current URL (currentURL
). When the user signs out of the app, the identity provider returns them to the page from which they signed out.
If the user signs out from a secure page, they're returned back to the same secure page after signing out only to be sent back through the authentication process. This behavior is fine when users need to switch accounts frequently. However, a alternative app specification may call for the user to be returned to the app's home page or some other page after signout. The following example shows how to set the app's home page as the return URL for signout operations.
The important changes to the LogInOrOut
component are demonstrated in the following example. There's no need to provide a hidden field for the ReturnUrl
set to the home page at /
because that's the default path. IDisposable is no longer implemented. The NavigationManager is no longer injected. The entire @code
block is removed.
Layout/LogInOrOut.razor
:
@using Microsoft.AspNetCore.Authorization
<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 @context.User.Identity?.Name
</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>
Cryptographic nonce
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 OpenIdConnectProtocolValidator.RequireNonce to false
.
Application roles for apps not registered with Microsoft Entra (ME-ID)
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 (TokenValidationParameters.RoleClaimType) in the 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.
Application roles for apps registered with Microsoft Entra (ME-ID)
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 (TokenValidationParameters.RoleClaimType) in 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
groupMembershipClaims
attribute. - 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
roles
claims, one claim per role. - Security groups appear in
groups
claims, 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
wids
claims, one claim per role. Thewids
claim with a value ofb79fbf4d-3ef9-4689-8143-76b194e85509
is 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).
Troubleshoot
Logging
The server app is a standard ASP.NET Core app. See the ASP.NET Core logging guidance to enable a lower logging level in the server app.
To enable debug or trace logging for Blazor WebAssembly authentication, see the Client-side authentication logging section of ASP.NET Core Blazor logging with the article version selector set to ASP.NET Core 7.0 or later.
Common errors
Misconfiguration of the app or Identity Provider (IP)
The most common errors are caused by incorrect configuration. The following are a few examples:
- Depending on the requirements of the scenario, a missing or incorrect Authority, Instance, Tenant ID, Tenant domain, Client ID, or Redirect URI prevents an app from authenticating clients.
- Incorrect request scopes prevent clients from accessing server web API endpoints.
- Incorrect or missing server API permissions prevent clients from accessing server web API endpoints.
- Running the app at a different port than is configured in the Redirect URI of the IP's app registration. Note that a port isn't required for Microsoft Entra ID and an app running at a
localhost
development testing address, but the app's port configuration and the port where the app is running must match for non-localhost
addresses.
Configuration coverage in this article shows examples of the correct configuration. Carefully check the configuration looking for app and IP misconfiguration.
If the configuration appears correct:
Analyze application logs.
Examine the network traffic between the client app and the IP or server app with the browser's developer tools. Often, an exact error message or a message with a clue to what's causing the problem is returned to the client by the IP or server app after making a request. Developer tools guidance is found in the following articles:
- Google Chrome (Google documentation)
- Microsoft Edge
- Mozilla Firefox (Mozilla documentation)
The documentation team responds to document feedback and bugs in articles (open an issue from the This page feedback section) but is unable to provide product support. Several public support forums are available to assist with troubleshooting an app. We recommend the following:
The preceding forums are not owned or controlled by Microsoft.
For non-security, non-sensitive, and non-confidential reproducible framework bug reports, open an issue with the ASP.NET Core product unit. Don't open an issue with the product unit until you've thoroughly investigated the cause of a problem and can't resolve it on your own and with the help of the community on a public support forum. The product unit isn't able to troubleshoot individual apps that are broken due to simple misconfiguration or use cases involving third-party services. If a report is sensitive or confidential in nature or describes a potential security flaw in the product that cyberattackers may exploit, see Reporting security issues and bugs (
dotnet/aspnetcore
GitHub repository).Unauthorized client for ME-ID
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2] Authorization failed. These requirements were not met: DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
Login callback error from ME-ID:
- Error:
unauthorized_client
- Description:
AADB2C90058: The provided application is not configured to allow public clients.
To resolve the error:
- In the Azure portal, access the app's manifest.
- Set the
allowPublicClient
attribute tonull
ortrue
.
- Error:
Cookies and site data
Cookies and site data can persist across app updates and interfere with testing and troubleshooting. Clear the following when making app code changes, user account changes with the provider, or provider app configuration changes:
- User sign-in cookies
- App cookies
- Cached and stored site data
One approach to prevent lingering cookies and site data from interfering with testing and troubleshooting is to:
- Configure a browser
- Use a browser for testing that you can configure to delete all cookie and site data each time the browser is closed.
- Make sure that the browser is closed manually or by the IDE for any change to the app, test user, or provider configuration.
- Use a custom command to open a browser in InPrivate or Incognito mode in Visual Studio:
- Open Browse With dialog box from Visual Studio's Run button.
- Select the Add button.
- Provide the path to your browser in the Program field. The following executable paths are typical installation locations for Windows 10. If your browser is installed in a different location or you aren't using Windows 10, provide the path to the browser's executable.
- Microsoft Edge:
C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe
- Google Chrome:
C:\Program Files (x86)\Google\Chrome\Application\chrome.exe
- Mozilla Firefox:
C:\Program Files\Mozilla Firefox\firefox.exe
- Microsoft Edge:
- In the Arguments field, provide the command-line option that the browser uses to open in InPrivate or Incognito mode. Some browsers require the URL of the app.
- Microsoft Edge: Use
-inprivate
. - Google Chrome: Use
--incognito --new-window {URL}
, where the{URL}
placeholder is the URL to open (for example,https://localhost:5001
). - Mozilla Firefox: Use
-private -url {URL}
, where the{URL}
placeholder is the URL to open (for example,https://localhost:5001
).
- Microsoft Edge: Use
- Provide a name in the Friendly name field. For example,
Firefox Auth Testing
. - Select the OK button.
- To avoid having to select the browser profile for each iteration of testing with an app, set the profile as the default with the Set as Default button.
- Make sure that the browser is closed by the IDE for any change to the app, test user, or provider configuration.
App upgrades
A functioning app may fail immediately after upgrading either the .NET Core SDK on the development machine or changing package versions within the app. In some cases, incoherent packages may break an app when performing major upgrades. Most of these issues can be fixed by following these instructions:
- Clear the local system's NuGet package caches by executing
dotnet nuget locals all --clear
from a command shell. - Delete the project's
bin
andobj
folders. - Restore and rebuild the project.
- Delete all of the files in the deployment folder on the server prior to redeploying the app.
Note
Use of package versions incompatible with the app's target framework isn't supported. For information on a package, use the NuGet Gallery or FuGet Package Explorer.
Run the server app
When testing and troubleshooting Blazor Web App, make sure that you're running the app from the server project.
Inspect the user
The following UserClaims
component can be used directly in apps or serve as the basis for further customization.
UserClaims.razor
:
@page "/user-claims"
@using System.Security.Claims
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
<PageTitle>User Claims</PageTitle>
<h1>User Claims</h1>
@if (claims.Any())
{
<ul>
@foreach (var claim in claims)
{
<li><b>@claim.Type:</b> @claim.Value</li>
}
</ul>
}
@code {
private IEnumerable<Claim> claims = Enumerable.Empty<Claim>();
[CascadingParameter]
private Task<AuthenticationState>? AuthState { get; set; }
protected override async Task OnInitializedAsync()
{
if (AuthState == null)
{
return;
}
var authState = await AuthState;
claims = authState.User.Claims;
}
}
Additional resources
AzureAD/microsoft-identity-web
GitHub 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.AuthenticationStateProvider
service- Manage authentication state in Blazor Web Apps
- Refresh token during http request in Blazor Interactive Server with OIDC (
dotnet/aspnetcore
#55213)