Skip to content
This repository was archived by the owner on Jan 24, 2025. It is now read-only.

Commit 4a4a99f

Browse files
committed
Added Duende.Bff.Blazor
1 parent a3cc773 commit 4a4a99f

21 files changed

+858
-67
lines changed

Directory.Build.targets

+16-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
<Project>
22
<PropertyGroup Condition=" '$(TargetFramework)' == 'net8.0'">
33
<FrameworkVersionRuntime>8.0.0</FrameworkVersionRuntime>
4-
<FrameworkVersionTesting>8.0.0</FrameworkVersionTesting>
4+
<FrameworkVersionTesting>8.0.8</FrameworkVersionTesting>
5+
<WilsonVersion>7.1.2</WilsonVersion> <!-- Used in samples -->
56
<YarpVersion>2.1.0</YarpVersion>
6-
<IdentityServerVersion>7.0.4</IdentityServerVersion>
7+
<IdentityServerVersion>7.0.6</IdentityServerVersion>
78
</PropertyGroup>
89

910
<ItemGroup>
@@ -13,15 +14,26 @@
1314

1415
<!-- runtime -->
1516
<PackageReference Update="IdentityModel" Version="7.0.0" />
16-
<PackageReference Update="Duende.AccessTokenManagement.OpenIdConnect" Version="3.0.0-preview.3" />
17+
<PackageReference Update="Duende.AccessTokenManagement.OpenIdConnect" Version="3.0.0" />
1718
<PackageReference Update="Microsoft.EntityFrameworkCore.Relational" Version="$(FrameworkVersionRuntime)" />
19+
<PackageReference Update="Microsoft.Extensions.Http" Version="$(FrameworkVersionRuntime)" />
20+
<PackageReference Update="Microsoft.AspNetCore.Components.WebAssembly" Version="$(FrameworkVersionRuntime)" />
21+
<PackageReference Update="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="$(FrameworkVersionRuntime)" />
22+
<PackageReference Update="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="$(FrameworkVersionRuntime)" />
23+
<PackageReference Update="Microsoft.AspNetCore.Components.Authorization" Version="$(FrameworkVersionRuntime)" />
1824
<PackageReference Update="Yarp.ReverseProxy" Version="$(YarpVersion)" />
1925

26+
<!-- samples -->
27+
<PackageReference Update="Serilog.AspNetCore" Version="8.0.2" />
28+
<PackageReference Update="Microsoft.IdentityModel.JsonWebTokens" Version="$(WilsonVersion)" />
29+
<PackageReference Update="System.IdentityModel.Tokens.Jwt" Version="$(WilsonVersion)" />
30+
2031
<!-- testing -->
2132
<PackageReference Update="Microsoft.EntityFrameworkCore.InMemory" Version="$(FrameworkVersionTesting)" />
2233
<PackageReference Update="Microsoft.AspNetCore.Authentication.JwtBearer" Version="$(FrameworkVersionTesting)" />
2334
<PackageReference Update="Microsoft.AspNetCore.TestHost" Version="$(FrameworkVersionTesting)" />
24-
<PackageReference Update="Microsoft.Extensions.TimeProvider.Testing" Version="$(FrameworkVersionTesting)" />
35+
<!-- Test timeprovider is released separately from the framework, so we can't use FrameworkVersionTesting -->
36+
<PackageReference Update="Microsoft.Extensions.TimeProvider.Testing" Version="8.8.0" />
2537

2638
<PackageReference Update="Duende.IdentityServer" Version="$(IdentityServerVersion)" />
2739

Duende.Bff.sln

