Secure an ASP.NET Core Blazor Web App with OpenID Connect (OIDC) (original) (raw)

Important

This information relates to a pre-release product that may be substantially modified before it's commercially released. Microsoft makes no warranties, express or implied, with respect to the information provided here.

For the current release, see the .NET 9 version of this article.

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 with an app that adopts global Interactive Auto rendering (server and .Client projects). The BFF pattern is useful for making authenticated requests to external services. Change the article version selector to BFF pattern if the app's specification calls for adopting the BFF pattern.

The following specification is adopted:

For an alternative experience using Microsoft Authentication Library for .NET, Microsoft Identity Web, and Microsoft Entra ID, see Secure an ASP.NET Core Blazor Web App with Microsoft Entra ID.

Sample solution

The sample app consists of the following projects:

Access the sample through the latest version folder in the Blazor samples repository with the following link. The sample is in the BlazorWebAppOidc folder for .NET 8 or later.

Start the solution from the Aspire/Aspire.AppHost project.

View or download sample code (how to download)

Sample solution features:

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.

Microsoft Entra ID app 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.

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/BlazorWebApOidc.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.

Establish the client secret

This section only applies to the server project of the Blazor Web App (BlazorWebAppOidc project).

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 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.

MinimalApiJwt project

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 Use .http files in Visual Studio 2022.

The project includes packages and configuration to produce OpenAPI documents and the Swagger UI in the Development environment. For more information, see Use the generated OpenAPI documents.

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 JwtBearerOptions of the AddJwtBearer call in the project's Program file.

The Authority 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 Audience 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";

Blazor Web App server project (BlazorWebAppOidc)

The BlazorWebAppOidc project is the server-side project of the Blazor Web App.

A 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 HttpContext is safe in this scenario. For more information, see IHttpContextAccessor/HttpContext in ASP.NET Core Blazor apps and ASP.NET Core server-side and Blazor Web App additional security scenarios.

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 AddHttpMessageHandler 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 OpenIdConnectOptions configuration is found in the project's Program file on the call to AddOpenIdConnect:

PushedAuthorizationBehavior: 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;

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 and profile (Scope) (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 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 true so the refresh token gets stored for non-interactive token refresh.

oidcOptions.SaveTokens = true;

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}";

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 IssuerValidator is required, as shown later in this section.

oidcOptions.Authority = "https://login.microsoftonline.com/common/v2.0/";

ResponseType: 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;

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 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

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.

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:

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.

SignedOutCallbackPath (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 SignedOutRedirectUri or RedirectUri, 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:

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.

RemoteSignOutPath: 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:

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) 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 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;

Blazor Web App client 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.

This version of the article covers implementing OIDC without adopting the Backend for Frontend (BFF) pattern with an app that adopts global Interactive Server rendering (single project). The BFF pattern is useful for making authenticated requests to external services. Change the article version selector to BFF pattern if the app's specification calls for adopting the BFF pattern with global Interactive Auto rendering.

The following specification is adopted:

For an alternative experience using Microsoft Authentication Library for .NET, Microsoft Identity Web, and Microsoft Entra ID, see Secure an ASP.NET Core Blazor Web App with Microsoft Entra ID.

Sample solution

The sample app consists of the following projects:

Access the sample through the latest version folder in the Blazor samples repository with the following link. The sample is in the BlazorWebAppOidcServer folder for .NET 8 or later.

View or download sample code (how to download)

Microsoft Entra ID app 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 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.

Establish the client secret

This section only applies to the server project of the Blazor Web App (BlazorWebAppOidcServer project).

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 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.

MinimalApiJwt project

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 Use .http files in Visual Studio 2022.

The project includes packages and configuration to produce OpenAPI documents and the Swagger UI in the Development environment. For more information, see Use the generated OpenAPI documents.

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 JwtBearerOptions of the AddJwtBearer call in the project's Program file.

The Authority 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 Audience 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";

BlazorWebAppOidcServer project

Automatic non-interactive token refresh is managed by a custom cookie refresher (CookieOidcRefresher.cs).

A 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 HttpContext is safe in this scenario. For more information, see IHttpContextAccessor/HttpContext in ASP.NET Core Blazor apps and ASP.NET Core server-side and Blazor Web App additional security scenarios.

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 AddHttpMessageHandler 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 OpenIdConnectOptions configuration is found in the project's Program file on the call to AddOpenIdConnect:

