Skip to content

Commit 1a2e7a4

Browse files
committed
Updates
1 parent 65e3b7f commit 1a2e7a4

File tree

1 file changed

+249
-1
lines changed

1 file changed

+249
-1
lines changed

aspnetcore/blazor/security/additional-scenarios.md

Lines changed: 249 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Note that <xref:Microsoft.AspNetCore.Http.HttpContext> used as a [cascading para
3737

3838
For more information, see <xref:blazor/components/httpcontext>.
3939

40-
### Example
40+
### Token handler example for web API calls
4141

4242
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 <xref:Microsoft.AspNetCore.Http.HttpContext> using <xref:Microsoft.AspNetCore.Http.IHttpContextAccessor> is only performed during static SSR.
4343

@@ -124,6 +124,254 @@ var response = await client.SendAsync(request);
124124

125125
Additional features are planned for Blazor, which are tracked by [Access `AuthenticationStateProvider` in outgoing request middleware (`dotnet/aspnetcore` #52379)](https://github.com/dotnet/aspnetcore/issues/52379). [Problem providing Access Token to HttpClient in Interactive Server mode (`dotnet/aspnetcore` #52390)](https://github.com/dotnet/aspnetcore/issues/52390) is a closed issue that contains helpful discussion and potential workaround strategies for advanced use cases.
126126

127+
### Root-level cascading values with notifications
128+
129+
Tokens can be passed via [root-level cascading values](xref:blazor/components/cascading-values-and-parameters#root-level-cascading-values) with a <xref:Microsoft.AspNetCore.Components.CascadingValueSource%601> with subscriber notifications. This general approach works well when you need to interact with tokens outside of calling a web API.
130+
131+
The following `CascadingStateServiceCollectionExtensions` creates a <xref:Microsoft.AspNetCore.Components.CascadingValueSource%601> from a type that implements <xref:System.ComponentModel.INotifyPropertyChanged>.
132+
133+
> [!NOTE]
134+
> For Blazor Web App solutions consisting of server and client (`.Client`) projects, place the following `CascadingStateServiceCollectionExtensions.cs` file into the `.Client` project.
135+
136+
`CascadingStateServiceCollectionExtensions.cs`:
137+
138+
```csharp
139+
using Microsoft.AspNetCore.Components;
140+
using System.ComponentModel;
141+
142+
namespace Microsoft.Extensions.DependencyInjection;
143+
144+
public static class CascadingStateServiceCollectionExtensions
145+
{
146+
public static IServiceCollection AddNotifyingCascadingValue<T>(
147+
this IServiceCollection services, T state, bool isFixed = false)
148+
where T : INotifyPropertyChanged
149+
{
150+
return services.AddCascadingValue<T>(sp =>
151+
{
152+
return new CascadingStateValueSource<T>(state, isFixed);
153+
});
154+
}
155+
156+
private sealed class CascadingStateValueSource<T>
157+
: CascadingValueSource<T>, IDisposable where T : INotifyPropertyChanged
158+
{
159+
private readonly T state;
160+
private readonly CascadingValueSource<T> source;
161+
162+
public CascadingStateValueSource(T state, bool isFixed = false)
163+
: base(state, isFixed = false)
164+
{
165+
this.state = state;
166+
source = new CascadingValueSource<T>(state, isFixed);
167+
this.state.PropertyChanged += HandlePropertyChanged;
168+
}
169+
170+
private void HandlePropertyChanged(object? sender, PropertyChangedEventArgs e)
171+
{
172+
_ = NotifyChangedAsync();
173+
}
174+
175+
public void Dispose()
176+
{
177+
state.PropertyChanged -= HandlePropertyChanged;
178+
}
179+
}
180+
}
181+
```
182+
183+
Create a class to manage the token state. The following `NotifyingState` example tracks state for access and refresh tokens.
184+
185+
> [!NOTE]
186+
> For Blazor Web App solutions consisting of server and client (`.Client`) projects, place the following `NotifyingState.cs` file into the `.Client` project.
187+
188+
`NotifyingState.cs`:
189+
190+
```csharp
191+
using System.ComponentModel;
192+
using System.Runtime.CompilerServices;
193+
194+
namespace BlazorWebAppOidcServer;
195+
196+
public class NotifyingState : INotifyPropertyChanged
197+
{
198+
public event PropertyChangedEventHandler? PropertyChanged;
199+
private string accessToken = string.Empty;
200+
private string refreshToken = string.Empty;
201+
202+
public string AccessToken
203+
{
204+
get => accessToken;
205+
set
206+
{
207+
if (accessToken != value)
208+
{
209+
accessToken = value;
210+
OnPropertyChanged();
211+
}
212+
}
213+
}
214+
215+
public string RefreshToken
216+
{
217+
get => refreshToken;
218+
set
219+
{
220+
if (refreshToken != value)
221+
{
222+
refreshToken = value;
223+
OnPropertyChanged();
224+
}
225+
}
226+
}
227+
228+
protected virtual void OnPropertyChanged(
229+
[CallerMemberName] string? propertyName = default)
230+
=> PropertyChanged?.Invoke(this, new(propertyName));
231+
}
232+
```
233+
234+
In the `Program` file&dagger;, `NotifyingState` is passed to create a <xref:Microsoft.AspNetCore.Components.CascadingValueSource%601>:
235+
236+
```csharp
237+
builder.Services.AddNotifyingCascadingValue(new NotifyingState());
238+
```
239+
240+
> [!NOTE]
241+
> &dagger;For Blazor Web App solutions consisting of server and client (`.Client`) projects, place the preceding code into each project's `Program` file.
242+
243+
The `Program` file of the server project must also register <xref:Microsoft.AspNetCore.Http.IHttpContextAccessor>:
244+
245+
```csharp
246+
builder.Services.AddHttpContextAccessor();
247+
```
248+
249+
At the top of the `App` component (`App.razor`), add an [`@using`](xref:mvc/views/razor#using) directive for <xref:Microsoft.AspNetCore.Authentication?displayProperty=fullName>:
250+
251+
```razor
252+
@using Microsoft.AspNetCore.Authentication
253+
```
254+
255+
Under the markup of the `App` component, add the following `@code` block to set the `AccessToken` and `RefreshToken` properties from the cascaded <xref:Microsoft.AspNetCore.Http.HttpContext>. The following example is suitable when using an OIDC identity provider, an example for Microsoft Entra ID with Microsoft Identity Web packages follows this example.
256+
257+
```razor
258+
@code {
259+
[CascadingParameter]
260+
public HttpContext? HttpContext { get; set; }
261+
262+
[CascadingParameter]
263+
public NotifyingState? State { get; set; }
264+
265+
protected override void OnInitialized()
266+
{
267+
if (State is not null)
268+
{
269+
State.AccessToken = HttpContext?.GetTokenAsync("access_token").Result ?? string.Empty;
270+
State.RefreshToken = HttpContext?.GetTokenAsync("refresh_token").Result ?? string.Empty;
271+
}
272+
}
273+
}
274+
```
275+
276+
For app's that rely upon Entra with Microsoft Identity Web, the following approach can obtain the access token:
277+
278+
```razor
279+
@using Microsoft.AspNetCore.Components.Authorization
280+
@using Microsoft.Identity.Web;
281+
@inject ITokenAcquisition TokenAcquisition
282+
@inject AuthenticationStateProvider AuthState
283+
284+
...
285+
286+
@code {
287+
[CascadingParameter]
288+
public HttpContext? HttpContext { get; set; }
289+
290+
[CascadingParameter]
291+
public NotifyingState? State { get; set; }
292+
293+
protected override async Task OnInitializedAsync()
294+
{
295+
if (State is not null)
296+
{
297+
var authState = await AuthState.GetAuthenticationStateAsync();
298+
299+
if (authState.User.Identity is not null && authState.User.Identity.IsAuthenticated)
300+
{
301+
State.AccessToken = await TokenAcquisition.GetAccessTokenForUserAsync([ "{SCOPE}" ]);
302+
}
303+
}
304+
}
305+
}
306+
```
307+
308+
The app, including within components, can now obtain the access and refresh tokens, keeping in mind that the values are set for the life of the circuit unless developer code updates them. The following approach is effective in server-side scenarios, and an example follows that's useful in Blazor Web Apps that adopt Interactive Auto rendering.
309+
310+
```csharp
311+
private string? accessToken;
312+
private string? refreshToken;
313+
314+
[CascadingParameter]
315+
public NotifyingState? State { get; set; }
316+
317+
protected override async Task OnInitializedAsync()
318+
{
319+
accessToken = State?.AccessToken;
320+
refreshToken = State?.RefreshToken;
321+
}
322+
```
323+
324+
The following example demonstrates the concept in an app that adopts Interactive Auto rendering with server and `.Client` projects, where the tokens must be persisted from the server during prerendering:
325+
326+
```razor
327+
@implements IDisposable
328+
@inject PersistentComponentState ApplicationState
329+
330+
...
331+
332+
@code {
333+
private PersistingComponentStateSubscription persistingSubscription;
334+
private string? accessToken;
335+
private string? refreshToken;
336+
337+
[CascadingParameter]
338+
public NotifyingState? State { get; set; }
339+
340+
protected override async Task OnInitializedAsync()
341+
{
342+
if (!ApplicationState.TryTakeFromJson<string>(nameof(accessToken), out var restoredAccessToken))
343+
{
344+
accessToken = State?.AccessToken;
345+
}
346+
else
347+
{
348+
accessToken = restoredAccessToken!;
349+
}
350+
351+
if (!ApplicationState.TryTakeFromJson<string>(nameof(refreshToken), out var restoredRefreshToken))
352+
{
353+
refreshToken = State?.RefreshToken;
354+
}
355+
else
356+
{
357+
refreshToken = restoredRefreshToken!;
358+
}
359+
360+
persistingSubscription = ApplicationState.RegisterOnPersisting(PersistData);
361+
}
362+
363+
private Task PersistData()
364+
{
365+
ApplicationState.PersistAsJson(nameof(accessToken), accessToken);
366+
ApplicationState.PersistAsJson(nameof(refreshToken), refreshToken);
367+
368+
return Task.CompletedTask;
369+
}
370+
371+
void IDisposable.Dispose() => persistingSubscription.Dispose();
372+
}
373+
```
374+
127375
### Passing the anti-request forgery (CSRF/XSRF) token
128376

129377
Passing the [anti-request forgery (CSRF/XSRF) token](xref:security/anti-request-forgery) to Razor components is useful in scenarios where components POST to Identity or other endpoints that require validation. However, don't follow the guidance in this section for processing form POST requests or web API requests with XSRF support. The Blazor framework provides built-in antiforgery support for forms and calling web APIs. For more information, see the following resources:

0 commit comments

Comments
 (0)