Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Note
This isn't the latest version of this article. For the current release, see the .NET 10 version of this article.
Warning
This version of ASP.NET Core is no longer supported. For more information, see the .NET and .NET Core Support Policy. For the current release, see the .NET 10 version of this article.
This article explains how to configure server-side Blazor for additional security scenarios, including how to pass tokens to a Blazor app.
Note
The code examples in this article adopt nullable reference types (NRTs) and .NET compiler null-state static analysis, which are supported in ASP.NET Core in .NET 6 or later. When targeting .NET 5 or earlier, remove the null type designation (?) from the string?, TodoItem[]?, WeatherForecast[]?, and IEnumerable<GitHubBranch>? types in the article's examples.
Pass tokens to a server-side Blazor app
This section applies to Blazor Web Apps. For Blazor Server, view the .NET 7 version of this article section.
If you merely want to use access tokens to make web API calls from a Blazor Web App with a named HTTP client, see the Use a token handler for web API calls section, which explains how to use a DelegatingHandler implementation to attach a user's access token to outgoing requests. The following guidance in this section is for developers who need access tokens, refresh tokens, and other authentication properties server-side for other purposes.
Note
For more information on DelegatingHandler instances, see HTTP requests with IHttpClientFactory - ASP.NET Core.
To save tokens and other authentication properties for server-side use in Blazor Web Apps, we recommend using IHttpContextAccessor/HttpContext (IHttpContextAccessor, HttpContext). Reading tokens from HttpContext, including as a cascading parameter, using IHttpContextAccessor is supported for obtaining tokens for use during interactive server rendering if the tokens are obtained during static server-side rendering (static SSR) or prerendering. However, tokens aren't updated if the user authenticates after the circuit is established, since the HttpContext is captured at the start of the SignalR connection. Also, the use of AsyncLocal<T> by IHttpContextAccessor means that you must be careful not to lose the execution context before reading the HttpContext. For more information, see IHttpContextAccessor/HttpContext in ASP.NET Core Blazor apps.
In a service class, obtain access to the members of the namespace Microsoft.AspNetCore.Authentication to surface the GetTokenAsync method on HttpContext. An alternative approach, which is commented out in the following example, is to call AuthenticateAsync on HttpContext. For the returned AuthenticateResult.Properties, call GetTokenValue.
using Microsoft.AspNetCore.Authentication;
public class AuthenticationProcessor(IHttpContextAccessor httpContextAccessor)
{
public async Task<string?> GetAccessToken()
{
if (httpContextAccessor.HttpContext is null)
{
throw new Exception("HttpContext not available");
}
// Approach 1: Call 'GetTokenAsync'
var accessToken = await httpContextAccessor.HttpContext
.GetTokenAsync("access_token");
// Approach 2: Authenticate the user and call 'GetTokenValue'
/*
var authResult = await httpContextAccessor.HttpContext.AuthenticateAsync();
var accessToken = authResult?.Properties?.GetTokenValue("access_token");
*/
return accessToken;
}
}
The service is registered in the server project's Program file:
builder.Services.AddScoped<AuthenticationProcessor>();
AuthenticationProcessor can be injected into server-side services, for example in a DelegatingHandler for a preconfigured HttpClient. The following example is only for demonstration purposes or in case you need to perform special processing in the AuthenticationProcessor service because you can just inject IHttpContextAccessor and obtain the token directly for calling external web APIs (for more information on using IHttpContextAccessor directly to call web APIs, see the Use a token handler for web API calls section).
using System.Net.Http.Headers;
public class TokenHandler(AuthenticationProcessor authProcessor) :
DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var accessToken = authProcessor.GetAccessToken();
request.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);
return await base.SendAsync(request, cancellationToken);
}
}
The token handler is registered and acts as the delegating handler for a named HTTP client in the Program file:
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<TokenHandler>();
builder.Services.AddHttpClient("ExternalApi",
client => client.BaseAddress = new Uri(builder.Configuration["ExternalApiUri"] ??
throw new Exception("Missing base address!")))
.AddHttpMessageHandler<TokenHandler>();
Caution
Ensure that tokens are never transmitted and handled by the client (the .Client project), for example, in a component that adopts Interactive Auto rendering and is rendered on the client or by a client-side service. Always have the client call the server (project) to process requests with tokens. Tokens and other authentication data should never leave the server.
For Interactive Auto components, see ASP.NET Core Blazor authentication and authorization, which demonstrates how to leave access tokens and other authentication properties on the server. Also, consider adopting the Backend-for-Frontend (BFF) pattern, which adopts a similar call structure and is described in Secure an ASP.NET Core Blazor Web App with OpenID Connect (OIDC) for OIDC providers and Secure an ASP.NET Core Blazor Web App with Microsoft Entra ID for Microsoft Identity Web with Entra.
Use a token handler for web API calls
The following approach is aimed at attaching a user's access token to outgoing requests, specifically to make web API calls to external web API apps. The approach is shown for a Blazor Web App that adopts global Interactive Server rendering, but the same general approach applies to Blazor Web Apps that adopt the global Interactive Auto render mode. The important concept to keep in mind is that accessing the HttpContext using IHttpContextAccessor is only performed on the server.
For a demonstration of the guidance in this section, see the BlazorWebAppOidc and BlazorWebAppOidcServer sample apps (.NET 8 or later) in the Blazor samples GitHub repository. The samples adopt a global interactive render mode and OIDC authentication with Microsoft Entra without using Entra-specific packages. The samples demonstrate how to pass a JWT access token to call a secure web API.
Microsoft identity platform with Microsoft Identity Web packages for Microsoft Entra ID provides a API to call web APIs from Blazor Web Apps with automatic token management and renewal. For more information, see Secure an ASP.NET Core Blazor Web App with Microsoft Entra ID and the BlazorWebAppEntra and BlazorWebAppEntraBff sample apps (.NET 9 or later) in the Blazor samples GitHub repository.
Subclass DelegatingHandler to attach a user's access token to outgoing requests. The token handler only executes on the server, so using HttpContext is safe.
TokenHandler.cs:
using System.Net.Http.Headers;
using Microsoft.AspNetCore.Authentication;
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");
if (accessToken is null)
{
throw new Exception("No access token");
}
request.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);
return await base.SendAsync(request, cancellationToken);
}
}
Note
For guidance on how to access an AuthenticationStateProvider from a DelegatingHandler, see the Access AuthenticationStateProvider in outgoing request middleware section.
In the project's Program file, the token handler (TokenHandler) is registered as a scoped service and specified as a named HTTP client's message handler with AddHttpMessageHandler.
In the following example, the {HTTP CLIENT NAME} placeholder is the name of the HttpClient, and the {BASE ADDRESS} placeholder is the web API's base address URI. For more information on AddHttpContextAccessor, see IHttpContextAccessor/HttpContext in ASP.NET Core Blazor apps.
In Program.cs:
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<TokenHandler>();
builder.Services.AddHttpClient("{HTTP CLIENT NAME}",
client => client.BaseAddress = new Uri("{BASE ADDRESS}"))
.AddHttpMessageHandler<TokenHandler>();
Example:
builder.Services.AddScoped<TokenHandler>();
builder.Services.AddHttpClient("ExternalApi",
client => client.BaseAddress = new Uri("https://localhost:7277"))
.AddHttpMessageHandler<TokenHandler>();
You can supply the HTTP client base address from configuration with builder.Configuration["{CONFIGURATION KEY}"], where the {CONFIGURATION KEY} placeholder is the configuration key:
new Uri(builder.Configuration["ExternalApiUri"] ?? throw new IOException("No URI!"))
In appsettings.json, specify the ExternalApiUri. The following example sets the value to the localhost address of the external web API to https://localhost:7277:
"ExternalApiUri": "https://localhost:7277"
At this point, an HttpClient created by a component can make secure web API requests. In the following example, the {REQUEST URI} is the relative request URI, and the {HTTP CLIENT NAME} placeholder is the name of the HttpClient:
using var request = new HttpRequestMessage(HttpMethod.Get, "{REQUEST URI}");
var client = ClientFactory.CreateClient("{HTTP CLIENT NAME}");
using var response = await client.SendAsync(request);
Example:
using var request = new HttpRequestMessage(HttpMethod.Get, "/weather-forecast");
var client = ClientFactory.CreateClient("ExternalApi");
using var response = await client.SendAsync(request);
Additional features are planned for Blazor, which are tracked by Access AuthenticationStateProvider in outgoing request middleware (dotnet/aspnetcore #52379). Problem providing Access Token to HttpClient in Interactive Server mode (dotnet/aspnetcore #52390) is a closed issue that contains helpful discussion and potential workaround strategies for advanced use cases.
Tokens available outside of the Razor components in a server-side Blazor app can be passed to components with the approach described in this section. The example in this section focuses on passing access, refresh, and anti-request forgery (XSRF) token tokens to the Blazor app, but the approach is valid for other HTTP context state.
Note
Passing the XSRF token to Razor components is useful in scenarios where components POST to Identity or other endpoints that require validation. If your app only requires access and refresh tokens, you can remove the XSRF token code from the following example.
Authenticate the app as you would with a regular Razor Pages or MVC app. Provision and save the tokens to the authentication cookie.
In the Program file:
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
...
builder.Services.Configure<OpenIdConnectOptions>(
OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.ResponseType = OpenIdConnectResponseType.Code;
options.SaveTokens = true;
options.Scope.Add(OpenIdConnectScope.OfflineAccess);
});
In Startup.cs:
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
...
services.Configure<OpenIdConnectOptions>(
OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.ResponseType = OpenIdConnectResponseType.Code;
options.SaveTokens = true;
options.Scope.Add(OpenIdConnectScope.OfflineAccess);
});
In Startup.cs:
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
...
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.ResponseType = OpenIdConnectResponseType.Code;
options.SaveTokens = true;
options.Scope.Add(OpenIdConnectScope.OfflineAccess);
});
Optionally, additional scopes are added with options.Scope.Add("{SCOPE}");, where the {SCOPE} placeholder is the additional scope to add.
Define a scoped token provider service that can be used within the Blazor app to resolve the tokens from dependency injection (DI).
TokenProvider.cs:
public class TokenProvider
{
public string? AccessToken { get; set; }
public string? RefreshToken { get; set; }
public string? XsrfToken { get; set; }
}
In the Program file, add services for:
- IHttpClientFactory: Used in a
WeatherForecastServiceclass that obtains weather data from a server API with an access token. TokenProvider: Holds the access and refresh tokens.
builder.Services.AddHttpClient();
builder.Services.AddScoped<TokenProvider>();
In Startup.ConfigureServices of Startup.cs, add services for:
- IHttpClientFactory: Used in a
WeatherForecastServiceclass that obtains weather data from a server API with an access token. TokenProvider: Holds the access and refresh tokens.
services.AddHttpClient();
services.AddScoped<TokenProvider>();
Define a class to pass in the initial app state with the access and refresh tokens.
InitialApplicationState.cs:
public class InitialApplicationState
{
public string? AccessToken { get; set; }
public string? RefreshToken { get; set; }
public string? XsrfToken { get; set; }
}
In the Pages/_Host.cshtml file, create and instance of InitialApplicationState and pass it as a parameter to the app:
In the Pages/_Layout.cshtml file, create and instance of InitialApplicationState and pass it as a parameter to the app:
In the Pages/_Host.cshtml file, create and instance of InitialApplicationState and pass it as a parameter to the app:
@using Microsoft.AspNetCore.Authentication
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf
...
@{
var tokens = new InitialApplicationState
{
AccessToken = await HttpContext.GetTokenAsync("access_token"),
RefreshToken = await HttpContext.GetTokenAsync("refresh_token"),
XsrfToken = Xsrf.GetAndStoreTokens(HttpContext).RequestToken
};
}
<component ... param-InitialState="tokens" ... />
In the App component (App.razor), resolve the service and initialize it with the data from the parameter:
@inject TokenProvider TokenProvider
...
@code {
[Parameter]
public InitialApplicationState? InitialState { get; set; }
protected override Task OnInitializedAsync()
{
TokenProvider.AccessToken = InitialState?.AccessToken;
TokenProvider.RefreshToken = InitialState?.RefreshToken;
TokenProvider.XsrfToken = InitialState?.XsrfToken;
return base.OnInitializedAsync();
}
}
Note
An alternative to assigning the initial state to the TokenProvider in the preceding example is to copy the data into a scoped service within OnInitializedAsync for use across the app.
Add a package reference to the app for the Microsoft.AspNet.WebApi.Client NuGet package.
Note
For guidance on adding packages to .NET apps, see the articles under Install and manage packages at Package consumption workflow (NuGet documentation). Confirm correct package versions at NuGet.org.
In the service that makes a secure API request, inject the token provider and retrieve the token for the API request:
WeatherForecastService.cs:
using System;
using System.Net.Http;
using System.Threading.Tasks;
public class WeatherForecastService
{
private readonly HttpClient http;
private readonly TokenProvider tokenProvider;
public WeatherForecastService(IHttpClientFactory clientFactory,
TokenProvider tokenProvider)
{
http = clientFactory.CreateClient();
this.tokenProvider = tokenProvider;
}
public async Task<WeatherForecast[]> GetForecastAsync()
{
var token = tokenProvider.AccessToken;
using var request = new HttpRequestMessage(HttpMethod.Get,
"https://localhost:5003/WeatherForecast");
request.Headers.Add("Authorization", $"Bearer {token}");
using var response = await http.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<WeatherForecast[]>() ??
Array.Empty<WeatherForecast>();
}
}
For an XSRF token passed to a component, inject the TokenProvider and add the XSRF token to the POST request. The following example adds the token to a logout endpoint POST. The scenario for the following example is that the logout endpoint (Areas/Identity/Pages/Account/Logout.cshtml, scaffolded into the app) doesn't specify an IgnoreAntiforgeryTokenAttribute (@attribute [IgnoreAntiforgeryToken]) because it performs some action in addition to a normal logout operation that must be protected. The endpoint requires a valid XSRF token to successfully process the request.
In a component that presents a Logout button to authorized users:
@inject TokenProvider TokenProvider
...
<AuthorizeView>
<Authorized>
<form action="/Identity/Account/Logout?returnUrl=%2F" method="post">
<button class="nav-link btn btn-link" type="submit">Logout</button>
<input name="__RequestVerificationToken" type="hidden"
value="@TokenProvider.XsrfToken">
</form>
</Authorized>
<NotAuthorized>
...
</NotAuthorized>
</AuthorizeView>
Set the authentication scheme
For an app that uses more than one Authentication Middleware and thus has more than one authentication scheme, the scheme that Blazor uses can be explicitly set in the endpoint configuration of the Program file. The following example sets the OpenID Connect (OIDC) scheme:
For an app that uses more than one Authentication Middleware and thus has more than one authentication scheme, the scheme that Blazor uses can be explicitly set in the endpoint configuration of Startup.cs. The following example sets the OpenID Connect (OIDC) scheme:
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
...
app.MapRazorComponents<App>().RequireAuthorization(
new AuthorizeAttribute
{
AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme
})
.AddInteractiveServerRenderMode();
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
...
app.MapBlazorHub().RequireAuthorization(
new AuthorizeAttribute
{
AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme
});
For an app that uses more than one Authentication Middleware and thus has more than one authentication scheme, the scheme that Blazor uses can be explicitly set in the endpoint configuration of Startup.Configure. The following example sets the Microsoft Entra ID scheme:
endpoints.MapBlazorHub().RequireAuthorization(
new AuthorizeAttribute
{
AuthenticationSchemes = AzureADDefaults.AuthenticationScheme
});
Use OpenID Connect (OIDC) v2.0 endpoints
In versions of ASP.NET Core prior to .NET 5, the authentication library and Blazor templates use OpenID Connect (OIDC) v1.0 endpoints. To use a v2.0 endpoint with versions of ASP.NET Core prior to .NET 5, configure the OpenIdConnectOptions.Authority option in the OpenIdConnectOptions:
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme,
options =>
{
options.Authority += "/v2.0";
}
Alternatively, the setting can be made in the app settings (appsettings.json) file:
{
"AzureAd": {
"Authority": "https://login.microsoftonline.com/common/oauth2/v2.0",
...
}
}
If tacking on a segment to the authority isn't appropriate for the app's OIDC provider, such as with non-ME-ID providers, set the Authority property directly. Either set the property in OpenIdConnectOptions or in the app settings file with the Authority key.
Code changes
The list of claims in the ID token changes for v2.0 endpoints. Microsoft documentation on the changes has been retired, but guidance on the claims in an ID token is available in the ID token claims reference.
Since resources are specified in scope URIs for v2.0 endpoints, remove the OpenIdConnectOptions.Resource property setting in OpenIdConnectOptions:
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options => { ... options.Resource = "..."; // REMOVE THIS LINE ... }
App ID URI
- When using v2.0 endpoints, APIs define an
App ID URI, which is meant to represent a unique identifier for the API. - All scopes include the App ID URI as a prefix, and v2.0 endpoints emit access tokens with the App ID URI as the audience.
- When using V2.0 endpoints, the client ID configured in the Server API changes from the API Application ID (Client ID) to the App ID URI.
appsettings.json:
{
"AzureAd": {
...
"ClientId": "https://{TENANT}.onmicrosoft.com/{PROJECT NAME}"
...
}
}
You can find the App ID URI to use in the OIDC provider app registration description.
Circuit handler to capture users for custom services
Use a CircuitHandler to capture a user from the AuthenticationStateProvider and set the user in a service. If you want to update the user, register a callback to AuthenticationStateChanged and enqueue a Task to obtain the new user and update the service. The following example demonstrates the approach.
In the following example:
- OnConnectionUpAsync is called every time the circuit reconnects, setting the user for the lifetime of the connection. Only the OnConnectionUpAsync method is required unless you implement updates via a handler for authentication changes (
AuthenticationChangedin the following example). - OnCircuitOpenedAsync is called to attach the authentication changed handler,
AuthenticationChanged, to update the user. - The
catchblock of theUpdateAuthenticationtask takes no action on exceptions because there's no way to report the exceptions at this point in code execution. If an exception is thrown from the task, the exception is reported elsewhere in app.
UserService.cs:
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server.Circuits;
public class UserService
{
private ClaimsPrincipal currentUser = new(new ClaimsIdentity());
public ClaimsPrincipal GetUser() => currentUser;
internal void SetUser(ClaimsPrincipal user)
{
if (currentUser != user)
{
currentUser = user;
}
}
}
internal sealed class UserCircuitHandler(
AuthenticationStateProvider authenticationStateProvider,
UserService userService)
: CircuitHandler, IDisposable
{
public override Task OnCircuitOpenedAsync(Circuit circuit,
CancellationToken cancellationToken)
{
authenticationStateProvider.AuthenticationStateChanged +=
AuthenticationChanged;
return base.OnCircuitOpenedAsync(circuit, cancellationToken);
}
private void AuthenticationChanged(Task<AuthenticationState> task)
{
_ = UpdateAuthentication(task);
async Task UpdateAuthentication(Task<AuthenticationState> task)
{
try
{
var state = await task;
userService.SetUser(state.User);
}
catch
{
}
}
}
public override async Task OnConnectionUpAsync(Circuit circuit,
CancellationToken cancellationToken)
{
var state = await authenticationStateProvider.GetAuthenticationStateAsync();
userService.SetUser(state.User);
}
public void Dispose()
{
authenticationStateProvider.AuthenticationStateChanged -=
AuthenticationChanged;
}
}
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server.Circuits;
public class UserService
{
private ClaimsPrincipal currentUser = new ClaimsPrincipal(new ClaimsIdentity());
public ClaimsPrincipal GetUser()
{
return currentUser;
}
internal void SetUser(ClaimsPrincipal user)
{
if (currentUser != user)
{
currentUser = user;
}
}
}
internal sealed class UserCircuitHandler : CircuitHandler, IDisposable
{
private readonly AuthenticationStateProvider authenticationStateProvider;
private readonly UserService userService;
public UserCircuitHandler(
AuthenticationStateProvider authenticationStateProvider,
UserService userService)
{
this.authenticationStateProvider = authenticationStateProvider;
this.userService = userService;
}
public override Task OnCircuitOpenedAsync(Circuit circuit,
CancellationToken cancellationToken)
{
authenticationStateProvider.AuthenticationStateChanged +=
AuthenticationChanged;
return base.OnCircuitOpenedAsync(circuit, cancellationToken);
}
private void AuthenticationChanged(Task<AuthenticationState> task)
{
_ = UpdateAuthentication(task);
async Task UpdateAuthentication(Task<AuthenticationState> task)
{
try
{
var state = await task;
userService.SetUser(state.User);
}
catch
{
}
}
}
public override async Task OnConnectionUpAsync(Circuit circuit,
CancellationToken cancellationToken)
{
var state = await authenticationStateProvider.GetAuthenticationStateAsync();
userService.SetUser(state.User);
}
public void Dispose()
{
authenticationStateProvider.AuthenticationStateChanged -=
AuthenticationChanged;
}
}
In the Program file:
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection.Extensions;
...
builder.Services.AddScoped<UserService>();
builder.Services.TryAddEnumerable(
ServiceDescriptor.Scoped<CircuitHandler, UserCircuitHandler>());
In Startup.ConfigureServices of Startup.cs:
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection.Extensions;
...
services.AddScoped<UserService>();
services.TryAddEnumerable(
ServiceDescriptor.Scoped<CircuitHandler, UserCircuitHandler>());
Use the service in a component to obtain the user:
@inject UserService UserService
<h1>Hello, @(UserService.GetUser().Identity?.Name ?? "world")!</h1>
To set the user in middleware for MVC, Razor Pages, and in other ASP.NET Core scenarios, call SetUser on the UserService in custom middleware after the Authentication Middleware runs, or set the user with an IClaimsTransformation implementation. The following example adopts the middleware approach.
UserServiceMiddleware.cs:
public class UserServiceMiddleware
{
private readonly RequestDelegate next;
public UserServiceMiddleware(RequestDelegate next)
{
this.next = next ?? throw new ArgumentNullException(nameof(next));
}
public async Task InvokeAsync(HttpContext context, UserService service)
{
service.SetUser(context.User);
await next(context);
}
}
Immediately before the call to app.MapRazorComponents<App>() in the Program file, call the middleware:
Immediately before the call to app.MapBlazorHub() in the Program file, call the middleware:
Immediately before the call to app.MapBlazorHub() in Startup.Configure of Startup.cs, call the middleware:
app.UseMiddleware<UserServiceMiddleware>();
Access AuthenticationStateProvider in outgoing request middleware
IHttpClientFactory creates DelegatingHandler instances in a separate dependency injection (DI) scope from the app. If you inject AuthenticationStateProvider into a derived DelegatingHandler type, the handler doesn't have access to the current user's authentication state from the Blazor circuit.
Use either of the following approaches to address this scenario:
- Application scope handler (Recommended)
- Circuit activity handler
Note
For general guidance on defining delegating handlers for HTTP requests by HttpClient instances created using IHttpClientFactory, see the following sections of HTTP requests with IHttpClientFactory - ASP.NET Core:
The examples in the following subsections attach a custom user name header for authenticated users to outgoing requests.
Application scope handler (Recommended)
The approach in this section uses a keyed service to register a custom HttpClient that wraps the base client with an application scope handler resolved from the current application scope to access AuthenticationStateProvider.
Overview of the approach:
- Base client configuration: AddHttpClient is called to register a named client with IHttpClientFactory.
- Keyed registration: A custom
AddApplicationScopeHandlerextension method registers a keyed HttpClient with the same client name. - Scope-aware handler: The application scope handler is resolved from the current scope, giving it access to AuthenticationStateProvider.
- Handler caching: The application scope handler uses IHttpMessageHandlerFactory to get the cached HttpMessageHandler, which preserves connection pooling.
- Configuration reuse: The application scope handler applies the same HttpClientFactoryOptions configuration to its HttpClient as the base client.
Create the following methods and classes:
AddApplicationScopeHandler: An extension method to add the application scope handler and a keyed HttpClient service to the DI container.ApplicationScopeHandler: The application scope handler class.AuthenticationStateHandler: A DelegatingHandler that attaches a custom user name header for authenticated users to outgoing requests.
Services/ApplicationScopeHttpClientExtensions.cs:
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Options;
namespace BlazorSample.Services;
public static class ApplicationScopeHttpClientExtensions
{
public static readonly HttpRequestOptionsKey<IServiceProvider> ScopeKey =
new("ApplicationScope");
public static IHttpClientBuilder AddApplicationScopeHandler(
this IHttpClientBuilder builder)
{
var name = builder.Name;
builder.Services.AddTransient<ApplicationScopeHandler>();
builder.Services.AddKeyedScoped<HttpClient>(name, (sp, key) =>
{
var handler = sp.GetRequiredService<ApplicationScopeHandler>();
handler.InnerHandler =
sp.GetRequiredService<IHttpMessageHandlerFactory>()
.CreateHandler(name);
var client = new HttpClient(handler, disposeHandler: false);
var options =
sp.GetRequiredService<IOptionsMonitor<HttpClientFactoryOptions>>()
.Get(name);
foreach (var action in options.HttpClientActions)
{
action(client);
}
return client;
});
return builder;
}
}
public class ApplicationScopeHandler(IServiceProvider serviceProvider)
: DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
request.Options.Set(ApplicationScopeHttpClientExtensions.ScopeKey,
serviceProvider);
return base.SendAsync(request, cancellationToken);
}
}
public class AuthenticationStateHandler : DelegatingHandler
{
private ClaimsPrincipal? user;
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
if (user is null)
{
if (request.Options.TryGetValue(
ApplicationScopeHttpClientExtensions.ScopeKey, out var sp))
{
var authStateProvider = sp.GetService<AuthenticationStateProvider>();
if (authStateProvider is not null)
{
user = (await authStateProvider.GetAuthenticationStateAsync())
.User;
}
}
}
if (user?.Identity?.IsAuthenticated)
{
request.Headers.TryAddWithoutValidation("X-USER-IDENTITY-NAME",
user.Identity.Name);
}
return await base.SendAsync(request, cancellationToken);
}
}
The AuthenticationStateHandler in the preceding example caches the user for the lifetime of the DelegatingHandler. To fetch the user's current authentication state for each request, remove the null conditional check on the user.
Register the named client in the Program file, calling AddApplicationScopeHandler to add the application scope handler:
builder.Services.AddHttpClient("ExternalApi", client =>
{
client.BaseAddress = new Uri("{REQUEST URI}");
})
.AddApplicationScopeHandler()
.AddHttpMessageHandler<AuthenticationStateHandler>();
The {REQUEST URI} placeholder in the preceding example is the request URI (localhost example: http://localhost:5209).
Inject the client into components using the keyed service:
@using Microsoft.Extensions.DependencyInjection
@code {
[Inject(Key = "ExternalApi")]
public HttpClient Http { get; set; } = default!;
private async Task CallApiAsync()
{
var response = await Http.GetAsync("/api/endpoint");
}
}
Circuit activity handler
The approach in this section uses a circuit activity handler to access the AuthenticationStateProvider, which is an alternative to the recommended application scope handler approach in the preceding section.
First, implement the CircuitServicesAccessor class in the following section of the Blazor dependency injection (DI) article:
Access server-side Blazor services from a different DI scope
Use the CircuitServicesAccessor to access the AuthenticationStateProvider in the DelegatingHandler implementation.
AuthenticationStateHandler.cs:
using Microsoft.AspNetCore.Components.Authorization;
public class AuthenticationStateHandler(
CircuitServicesAccessor circuitServicesAccessor)
: DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var authStateProvider = circuitServicesAccessor.Services?
.GetRequiredService<AuthenticationStateProvider>();
if (authStateProvider is null)
{
throw new Exception("AuthenticationStateProvider not available");
}
var authState = await authStateProvider.GetAuthenticationStateAsync();
var user = authState?.User;
if (user?.Identity is not null && user.Identity.IsAuthenticated)
{
request.Headers.Add("X-USER-IDENTITY-NAME", user.Identity.Name);
}
return await base.SendAsync(request, cancellationToken);
}
}
In the Program file, register the AuthenticationStateHandler and add the handler to the IHttpClientFactory that creates HttpClient instances:
builder.Services.AddTransient<AuthenticationStateHandler>();
builder.Services.AddHttpClient("HttpMessageHandler")
.AddHttpMessageHandler<AuthenticationStateHandler>();
Opaque (reference) access token support
The guidance in this section explains how to implement opaque (reference) access token support, which offers the following advantages over JSON Web Tokens (JWTs):
- Strict revocation: Invalidate access tokens at any time before they naturally expire.
- Token size limits: Store a large number of user claims in the token to avoid a prohibitively large JWT.
- Security: Prevent API consumers or third parties from reading access token claims.
Note
The following guidance requires an authentication server that supports opaque (reference) access tokens. Currently, Microsoft Entra doesn't support opaque access token validation. Keycloak and Okta issue JWT access tokens by default. The opaque token handler in this section still works against Keycloak and Okta because it relies only on RFC 7662 introspection. "Opaque" in this section describes how the client treats the token rather than how the server mints it. Alternatively, Duende IdentityServer can be configured to only issue opaque tokens.
When testing this pattern against Keycloak, the API's introspection client must be a different OIDC client than the one that issued the user's access token. Introspecting a token using the client that minted it returns {"active": false} with "Access token JWT check failed" in the server's log. This doesn't happen naturally for the following scenario because the Blazor Web App and the Minimal API (MinimalApiJwt) are separate clients.
AddOpenIdConnect supports opaque tokens because it doesn't perform access token validation when configured for Proof Key for Code Exchange (PKCE) authorization code flow. It relies on the ASP.NET Core server's HTTPS backchannel to the OIDC authentication service to obtain the ID token using the authorization code received when the user redirects back to the ASP.NET Core app after signing in. If the app is only required to log a user in with OIDC to get a valid authentication cookie, opaque access tokens are supported without modifying the app.
A failure occurs only when the opaque token acquired by AddOpenIdConnect is passed to another service that attempts to validate it with AddJwtBearer. Unlike self-contained JWTs, opaque tokens require a request to an authorization server to validate the status and to retrieve the claims. To work around this limitation, either use a third-party API, such as the Duende Introspection Authentication Handler, or create a custom AuthenticationHandler to validate the token.
Important
Duende Software and Okta aren't owned or controlled by Microsoft and might require you to pay a license fee for production use of their services and libraries.
The following AuthenticationHandler<TOptions> and associated configuration and helper code is provided as a general approach, which might require further development to suit a specific authorization server's requirements. The following handler extracts the opaque token from the Authorization header for an HTTP call to an authorization server's introspection endpoint and creates an AuthenticationTicket containing the user's claims.
Calling an authorization server's introspection endpoint requires authentication. The following example relies on setting the client secret for authentication in the request's Authorization header (base64 encoded credentials) using the Secret Manager tool for local development and testing.
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.
In the following handler, the authorization server's introspection endpoint client secret uses the configuration key Authentication:Schemes:OpaqueTokenAuthentication:ClientSecret. For production apps, consider using client assertions. For more information, see Confidential client assertions (Microsoft Entra documentation).
If 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 for the authorization server. The {SECRET} placeholder is the client secret:
dotnet user-secrets set "Authentication:Schemes:OpaqueTokenAuthentication: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.
Extensions/HttpRequestExtensions.cs:
namespace MinimalApiJwt.Extensions;
public static class HttpRequestExtensions
{
public static string? ExtractBearerToken(this HttpRequest request)
{
var authorizationHeader = request.Headers.Authorization.ToString();
if (!string.IsNullOrEmpty(authorizationHeader) &&
authorizationHeader.StartsWith("Bearer ",
StringComparison.OrdinalIgnoreCase))
{
var token = authorizationHeader["Bearer ".Length..].Trim();
if (!string.IsNullOrEmpty(token))
{
return token;
}
}
return null;
}
}
Authentication/OpaqueTokenAuthenticationOptions.cs:
using Microsoft.AspNetCore.Authentication;
namespace MinimalApiJwt.Authentication;
public class OpaqueTokenAuthenticationOptions : AuthenticationSchemeOptions
{
public const string DefaultScheme = "OpaqueTokenAuthentication";
public string? IntrospectionEndpoint { get; set; }
public string? ClientId { get; set; }
public string? ClientSecret { get; set; }
}
The following handler attempts to validate an opaque (reference) access token. An HTTP call is made to the authorization server's introspection endpoint with the token and the API's credentials. The response is processed to determine if the token is valid:
- If the token is valid, an AuthenticationTicket is created containing the user's claims.
- If the token is invalid, a failed authorization result is returned.
The handler's options (Options) is an instance of OpaqueTokenAuthenticationOptions provided by the AuthenticationHandler<TOptions> base type, which is configured in the app's Program file with the authorization server's introspection endpoint and the API's client ID. The API's client secret is provided by the Secret Manager tool during development.
IOptionsMonitor<OpaqueTokenAuthenticationOptions> (optionsMonitor) isn't used directly by the handler, but it could be used to support dynamic configuration changes at runtime.
For the request's content in FormUrlEncodedContent, some servers require a token type hint (token_type_hint). For example, the required value might be access_token. See your authentication server's documentation for details.
Authentication/OpaqueTokenAuthenticationHandler.cs:
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Text.Json;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using MinimalApiJwt.Extensions;
namespace MinimalApiJwt.Authentication;
public class OpaqueTokenAuthenticationHandler(
IOptionsMonitor<OpaqueTokenAuthenticationOptions> optionsMonitor,
ILoggerFactory logger,
UrlEncoder encoder,
IHttpClientFactory httpClientFactory)
: AuthenticationHandler<OpaqueTokenAuthenticationOptions>(optionsMonitor,
logger, encoder)
{
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var opaqueToken = Request.ExtractBearerToken();
if (opaqueToken is null)
{
var failedResult = AuthenticateResult.Fail(
"Bearer token not found in Authorization header.");
return failedResult;
}
var introspectionUri = Options.IntrospectionEndpoint;
var clientId = Options.ClientId;
var clientSecret = Options.ClientSecret;
if (string.IsNullOrWhiteSpace(introspectionUri) ||
string.IsNullOrWhiteSpace(clientId) ||
string.IsNullOrWhiteSpace(clientSecret))
{
var failedResult = AuthenticateResult.Fail(
"Opaque token authentication isn't fully configured.");
return failedResult;
}
using var client = httpClientFactory.CreateClient();
// Set the Authorization header (base64 encoded credentials)
var authString = Convert.ToBase64String(
System.Text.Encoding.ASCII.GetBytes($"{clientId}:{clientSecret}"));
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Basic", authString);
// Prepare the form-encoded body containing the token
var content = new FormUrlEncodedContent(
[
new KeyValuePair<string, string>("token", opaqueToken)
]);
// Post to the introspection endpoint
var response = await client.PostAsync(introspectionUri, content);
if (!response.IsSuccessStatusCode)
{
var failedResult = AuthenticateResult.Fail(
"Introspection endpoint failure.");
return failedResult;
}
// Parse the JSON response
var responseString = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(responseString);
// The 'active' property determines if the token is valid and not expired
var tokenIsValid =
doc.RootElement.TryGetProperty("active", out var activeProperty) &&
activeProperty.ValueKind == JsonValueKind.True;
if (tokenIsValid)
{
// Map standard introspection response fields onto claims.
// Field names below match what Keycloak, Duende IdentityServer,
// Auth0, and Okta return; adjust the role source for your provider.
var claims = new List<Claim>();
string? Get(string name) =>
doc.RootElement.TryGetProperty(name, out var v) &&
v.ValueKind == JsonValueKind.String ? v.GetString() : null;
var sub = Get("sub");
var username = Get("preferred_username") ?? Get("username") ?? sub;
if (sub is not null) claims.Add(new Claim(ClaimTypes.NameIdentifier, sub));
if (username is not null) claims.Add(new Claim(ClaimTypes.Name, username));
if (Get("email") is { } email) claims.Add(new Claim(ClaimTypes.Email, email));
if ((Get("client_id") ?? Get("azp")) is { } cid)
claims.Add(new Claim("client_id", cid));
if (Get("scope") is { } scope)
foreach (var s in scope.Split(' ', StringSplitOptions.RemoveEmptyEntries))
claims.Add(new Claim("scope", s));
// Keycloak surfaces realm roles under realm_access.roles.
// Duende/IdentityServer uses a flat "role" claim; Auth0 uses a
// configurable custom claim. Adjust for your authorization server.
if (doc.RootElement.TryGetProperty("realm_access", out var ra) &&
ra.ValueKind == JsonValueKind.Object &&
ra.TryGetProperty("roles", out var roles) &&
roles.ValueKind == JsonValueKind.Array)
{
foreach (var r in roles.EnumerateArray())
if (r.ValueKind == JsonValueKind.String)
claims.Add(new Claim(ClaimTypes.Role, r.GetString()!));
}
var identity = new ClaimsIdentity(claims,
OpaqueTokenAuthenticationOptions.DefaultScheme,
nameType: ClaimTypes.Name,
roleType: ClaimTypes.Role);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal,
OpaqueTokenAuthenticationOptions.DefaultScheme);
var result = AuthenticateResult.Success(ticket);
return result;
}
else
{
var failedResult = AuthenticateResult.Fail("Bearer token invalid.");
return failedResult;
}
}
}
Note
The preceding approach can be further improved by using the OpenID Connect discovery endpoint and adding a cache for the client's HttpClient introspection requests.
In the Program file:
using MinimalApiJwt.Authentication;
...
builder.Services.AddHttpClient();
builder.Services.AddAuthentication()
.AddScheme<OpaqueTokenAuthenticationOptions, OpaqueTokenAuthenticationHandler>(
OpaqueTokenAuthenticationOptions.DefaultScheme,
options =>
{
options.IntrospectionEndpoint = "{AUTH SERVER INTROSPECTION URI}";
options.ClientId = "{API CLIENT ID}";
options.ClientSecret =
builder.Configuration[
"Authentication:Schemes:OpaqueTokenAuthentication:ClientSecret"];
});
The preceding example's placeholders:
{AUTH SERVER INTROSPECTION URI}: Authentication server's introspection URI{API CLIENT ID}: API client ID
Values for the authentication server introspection URI ({AUTH SERVER INTROSPECTION URI}) and the API client ID ({API CLIENT ID}) can be supplied from app settings or any other configuration source.
Tokens are typically invalidated on a logout event using the revocation endpoint. The following example is a starting point for further development:
app.MapPost("/logout",
async ([FromForm] string? returnUrl, HttpContext context,
IHttpClientFactory httpClientFactory) =>
{
var accessToken = await context.GetTokenAsync("access_token");
if (!string.IsNullOrEmpty(accessToken))
{
// Prepare the revocation request (RFC 7009)
var content =
new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "token", accessToken },
{ "token_type_hint", "access_token" },
{ "client_id", "{API CLIENT ID}" },
{ "client_secret", "{CLIENT SECRET}" }
});
// POST to the revocation endpoint
using var client = httpClientFactory.CreateClient();
await client.PostAsync("{AUTH SERVER TOKEN REVOCATION URI}", content);
}
return TypedResults.SignOut(new AuthenticationProperties { RedirectUri = "{REDIRECT URI}" },
[CookieAuthenticationDefaults.AuthenticationScheme]);
});
The preceding example's placeholders:
{AUTH SERVER TOKEN REVOCATION URI}: The authentication server's token revocation URI.{API CLIENT ID}: The API client ID.{CLIENT SECRET}: The client secret obtained securely.{REDIRECT URI}: The redirect URI.
In Duende IdentityServer, tokens are revoked automatically by setting the CoordinateLifetimeWithUserSession client configuration property to true, which automatically cleans up associated tokens when a session ends. For more information, see Session Cleanup and Logout (Duende documentation).
Built-in opaque access token support is under consideration for a future release of .NET. For more information, see Opaque - reference token validation (dotnet/aspnetcore #46026).
ASP.NET Core