Skip to content

Commit d653a02

Browse files
committed
Add the latest recommended passing tokens approach
1 parent 498c6d9 commit d653a02

File tree

1 file changed

+198
-4
lines changed

1 file changed

+198
-4
lines changed

aspnetcore/blazor/security/additional-scenarios.md

+198-4
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,206 @@ This article explains how to configure server-side Blazor for additional securit
2323

2424
*This section applies to Blazor Web Apps. For Blazor Server, view the [7.0 version of this article section](xref:blazor/security/additional-scenarios?view=aspnetcore-7.0&preserve-view=true#pass-tokens-to-a-server-side-blazor-app).*
2525

26-
For more information, see the following issues:
26+
If you merely want to use access tokens to make web API calls from a Blazor Web App with a [named HTTP client](xref:blazor/call-web-api#named-httpclient-with-ihttpclientfactory), see the [Use a token handler for web API calls](#use-a-token-handler-for-web-api-calls) section, which explains how to use a <xref:System.Net.Http.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 throughout the app for general use.
2727

28-
* [Access `AuthenticationStateProvider` in outgoing request middleware (`dotnet/aspnetcore` #52379)](https://github.com/dotnet/aspnetcore/issues/52379): This is the current issue to address passing tokens in Blazor Web Apps with framework features, which will probably be addressed for .NET 11 (late 2026).
29-
* [Problem providing Access Token to HttpClient in Interactive Server mode (`dotnet/aspnetcore` #52390)](https://github.com/dotnet/aspnetcore/issues/52390): This issue was closed as a duplicate of the preceding issue, but it contains helpful discussion and potential workaround strategies.
28+
To save tokens and other authentication properties in Blazor Web Apps, we recommend putting them into user claims, which can be accessed from anywhere in the app, including on the client (in the `.Client` project) when [passing authentication state](xref:blazor/security/index#manage-authentication-state-in-blazor-web-apps) and setting <xref:Microsoft.AspNetCore.Components.WebAssembly.Server.AuthenticationStateSerializationOptions.SerializeAllClaims%2A> to `true`.
3029

31-
For Blazor Server, view the [7.0 version of this article section](xref:blazor/security/additional-scenarios?view=aspnetcore-7.0&preserve-view=true#pass-tokens-to-a-server-side-blazor-app).
30+
In the context of an app that adopts [OpenId Connect (OIDC) authentication](xref:blazor/security/blazor-web-app-oidc), the following example shows how to retain the access token of a user that just signed into the app.
31+
32+
Where cookie authentication options (`CookieAuthenticationOptions`) are configured:
33+
34+
```csharp
35+
services.AddOptions<CookieAuthenticationOptions>(cookieScheme)
36+
.Configure<CookieOidcRefresher>((cookieOptions, refresher) =>
37+
{
38+
cookieOptions.Events.OnValidatePrincipal = context =>
39+
refresher.ValidateOrRefreshCookieAsync(context, oidcScheme);
40+
41+
cookieOptions.Events.OnSigningIn = (context) =>
42+
{
43+
if (context.Principal?.Identity is not null &&
44+
context.Principal.Identity.IsAuthenticated)
45+
{
46+
var accessToken = context.Properties.GetTokenValue("access_token");
47+
var claimsIdentity = new ClaimsIdentity(context.Principal?.Identity,
48+
[new Claim("AccessToken", accessToken ?? "No Access Token!")]);
49+
context.Principal = new ClaimsPrincipal(claimsIdentity);
50+
context.Properties.Items.Remove("access_token");
51+
}
52+
53+
return Task.CompletedTask;
54+
};
55+
});
56+
```
57+
58+
Where the principal is validated (<xref:Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationEvents.OnValidatePrincipal%2A>) to update user access tokens when they expire, the claim is also updated with the new access token by replacing the principal:
59+
60+
```csharp
61+
public async Task ValidateOrRefreshCookieAsync(
62+
CookieValidatePrincipalContext validateContext, string oidcScheme)
63+
{
64+
...
65+
66+
validationResult.Claims.Remove("AccessToken");
67+
validationResult.ClaimsIdentity.AddClaim(
68+
new Claim("AccessToken", message.AccessToken));
69+
validateContext.ReplacePrincipal(
70+
new ClaimsPrincipal(validationResult.ClaimsIdentity));
71+
72+
...
73+
}
74+
```
75+
76+
App code and components, including components that render on the client, can use the claim to read tokens and authentication properties. In the following `ServerWeatherForecaster` service for obtaining weather data on the server, the `AccessToken` claim is used to make a secure call to a backend web API for weather data:
77+
78+
```csharp
79+
internal sealed class ServerWeatherForecaster(IHttpClientFactory clientFactory,
80+
IHttpContextAccessor httpContextAccessor, IConfiguration config)
81+
: IWeatherForecaster
82+
{
83+
public async Task<IEnumerable<WeatherForecast>> GetWeatherForecastAsync()
84+
{
85+
var request = new HttpRequestMessage(HttpMethod.Get, "/weather-forecast");
86+
var accessToken = httpContextAccessor.HttpContext?.User.Claims.First(
87+
c => c.Type == "AccessToken").Value
88+
?? throw new Exception("No access token!");
89+
request.Headers.Authorization =
90+
new AuthenticationHeaderValue("Bearer", accessToken);
91+
var client = clientFactory.CreateClient();
92+
client.BaseAddress = new Uri(config["ExternalApiUri"]
93+
?? throw new Exception("No base address!"));
94+
95+
var response = await client.SendAsync(request);
96+
97+
response.EnsureSuccessStatusCode();
98+
99+
return await response.Content.ReadFromJsonAsync<WeatherForecast[]>() ??
100+
throw new IOException("No weather forecast!");
101+
}
102+
}
103+
```
104+
105+
The following code demonstrates a similar approach in a component that calls a secure web API:
106+
107+
:::moniker-end
108+
109+
:::moniker range=">= aspnetcore-10.0"
110+
111+
```razor
112+
@page "/..."
113+
@using Microsoft.AspNetCore.Authorization
114+
@attribute [Authorize]
115+
116+
...
117+
118+
@code {
119+
[CascadingParameter]
120+
private Task<AuthenticationState>? authenticationState { get; set; }
121+
122+
[SupplyParameterFromPersistentComponentState]
123+
public string AccessToken { get; set; } = "Not set!";
124+
125+
protected override async Task OnInitializedAsync()
126+
{
127+
if (authenticationState is not null)
128+
{
129+
var authState = await authenticationState;
130+
var user = authState?.User;
131+
132+
if (user is not null)
133+
{
134+
AccessToken ??= user.Claims.FirstOrDefault(
135+
c => c.Type == "AccessToken")?.Value ?? "Not found!";
136+
137+
request.Headers.Authorization =
138+
new AuthenticationHeaderValue("Bearer", AccessToken);
139+
var client = clientFactory.CreateClient();
140+
client.BaseAddress = new Uri(...);
141+
142+
var response = await client.SendAsync(request);
143+
144+
response.EnsureSuccessStatusCode();
145+
146+
return await response.Content.ReadFromJsonAsync<...>() ??
147+
throw new IOException("No data!");
148+
}
149+
}
150+
}
151+
}
152+
```
153+
154+
:::moniker-end
155+
156+
:::moniker range=">= aspnetcore-8.0 < aspnetcore-10.0"
157+
158+
```razor
159+
@page "/..."
160+
@using Microsoft.AspNetCore.Authorization
161+
@attribute [Authorize]
162+
@implements IDisposable
163+
@inject PersistentComponentState ApplicationState
164+
165+
...
166+
167+
@code {
168+
private PersistingComponentStateSubscription persistingSubscription;
169+
private string accessToken = "Not set!";
170+
171+
[CascadingParameter]
172+
private Task<AuthenticationState>? authenticationState { get; set; }
173+
174+
protected override async Task OnInitializedAsync()
175+
{
176+
if (authenticationState is not null)
177+
{
178+
var authState = await authenticationState;
179+
var user = authState?.User;
180+
181+
if (user is not null)
182+
{
183+
if (!ApplicationState.TryTakeFromJson<string>(nameof(accessToken),
184+
out var restoredAccessToken))
185+
{
186+
accessToken = user.Claims.FirstOrDefault(
187+
c => c.Type == "AccessToken")?.Value ?? "Not found!";
188+
189+
request.Headers.Authorization =
190+
new AuthenticationHeaderValue("Bearer", accessToken);
191+
var client = clientFactory.CreateClient();
192+
client.BaseAddress = new Uri(...);
193+
194+
var response = await client.SendAsync(request);
195+
196+
response.EnsureSuccessStatusCode();
197+
198+
return await response.Content.ReadFromJsonAsync<...>() ??
199+
throw new IOException("No data!");
200+
}
201+
else
202+
{
203+
accessToken = restoredAccessToken!;
204+
}
205+
}
206+
}
207+
208+
// Call at the end to avoid a potential race condition at app shutdown
209+
persistingSubscription = ApplicationState.RegisterOnPersisting(PersistData);
210+
}
211+
212+
private Task PersistData()
213+
{
214+
ApplicationState.PersistAsJson(nameof(accessToken), accessToken);
215+
216+
return Task.CompletedTask;
217+
}
218+
219+
void IDisposable.Dispose() => persistingSubscription.Dispose();
220+
}
221+
```
222+
223+
:::moniker-end
224+
225+
:::moniker range=">= aspnetcore-8.0"
32226

33227
## Reading tokens from `HttpContext`
34228

0 commit comments

Comments
 (0)