PushedAuthorizationBehavior: 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;

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 and profile (Scope) (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 Configuration in ASP.NET Core and ASP.NET Core Blazor 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");

SaveTokens: Defines whether access and refresh tokens should be stored in the 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 (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}";

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 IssuerValidator is required, as shown later in this section.

oidcOptions.Authority = "https://login.microsoftonline.com/common/v2.0/";

ResponseType: 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;

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 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

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.

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:

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.

SignedOutCallbackPath (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 SignedOutRedirectUri or RedirectUri, 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:

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.

RemoteSignOutPath: 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:

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) 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 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;

This version of the article covers implementing OIDC with the Backend for Frontend (BFF) pattern. If the app's specification doesn't call for adopting the BFF pattern, change the article version selector to Non-BFF pattern (Interactive Auto) (Interactive Auto rendering) or Non-BFF pattern (Interactive Server) (Interactive Server rendering).

Prerequisites

.NET Aspire requires Visual Studio version 17.10 or later.

Also, see the Prerequisites section of Quickstart: Build your first .NET Aspire app.

Sample solution

The sample app consists of the following projects:

Access the sample through the latest version folder in the Blazor samples repository with the following link. The sample is in the BlazorWebAppOidcBff folder for .NET 8 or later.

View or download sample code (how to download)

The Blazor Web App uses the Auto render mode with global interactivity.

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 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 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.

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 .NET 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:

For more information on .NET Aspire, see General Availability of .NET Aspire: Simplifying .NET Cloud-Native Development (May, 2024).

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.

Microsoft Entra ID app 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.

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/BlazorWebApOidc.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.

Establish the client secret

This section only applies to the server project of the Blazor Web App (BlazorWebAppOidc project).

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 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.

.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).

MinimalApiJwt project

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 Use .http files in Visual Studio 2022.

The project includes packages and configuration to produce OpenAPI documents and the Swagger UI in the Development environment. For more information, see Use the generated OpenAPI documents.

A secure weather forecast data endpoint is in the project's Program file:

app.MapGet("/weather-forecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
}).RequireAuthorization();

The 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.

Configure the project in the JwtBearerOptions of the AddJwtBearer call in the project's Program file.

The Authority 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 Audience 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";

Server-side Blazor Web App project (BlazorWebAppOidc)

This section explains how to configure the server-side Blazor project.

The following OpenIdConnectOptions configuration is found in the project's Program file on the call to AddOpenIdConnect.

PushedAuthorizationBehavior: 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;

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 and profile (Scope) (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 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): 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");

Authority and ClientId: 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 IssuerValidator is required, as shown later in this section.

oidcOptions.Authority = "https://login.microsoftonline.com/common/v2.0/";

ResponseType: 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;

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 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

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.

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:

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.

SignedOutCallbackPath (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 SignedOutRedirectUri or RedirectUri, 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:

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.

RemoteSignOutPath: 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:

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) 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 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;

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.

Only serialize the name and role claims

This section only applies to the non-BFF pattern (Interactive Auto) and BFF pattern (Interactive Auto) and their sample apps.

In the Program file, all claims are serialized by setting SerializeAllClaims to true. If you only want the name and role claims serialized for CSR, remove the option or set it to false.

Supply configuration with the JSON configuration provider (app settings)

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:

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:

Authority formats adopt the following patterns:

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):

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:

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 BlazorWebAppOidc, BlazorWebAppOidcServer, or BlazorWebAppOidcBff project.

At the top of the Program file, make the Microsoft.IdentityModel.Validators 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).

Redirect to the home page on logout

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>

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:

  1. Open the app's registration in the Azure portal.
  2. Select Manage > Manifest in the sidebar.
  3. Find the groupMembershipClaims attribute.
  4. Set the value to All ("groupMembershipClaims": "All").
  5. 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:

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 in .NET 7 or later.

Common errors

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:

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:

One approach to prevent lingering cookies and site data from interfering with testing and troubleshooting is to:

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:

  1. Clear the local system's NuGet package caches by executing dotnet nuget locals all --clear from a command shell.
  2. Delete the project's bin and obj folders.
  3. Restore and rebuild the project.
  4. 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.

Start the solution from the correct project

Blazor Web Apps:

Blazor Server:

Start the solution 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