+31-29
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
1+
22
Microsoft Visual Studio Solution File, Format Version 12.00
33
# Visual Studio Version 17
44
VisualStudioVersion = 17.9.34414.90
@@ -39,7 +39,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JS8.DPoP", "samples\JS8.DPo
3939
EndProject
4040
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JS8.EF", "samples\JS8.EF\JS8.EF.csproj", "{CBB98134-92F5-487D-8CA3-84C19FF46775}"
4141
EndProject
42-
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Blazor.Wasm", "Blazor.Wasm", "{7E6EA8BA-EE8B-450E-AE89-C4604C0DD326}"
42+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duende.Bff.Blazor", "src\Duende.Bff.Blazor\Duende.Bff.Blazor.csproj", "{E02DF032-C17F-4D0C-9CAA-3BD3CC9E4F84}"
43+
EndProject
44+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duende.Bff.Blazor.Client", "src\Duende.Bff.Blazor.Client\Duende.Bff.Blazor.Client.csproj", "{DDB9C401-6B1F-4727-A4CB-932034FBF94E}"
45+
EndProject
4346
EndProject
4447
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blazor.Wasm.Bff", "samples\Blazor.Wasm\Blazor.Wasm.Bff\Blazor.Wasm.Bff.csproj", "{BC21ADB7-F2CA-44F0-B6ED-0405E1EFFFA3}"
4548
EndProject
@@ -223,30 +226,30 @@ Global
223226
{CBB98134-92F5-487D-8CA3-84C19FF46775}.Release|x64.Build.0 = Release|Any CPU
224227
{CBB98134-92F5-487D-8CA3-84C19FF46775}.Release|x86.ActiveCfg = Release|Any CPU
225228
{CBB98134-92F5-487D-8CA3-84C19FF46775}.Release|x86.Build.0 = Release|Any CPU
226-
{BC21ADB7-F2CA-44F0-B6ED-0405E1EFFFA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
227-
{BC21ADB7-F2CA-44F0-B6ED-0405E1EFFFA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
228-
{BC21ADB7-F2CA-44F0-B6ED-0405E1EFFFA3}.Debug|x64.ActiveCfg = Debug|Any CPU
229-
{BC21ADB7-F2CA-44F0-B6ED-0405E1EFFFA3}.Debug|x64.Build.0 = Debug|Any CPU
230-
{BC21ADB7-F2CA-44F0-B6ED-0405E1EFFFA3}.Debug|x86.ActiveCfg = Debug|Any CPU
231-
{BC21ADB7-F2CA-44F0-B6ED-0405E1EFFFA3}.Debug|x86.Build.0 = Debug|Any CPU
232-
{BC21ADB7-F2CA-44F0-B6ED-0405E1EFFFA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
233-
{BC21ADB7-F2CA-44F0-B6ED-0405E1EFFFA3}.Release|Any CPU.Build.0 = Release|Any CPU
234-
{BC21ADB7-F2CA-44F0-B6ED-0405E1EFFFA3}.Release|x64.ActiveCfg = Release|Any CPU
235-
{BC21ADB7-F2CA-44F0-B6ED-0405E1EFFFA3}.Release|x64.Build.0 = Release|Any CPU
236-
{BC21ADB7-F2CA-44F0-B6ED-0405E1EFFFA3}.Release|x86.ActiveCfg = Release|Any CPU
237-
{BC21ADB7-F2CA-44F0-B6ED-0405E1EFFFA3}.Release|x86.Build.0 = Release|Any CPU
238-
{4E69FCF6-AE76-4F6D-98B8-969E9D244AE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
239-
{4E69FCF6-AE76-4F6D-98B8-969E9D244AE4}.Debug|Any CPU.Build.0 = Debug|Any CPU
240-
{4E69FCF6-AE76-4F6D-98B8-969E9D244AE4}.Debug|x64.ActiveCfg = Debug|Any CPU
241-
{4E69FCF6-AE76-4F6D-98B8-969E9D244AE4}.Debug|x64.Build.0 = Debug|Any CPU
242-
{4E69FCF6-AE76-4F6D-98B8-969E9D244AE4}.Debug|x86.ActiveCfg = Debug|Any CPU
243-
{4E69FCF6-AE76-4F6D-98B8-969E9D244AE4}.Debug|x86.Build.0 = Debug|Any CPU
244-
{4E69FCF6-AE76-4F6D-98B8-969E9D244AE4}.Release|Any CPU.ActiveCfg = Release|Any CPU
245-
{4E69FCF6-AE76-4F6D-98B8-969E9D244AE4}.Release|Any CPU.Build.0 = Release|Any CPU
246-
{4E69FCF6-AE76-4F6D-98B8-969E9D244AE4}.Release|x64.ActiveCfg = Release|Any CPU
247-
{4E69FCF6-AE76-4F6D-98B8-969E9D244AE4}.Release|x64.Build.0 = Release|Any CPU
248-
{4E69FCF6-AE76-4F6D-98B8-969E9D244AE4}.Release|x86.ActiveCfg = Release|Any CPU
249-
{4E69FCF6-AE76-4F6D-98B8-969E9D244AE4}.Release|x86.Build.0 = Release|Any CPU
229+
{E02DF032-C17F-4D0C-9CAA-3BD3CC9E4F84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
230+
{E02DF032-C17F-4D0C-9CAA-3BD3CC9E4F84}.Debug|Any CPU.Build.0 = Debug|Any CPU
231+
{E02DF032-C17F-4D0C-9CAA-3BD3CC9E4F84}.Debug|x64.ActiveCfg = Debug|Any CPU
232+
{E02DF032-C17F-4D0C-9CAA-3BD3CC9E4F84}.Debug|x64.Build.0 = Debug|Any CPU
233+
{E02DF032-C17F-4D0C-9CAA-3BD3CC9E4F84}.Debug|x86.ActiveCfg = Debug|Any CPU
234+
{E02DF032-C17F-4D0C-9CAA-3BD3CC9E4F84}.Debug|x86.Build.0 = Debug|Any CPU
235+
{E02DF032-C17F-4D0C-9CAA-3BD3CC9E4F84}.Release|Any CPU.ActiveCfg = Release|Any CPU
236+
{E02DF032-C17F-4D0C-9CAA-3BD3CC9E4F84}.Release|Any CPU.Build.0 = Release|Any CPU
237+
{E02DF032-C17F-4D0C-9CAA-3BD3CC9E4F84}.Release|x64.ActiveCfg = Release|Any CPU
238+
{E02DF032-C17F-4D0C-9CAA-3BD3CC9E4F84}.Release|x64.Build.0 = Release|Any CPU
239+
{E02DF032-C17F-4D0C-9CAA-3BD3CC9E4F84}.Release|x86.ActiveCfg = Release|Any CPU
240+
{E02DF032-C17F-4D0C-9CAA-3BD3CC9E4F84}.Release|x86.Build.0 = Release|Any CPU
241+
{DDB9C401-6B1F-4727-A4CB-932034FBF94E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
242+
{DDB9C401-6B1F-4727-A4CB-932034FBF94E}.Debug|Any CPU.Build.0 = Debug|Any CPU
243+
{DDB9C401-6B1F-4727-A4CB-932034FBF94E}.Debug|x64.ActiveCfg = Debug|Any CPU
244+
{DDB9C401-6B1F-4727-A4CB-932034FBF94E}.Debug|x64.Build.0 = Debug|Any CPU
245+
{DDB9C401-6B1F-4727-A4CB-932034FBF94E}.Debug|x86.ActiveCfg = Debug|Any CPU
246+
{DDB9C401-6B1F-4727-A4CB-932034FBF94E}.Debug|x86.Build.0 = Debug|Any CPU
247+
{DDB9C401-6B1F-4727-A4CB-932034FBF94E}.Release|Any CPU.ActiveCfg = Release|Any CPU
248+
{DDB9C401-6B1F-4727-A4CB-932034FBF94E}.Release|Any CPU.Build.0 = Release|Any CPU
249+
{DDB9C401-6B1F-4727-A4CB-932034FBF94E}.Release|x64.ActiveCfg = Release|Any CPU
250+
{DDB9C401-6B1F-4727-A4CB-932034FBF94E}.Release|x64.Build.0 = Release|Any CPU
251+
{DDB9C401-6B1F-4727-A4CB-932034FBF94E}.Release|x86.ActiveCfg = Release|Any CPU
252+
{DDB9C401-6B1F-4727-A4CB-932034FBF94E}.Release|x86.Build.0 = Release|Any CPU
250253
EndGlobalSection
251254
GlobalSection(SolutionProperties) = preSolution
252255
HideSolutionNode = FALSE
@@ -266,9 +269,8 @@ Global
266269
{B37CA136-3F20-4D8A-9677-E3A9C9D893EF} = {E14F66D1-EA3E-40C6-835A-91A4382D4646}
267270
{D8757F0F-254E-495F-961F-0192F8C97E3F} = {E14F66D1-EA3E-40C6-835A-91A4382D4646}
268271
{CBB98134-92F5-487D-8CA3-84C19FF46775} = {E14F66D1-EA3E-40C6-835A-91A4382D4646}
269-
{7E6EA8BA-EE8B-450E-AE89-C4604C0DD326} = {E14F66D1-EA3E-40C6-835A-91A4382D4646}
270-
{BC21ADB7-F2CA-44F0-B6ED-0405E1EFFFA3} = {7E6EA8BA-EE8B-450E-AE89-C4604C0DD326}
271-
{4E69FCF6-AE76-4F6D-98B8-969E9D244AE4} = {7E6EA8BA-EE8B-450E-AE89-C4604C0DD326}
272+
{E02DF032-C17F-4D0C-9CAA-3BD3CC9E4F84} = {3C549079-A502-4B40-B051-5278915AE91B}
273+
{DDB9C401-6B1F-4727-A4CB-932034FBF94E} = {3C549079-A502-4B40-B051-5278915AE91B}
272274
EndGlobalSection
273275
GlobalSection(ExtensibilityGlobals) = postSolution
274276
SolutionGuid = {3DAD5980-4688-4794-9CF0-6F3CB67194E7}

build/Program.cs

+2
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ internal static async Task Main(string[] args)
5959
Run("dotnet", $"pack ./src/Duende.Bff/Duende.Bff.csproj -c Release -o {Directory.CreateDirectory(packOutput).FullName} --no-build --nologo");
6060
Run("dotnet", $"pack ./src/Duende.Bff.EntityFramework/Duende.Bff.EntityFramework.csproj -c Release -o {Directory.CreateDirectory(packOutput).FullName} --no-build --nologo");
6161
Run("dotnet", $"pack ./src/Duende.Bff.Yarp/Duende.Bff.Yarp.csproj -c Release -o {Directory.CreateDirectory(packOutput).FullName} --no-build --nologo");
62+
Run("dotnet", $"pack ./src/Duende.Bff.Blazor/Duende.Bff.Blazor.csproj -c Release -o {Directory.CreateDirectory(packOutput).FullName} --no-build --nologo");
63+
Run("dotnet", $"pack ./src/Duende.Bff.Blazor.Client/Duende.Bff.Blazor.Client.csproj -c Release -o {Directory.CreateDirectory(packOutput).FullName} --no-build --nologo");
6264
});
6365

6466
Target(Targets.SignPackage, DependsOn(Targets.Pack, Targets.RestoreTools), () =>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// See LICENSE in the project root for license information.
3+
4+
namespace Duende.Bff.Blazor.Client;
5+
6+
public class AntiforgeryHandler : DelegatingHandler
7+
{
8+
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
9+
CancellationToken cancellationToken)
10+
{
11+
request.Headers.Add("X-CSRF", "1");
12+
return base.SendAsync(request, cancellationToken);
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// See LICENSE in the project root for license information.
3+
4+
namespace Duende.Bff.Blazor.Client;
5+
6+
/// <summary>
7+
/// Options for Blazor BFF
8+
/// </summary>
9+
public class BffBlazorOptions
10+
{
11+
/// <summary>
12+
/// The base path to use for remote APIs.
13+
/// </summary>
14+
public string RemoteApiPath { get; set; } = "remote-apis/";
15+
16+
/// <summary>
17+
/// The base address to use for remote APIs. If unset (the default), the
18+
/// blazor hosting environment's base address is used.
19+
/// </summary>
20+
public string? RemoteApiBaseAddress { get; set; } = null;
21+
22+
/// <summary>
23+
/// The delay, in milliseconds, before the AuthenticationStateProvider
24+
/// will start polling the /bff/user endpoint. Defaults to 1000 ms.
25+
/// </summary>
26+
public int StateProviderPollingDelay { get; set; } = 1000;
27+
28+
/// <summary>
29+
/// The delay, in milliseconds, between polling requests by the
30+
/// AuthenticationStateProvider to the /bff/user endpoint. Defaults to
31+
/// 5000 ms.
32+
/// </summary>
33+
public int StateProviderPollingInterval { get; set; } = 5000;
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// See LICENSE in the project root for license information.
3+
4+
using System.Net.Http.Json;
5+
using System.Security.Claims;
6+
using Microsoft.Extensions.Logging;
7+
using Microsoft.AspNetCore.Components;
8+
using Microsoft.AspNetCore.Components.Authorization;
9+
using Microsoft.Extensions.Options;
10+
11+
namespace Duende.Bff.Blazor.Client;
12+
13+
public class BffClientAuthenticationStateProvider : AuthenticationStateProvider
14+
{
15+
private static readonly TimeSpan UserCacheRefreshInterval = TimeSpan.FromSeconds(60);
16+
17+
private readonly HttpClient _client;
18+
private readonly ILogger<BffClientAuthenticationStateProvider> _logger;
19+
private readonly BffBlazorOptions _options;
20+
21+
private DateTimeOffset _userLastCheck = DateTimeOffset.MinValue;
22+
private ClaimsPrincipal _cachedUser = new(new ClaimsIdentity());
23+
24+
/// <summary>
25+
/// An <see cref="AuthenticationStateProvider"/> intended for use in
26+
/// Blazor WASM. It polls the /bff/user endpoint to monitor session
27+
/// state.
28+
/// </summary>
29+
public BffClientAuthenticationStateProvider(
30+
PersistentComponentState state,
31+
IHttpClientFactory factory,
32+
IOptions<BffBlazorOptions> options,
33+
ILogger<BffClientAuthenticationStateProvider> logger)
34+
{
35+
_client = factory.CreateClient("BffAuthenticationStateProvider");
36+
_logger = logger;
37+
_cachedUser = GetPersistedUser(state);
38+
if (_cachedUser.Identity?.IsAuthenticated == true)
39+
{
40+
_userLastCheck = DateTimeOffset.Now;
41+
}
42+
43+
_options = options.Value;
44+
}
45+
46+
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
47+
{
48+
var user = await GetUser();
49+
var state = new AuthenticationState(user);
50+
51+
// Periodically
52+
if (user.Identity is { IsAuthenticated: true })
53+
{
54+
_logger.LogInformation("starting background check..");
55+
Timer? timer = null;
56+
57+
timer = new Timer(async _ =>
58+
{
59+
var currentUser = await GetUser(false);
60+
// Always notify that auth state has changed, because the user
61+
// management claims (usually) change over time.
62+
//
63+
// Future TODO - Someday we may want an extensibility point. If the
64+
// user management claims have been customized, then auth state
65+
// wouldn't always change. In that case, we'd want to only fire
66+
// if the user actually had changed.
67+
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(currentUser)));
68+
69+
if (currentUser!.Identity!.IsAuthenticated == false)
70+
{
71+
_logger.LogInformation("user logged out");
72+
73+
if (timer != null)
74+
{
75+
await timer.DisposeAsync();
76+
}
77+
}
78+
}, null, _options.StateProviderPollingDelay, _options.StateProviderPollingInterval);
79+
}
80+
81+
return state;
82+
}
83+
84+
private async ValueTask<ClaimsPrincipal> GetUser(bool useCache = true)
85+
{
86+
var now = DateTimeOffset.Now;
87+
if (useCache && now < _userLastCheck + UserCacheRefreshInterval)
88+
{
89+
_logger.LogDebug("Taking user from cache");
90+
return _cachedUser;
91+
}
92+
93+
_logger.LogDebug("Fetching user");
94+
_cachedUser = await FetchUser();
95+
_userLastCheck = now;
96+
97+
return _cachedUser;
98+
}
99+
100+
// TODO - Consider using ClaimLite instead here
101+
record ClaimRecord(string Type, object Value);
102+
103+
private async Task<ClaimsPrincipal> FetchUser()
104+
{
105+
try
106+
{
107+
_logger.LogInformation("Fetching user information.");
108+
var response = await _client.GetAsync("bff/user?slide=false");
109+
response.EnsureSuccessStatusCode();
110+
var claims = await response.Content.ReadFromJsonAsync<List<ClaimRecord>>();
111+
112+
var identity = new ClaimsIdentity(
113+
nameof(BffClientAuthenticationStateProvider),
114+
"name",
115+
"role");
116+
117+
if (claims != null)
118+
{
119+
foreach (var claim in claims)
120+
{
121+
identity.AddClaim(new Claim(claim.Type, claim.Value.ToString() ?? "no value"));
122+
}
123+
}
124+
125+
return new ClaimsPrincipal(identity);
126+
}
127+
catch (Exception ex)
128+
{
129+
_logger.LogWarning(ex, "Fetching user failed.");
130+
}
131+
132+
return new ClaimsPrincipal(new ClaimsIdentity());
133+
}
134+
135+
private ClaimsPrincipal GetPersistedUser(PersistentComponentState state)
136+
{
137+
if (!state.TryTakeFromJson<ClaimsPrincipalLite>(nameof(ClaimsPrincipalLite), out var lite) || lite is null)
138+
{
139+
_logger.LogDebug("Failed to load persisted user.");
140+
return new ClaimsPrincipal(new ClaimsIdentity());
141+
}
142+
143+
_logger.LogDebug("Persisted user loaded.");
144+
145+
return lite.ToClaimsPrincipal();
146+
}
147+
}
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// See LICENSE in the project root for license information.
3+
4+
namespace Duende.Bff.Blazor.Client;
5+
6+
// TODO - Consider consolidating this and Duende.Bff.ClaimLite
7+
8+
/// <summary>
9+
/// Serialization friendly claim
10+
/// </summary>
11+
public class ClaimLite
12+
{
13+
/// <summary>
14+
/// The type
15+
/// </summary>
16+
public string Type { get; init; } = default!;
17+
18+
/// <summary>
19+
/// The value
20+
/// </summary>
21+
public string Value { get; init; } = default!;
22+
23+
/// <summary>
24+
/// The value type
25+
/// </summary>
26+
public string? ValueType { get; init; }
27+
}

0 commit comments

Comments
 (0)