diff --git a/Directory.Build.targets b/Directory.Build.targets index de1c12d..f6ea513 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -17,7 +17,9 @@ - + + + @@ -30,6 +32,7 @@ + diff --git a/samples/Blazor/PerComponent/PerComponent.Client/Components/Auto.razor b/samples/Blazor/PerComponent/PerComponent.Client/Components/Auto.razor index c8cf14b..ccf4a55 100644 --- a/samples/Blazor/PerComponent/PerComponent.Client/Components/Auto.razor +++ b/samples/Blazor/PerComponent/PerComponent.Client/Components/Auto.razor @@ -1,8 +1,3 @@ @rendermode InteractiveAuto - -@code { - [CascadingParameter] - private Task? authenticationState { get; set; } -} diff --git a/samples/Blazor/PerComponent/PerComponent.Client/Components/CallApi.razor b/samples/Blazor/PerComponent/PerComponent.Client/Components/CallApi.razor index 75ac46a..476b822 100644 --- a/samples/Blazor/PerComponent/PerComponent.Client/Components/CallApi.razor +++ b/samples/Blazor/PerComponent/PerComponent.Client/Components/CallApi.razor @@ -37,7 +37,9 @@ protected async Task CallApiAsync() { + DisableUi = true; apiResult = await Http.GetFromJsonAsync("user-token"); + DisableUi = false; } protected override void OnAfterRender(bool firstRender) diff --git a/samples/Blazor/PerComponent/PerComponent.Client/Components/Wasm.razor b/samples/Blazor/PerComponent/PerComponent.Client/Components/Wasm.razor index f44d929..1e556d9 100644 --- a/samples/Blazor/PerComponent/PerComponent.Client/Components/Wasm.razor +++ b/samples/Blazor/PerComponent/PerComponent.Client/Components/Wasm.razor @@ -1,12 +1,3 @@ @rendermode InteractiveWebAssembly - -@* TODO - This cascading auth state gets the auth state provider running. But -actually, we don't need the auth state provider to be running. I thought it was -weird that it wasn't, so this forces it to do so, but actually we're not doing -anything with the state changes. *@ -@code { - [CascadingParameter] - private Task? authenticationState { get; set; } -} diff --git a/samples/Blazor/PerComponent/PerComponent.Client/PerComponent.Client.csproj b/samples/Blazor/PerComponent/PerComponent.Client/PerComponent.Client.csproj index 0f7e952..01cbcc0 100644 --- a/samples/Blazor/PerComponent/PerComponent.Client/PerComponent.Client.csproj +++ b/samples/Blazor/PerComponent/PerComponent.Client/PerComponent.Client.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/samples/Blazor/PerComponent/PerComponent.Client/Program.cs b/samples/Blazor/PerComponent/PerComponent.Client/Program.cs index 358897f..cf9f8b9 100644 --- a/samples/Blazor/PerComponent/PerComponent.Client/Program.cs +++ b/samples/Blazor/PerComponent/PerComponent.Client/Program.cs @@ -7,7 +7,9 @@ builder.Services.AddScoped(); -builder.Services.AddBff(); -builder.Services.AddRemoteApiHttpClient("callApi"); +builder.Services + .AddBffBlazorClient() + .AddCascadingAuthenticationState() + .AddRemoteApiHttpClient("callApi"); await builder.Build().RunAsync(); diff --git a/samples/Blazor/PerComponent/PerComponent/PerComponent.csproj b/samples/Blazor/PerComponent/PerComponent/PerComponent.csproj index d60e950..2ff5737 100644 --- a/samples/Blazor/PerComponent/PerComponent/PerComponent.csproj +++ b/samples/Blazor/PerComponent/PerComponent/PerComponent.csproj @@ -7,7 +7,7 @@ - + diff --git a/samples/Blazor/PerComponent/PerComponent/Program.cs b/samples/Blazor/PerComponent/PerComponent/Program.cs index bc3ddcf..f10bb9c 100644 --- a/samples/Blazor/PerComponent/PerComponent/Program.cs +++ b/samples/Blazor/PerComponent/PerComponent/Program.cs @@ -7,20 +7,21 @@ var builder = WebApplication.CreateBuilder(args); -// Add services to the container. +// BFF setup for blazor builder.Services.AddBff() .AddServerSideSessions() .AddBlazorServer() .AddRemoteApis(); - -builder.Services.AddCascadingAuthenticationState(); - -builder.Services.AddScoped(); builder.Services.AddUserAccessTokenHttpClient("callApi", configureClient: client => client.BaseAddress = new Uri("https://localhost:5010/")); +// General blazor services builder.Services.AddRazorComponents() .AddInteractiveServerComponents() .AddInteractiveWebAssemblyComponents(); +builder.Services.AddCascadingAuthenticationState(); + +// Service used by the sample to describe where code is running +builder.Services.AddScoped(); builder.Services.AddAuthentication(options => { diff --git a/samples/Blazor/PerComponent/PerComponent/ServerRenderModeContext.cs b/samples/Blazor/PerComponent/PerComponent/ServerRenderModeContext.cs index 11e10d7..27abf02 100644 --- a/samples/Blazor/PerComponent/PerComponent/ServerRenderModeContext.cs +++ b/samples/Blazor/PerComponent/PerComponent/ServerRenderModeContext.cs @@ -10,7 +10,8 @@ RenderMode IRenderModeContext.GetMode() if(prerendering) { return RenderMode.Prerender; - } else + } + else { return RenderMode.Server; } diff --git a/samples/Blazor/WebAssembly/WebAssembly.Client/Program.cs b/samples/Blazor/WebAssembly/WebAssembly.Client/Program.cs index b30bbf4..1348424 100644 --- a/samples/Blazor/WebAssembly/WebAssembly.Client/Program.cs +++ b/samples/Blazor/WebAssembly/WebAssembly.Client/Program.cs @@ -3,7 +3,8 @@ var builder = WebAssemblyHostBuilder.CreateDefault(args); -// authentication state and authorization -builder.Services.AddBff(); +builder.Services + .AddBffBlazorClient() // Provides auth state provider that polls the /bff/user endpoint + .AddCascadingAuthenticationState(); await builder.Build().RunAsync(); diff --git a/samples/Blazor/WebAssembly/WebAssembly.Client/WebAssembly.Client.csproj b/samples/Blazor/WebAssembly/WebAssembly.Client/WebAssembly.Client.csproj index 0984e58..b51a817 100644 --- a/samples/Blazor/WebAssembly/WebAssembly.Client/WebAssembly.Client.csproj +++ b/samples/Blazor/WebAssembly/WebAssembly.Client/WebAssembly.Client.csproj @@ -9,9 +9,9 @@ - - - + + + diff --git a/samples/Blazor/WebAssembly/WebAssembly/Program.cs b/samples/Blazor/WebAssembly/WebAssembly/Program.cs index d7362f6..49ed54e 100644 --- a/samples/Blazor/WebAssembly/WebAssembly/Program.cs +++ b/samples/Blazor/WebAssembly/WebAssembly/Program.cs @@ -5,7 +5,6 @@ var builder = WebApplication.CreateBuilder(args); -// Add services to the container. builder.Services.AddRazorComponents() .AddInteractiveWebAssemblyComponents(); diff --git a/src/Duende.Bff.Blazor.Client/BffBlazorOptions.cs b/src/Duende.Bff.Blazor.Client/BffBlazorOptions.cs index d9b83e4..5e73bf3 100644 --- a/src/Duende.Bff.Blazor.Client/BffBlazorOptions.cs +++ b/src/Duende.Bff.Blazor.Client/BffBlazorOptions.cs @@ -19,6 +19,16 @@ public class BffBlazorOptions /// public string? RemoteApiBaseAddress { get; set; } = null; + /// + /// The delay, in milliseconds, before the AuthenticationStateProvider + /// will start polling the /bff/user endpoint. Defaults to 1000 ms. + /// public int StateProviderPollingDelay { get; set; } = 1000; + + /// + /// The delay, in milliseconds, between polling requests by the + /// AuthenticationStateProvider to the /bff/user endpoint. Defaults to + /// 5000 ms. + /// public int StateProviderPollingInterval { get; set; } = 5000; } \ No newline at end of file diff --git a/src/Duende.Bff.Blazor.Client/BffClientAuthenticationStateProvider.cs b/src/Duende.Bff.Blazor.Client/BffClientAuthenticationStateProvider.cs index b39ac29..60fa9bc 100644 --- a/src/Duende.Bff.Blazor.Client/BffClientAuthenticationStateProvider.cs +++ b/src/Duende.Bff.Blazor.Client/BffClientAuthenticationStateProvider.cs @@ -21,6 +21,11 @@ public class BffClientAuthenticationStateProvider : AuthenticationStateProvider private DateTimeOffset _userLastCheck = DateTimeOffset.MinValue; private ClaimsPrincipal _cachedUser = new(new ClaimsIdentity()); + /// + /// An intended for use in + /// Blazor WASM. It polls the /bff/user endpoint to monitor session + /// state. + /// public BffClientAuthenticationStateProvider( PersistentComponentState state, IHttpClientFactory factory, @@ -43,10 +48,8 @@ public override async Task GetAuthenticationStateAsync() var user = await GetUser(); var state = new AuthenticationState(user); - // checks periodically for a session state change and fires event - // this causes a round trip to the server - // adjust the period accordingly if that feature is needed - if (user!.Identity!.IsAuthenticated) + // Periodically + if (user.Identity is { IsAuthenticated: true }) { _logger.LogInformation("starting background check.."); Timer? timer = null; @@ -54,7 +57,13 @@ public override async Task GetAuthenticationStateAsync() timer = new Timer(async _ => { var currentUser = await GetUser(false); - // Always notify that auth state has changed, because the user management claims change over time + // Always notify that auth state has changed, because the user + // management claims (usually) change over time. + // + // Future TODO - Someday we may want an extensibility point. If the + // user management claims have been customized, then auth state + // wouldn't always change. In that case, we'd want to only fire + // if the user actually had changed. NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(currentUser))); if (currentUser!.Identity!.IsAuthenticated == false) diff --git a/src/Duende.Bff.Blazor.Client/Duende.Bff.Blazor.Client.csproj b/src/Duende.Bff.Blazor.Client/Duende.Bff.Blazor.Client.csproj index 676c7ec..26e2eb1 100644 --- a/src/Duende.Bff.Blazor.Client/Duende.Bff.Blazor.Client.csproj +++ b/src/Duende.Bff.Blazor.Client/Duende.Bff.Blazor.Client.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Duende.Bff.Blazor.Client/ServiceCollectionExtensions.cs b/src/Duende.Bff.Blazor.Client/ServiceCollectionExtensions.cs index 87ecef2..17ab858 100644 --- a/src/Duende.Bff.Blazor.Client/ServiceCollectionExtensions.cs +++ b/src/Duende.Bff.Blazor.Client/ServiceCollectionExtensions.cs @@ -10,7 +10,7 @@ namespace Duende.Bff.Blazor.Client; public static class ServiceCollectionExtensions { - public static IServiceCollection AddBff(this IServiceCollection services, + public static IServiceCollection AddBffBlazorClient(this IServiceCollection services, Action? configureAction = null) { if (configureAction != null) @@ -21,7 +21,6 @@ public static IServiceCollection AddBff(this IServiceCollection services, services .AddAuthorizationCore() .AddScoped() - .AddCascadingAuthenticationState() .AddTransient() .AddHttpClient("BffAuthenticationStateProvider", (sp, client) => { @@ -52,7 +51,7 @@ private static string GetRemoteApiPath(IServiceProvider sp) return opt.Value.RemoteApiPath; } - private static Action SetBaseAddressInConfigureClient( + private static Action SetBaseAddress( Action? configureClient) { return (sp, client) => @@ -62,7 +61,7 @@ private static Action SetBaseAddressInConfigureCli }; } - private static Action SetBaseAddressInConfigureClient( + private static Action SetBaseAddress( Action? configureClient) { return (sp, client) => @@ -100,14 +99,14 @@ private static void SetBaseAddress(IServiceProvider sp, HttpClient client) public static IHttpClientBuilder AddRemoteApiHttpClient(this IServiceCollection services, string clientName, Action configureClient) { - return services.AddHttpClient(clientName, SetBaseAddressInConfigureClient(configureClient)) + return services.AddHttpClient(clientName, SetBaseAddress(configureClient)) .AddHttpMessageHandler(); } public static IHttpClientBuilder AddRemoteApiHttpClient(this IServiceCollection services, string clientName, Action? configureClient = null) { - return services.AddHttpClient(clientName, SetBaseAddressInConfigureClient(configureClient)) + return services.AddHttpClient(clientName, SetBaseAddress(configureClient)) .AddHttpMessageHandler(); } @@ -115,7 +114,7 @@ public static IHttpClientBuilder AddRemoteApiHttpClient(this IServiceCollecti Action configureClient) where T : class { - return services.AddHttpClient(SetBaseAddressInConfigureClient(configureClient)) + return services.AddHttpClient(SetBaseAddress(configureClient)) .AddHttpMessageHandler(); } @@ -123,7 +122,7 @@ public static IHttpClientBuilder AddRemoteApiHttpClient(this IServiceCollecti Action? configureClient = null) where T : class { - return services.AddHttpClient(SetBaseAddressInConfigureClient(configureClient)) + return services.AddHttpClient(SetBaseAddress(configureClient)) .AddHttpMessageHandler(); } } \ No newline at end of file diff --git a/src/Duende.Bff.Blazor/CaptureManagementClaimsCookieEvents.cs b/src/Duende.Bff.Blazor/CaptureManagementClaimsCookieEvents.cs index 9591650..1790f9c 100644 --- a/src/Duende.Bff.Blazor/CaptureManagementClaimsCookieEvents.cs +++ b/src/Duende.Bff.Blazor/CaptureManagementClaimsCookieEvents.cs @@ -6,6 +6,12 @@ namespace Duende.Bff.Blazor; +/// +/// This subclass invokes the BFF to retrieve management claims and add them to the +/// session. This is useful in interactive render modes where components are +/// initialled rendered server side. +/// public class CaptureManagementClaimsCookieEvents : CookieAuthenticationEvents { private readonly IClaimsService _claimsService; diff --git a/src/Duende.Bff/Configuration/BffServiceCollectionExtensions.cs b/src/Duende.Bff/Configuration/BffServiceCollectionExtensions.cs index 52c8570..ff49ed4 100644 --- a/src/Duende.Bff/Configuration/BffServiceCollectionExtensions.cs +++ b/src/Duende.Bff/Configuration/BffServiceCollectionExtensions.cs @@ -55,6 +55,7 @@ public static BffBuilder AddBff(this IServiceCollection services, Action, PostConfigureSlidingExpirationCheck>(); + services.AddSingleton, PostConfigureApplicationCookieRevokeRefreshToken>(); services.AddSingleton, PostConfigureOidcOptionsForSilentLogin>(); diff --git a/src/Duende.Bff/EndpointServices/User/DefaultUserService.cs b/src/Duende.Bff/EndpointServices/User/DefaultUserService.cs index 6a82bc0..c3e5abe 100644 --- a/src/Duende.Bff/EndpointServices/User/DefaultUserService.cs +++ b/src/Duende.Bff/EndpointServices/User/DefaultUserService.cs @@ -80,7 +80,7 @@ public virtual async Task ProcessRequestAsync(HttpContext context) { // In blazor, it is sometimes necessary to copy management claims into the session. // So, we don't want duplicate mgmt claims. Instead, they should overwrite the existing mgmt claims - // (in case they changed when the session slide, etc) + // (in case they changed when the session slid, etc) var claims = (await GetUserClaimsAsync(result)).ToList(); var mgmtClaims = await GetManagementClaimsAsync(context, result); diff --git a/src/Duende.Bff/SessionManagement/Configuration/PostConfigureApplicationCookieRefreshToken.cs b/src/Duende.Bff/SessionManagement/Configuration/PostConfigureApplicationCookieRefreshToken.cs new file mode 100644 index 0000000..fbb2b1e --- /dev/null +++ b/src/Duende.Bff/SessionManagement/Configuration/PostConfigureApplicationCookieRefreshToken.cs @@ -0,0 +1,59 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + +using IdentityModel; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.Threading.Tasks; + +namespace Duende.Bff; + +/// +/// Cookie configuration to revoke refresh token on logout. +/// +public class PostConfigureApplicationCookieRevokeRefreshToken : IPostConfigureOptions +{ + private readonly BffOptions _options; + private readonly string? _scheme; + private readonly ILogger _logger; + + /// + /// ctor + /// + /// + /// + /// + public PostConfigureApplicationCookieRevokeRefreshToken(IOptions bffOptions, IOptions authOptions, ILogger logger) + { + _options = bffOptions.Value; + _scheme = authOptions.Value.DefaultAuthenticateScheme ?? authOptions.Value.DefaultScheme; + _logger = logger; + } + + /// + public void PostConfigure(string? name, CookieAuthenticationOptions options) + { + if (_options.RevokeRefreshTokenOnLogout && name == _scheme) + { + options.Events.OnSigningOut = CreateCallback(options.Events.OnSigningOut); + } + } + + private Func CreateCallback(Func inner) + { + async Task Callback(CookieSigningOutContext ctx) + { + _logger.LogDebug("Revoking user's refresh tokens in OnSigningOut for subject id: {subjectId}", ctx.HttpContext.User.FindFirst(JwtClaimTypes.Subject)?.Value); + await ctx.HttpContext.RevokeRefreshTokenAsync(); + if (inner != null) + { + await inner.Invoke(ctx); + } + }; + + return Callback; + } +}