Skip to content

Commit 70532cf

Browse files
authored
Notifications on AuthenticationState update & expiry (#55)
1 parent 9304c3d commit 70532cf

17 files changed

Lines changed: 275 additions & 73 deletions

src/BitzArt.Blazor.Auth.Client/Extensions/ClientSideAddBlazorAuthExtension.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,16 @@ public static class ClientSideAddBlazorAuthExtension
1414
/// Adds client-side Blazor.Auth services to the specified <see cref="WebAssemblyHostBuilder"/>.
1515
/// </summary>
1616
/// <param name="builder">The <see cref="WebAssemblyHostBuilder"/> to add services to.</param>
17+
/// <param name="configure">An <see cref="Action"/> to configure <see cref="BlazorAuthOptions"/>.</param>
1718
/// <returns><see cref="WebAssemblyHostBuilder"/> to allow chaining.</returns>
18-
public static WebAssemblyHostBuilder AddBlazorAuth(this WebAssemblyHostBuilder builder)
19+
public static WebAssemblyHostBuilder AddBlazorAuth(this WebAssemblyHostBuilder builder, Action<BlazorAuthOptions>? configure = null)
1920
{
21+
var options = new BlazorAuthOptions();
22+
23+
configure?.Invoke(options);
24+
25+
builder.Services.AddSingleton(options);
26+
2027
builder.AddBlazorCookies();
2128
builder.Services.AddScoped<IBlazorAuthLogger, BlazorAuthLogger>();
2229

src/BitzArt.Blazor.Auth.Client/Services/BlazorHostHttpClient.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ public async Task<TResponse> PostAsync<TResponse>(string requestUri, object valu
2525
return result;
2626
}
2727

28+
public async Task<TResponse> PostAsync<TResponse>(string requestUri, CancellationToken cancellationToken = default)
29+
{
30+
var response = await _httpClient.PostAsync(requestUri, null, cancellationToken);
31+
var result = await ParseResponseAsync<TResponse>(response, cancellationToken);
32+
33+
return result;
34+
}
35+
2836
public async Task<HttpResponseMessage> GetAsync(string requestUri, CancellationToken cancellationToken = default)
2937
=> await _httpClient.PostAsync(requestUri, null, cancellationToken);
3038

src/BitzArt.Blazor.Auth.Client/Services/UserService.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,15 @@
44
namespace BitzArt.Blazor.Auth.Client;
55

66
// Client-side implementation of the user service.
7-
internal class UserService(BlazorHostHttpClient hostClient) : IUserService
7+
internal class UserService(BlazorHostHttpClient hostClient) : IUserService, IAuthStateUpdateNotifier
88
{
99
private protected readonly BlazorHostHttpClient HostClient = hostClient;
1010

11+
public event IAuthStateUpdateNotifier.AuthenticationStateUpdatedEventHandler? AuthenticationStateUpdated;
12+
13+
protected void NotifyAuthenticationStateUpdated(AuthenticationOperationInfo? authInfo)
14+
=> AuthenticationStateUpdated?.Invoke(this, authInfo);
15+
1116
public async Task<AuthenticationState> GetAuthenticationStateAsync(CancellationToken cancellationToken = default)
1217
{
1318
var response = await HostClient.GetAsync<ClaimsPrincipalDto>("/_auth/me", cancellationToken);
@@ -19,17 +24,30 @@ public async Task<AuthenticationState> GetAuthenticationStateAsync(CancellationT
1924
return new AuthenticationState(principal);
2025
}
2126

27+
public async Task<AuthenticationOperationInfo> RefreshJwtPairAsync(CancellationToken cancellationToken = default)
28+
{
29+
var result = await HostClient.PostAsync<AuthenticationOperationInfo>("/_auth/refresh", cancellationToken);
30+
31+
NotifyAuthenticationStateUpdated(result);
32+
33+
return result;
34+
}
35+
2236
public async Task<AuthenticationOperationInfo> RefreshJwtPairAsync(string refreshToken, CancellationToken cancellationToken = default)
2337
{
2438
var result = await HostClient.PostAsync<AuthenticationOperationInfo>("/_auth/refresh", refreshToken, cancellationToken);
2539

40+
NotifyAuthenticationStateUpdated(result);
41+
2642
return result;
2743
}
2844

2945
public async Task SignOutAsync(CancellationToken cancellationToken = default)
3046
{
3147
var response = await HostClient.PostAsync("/_auth/sign-out", cancellationToken);
3248

49+
NotifyAuthenticationStateUpdated(null);
50+
3351
response.Validate();
3452
}
3553
}
@@ -41,6 +59,8 @@ public async Task<AuthenticationOperationInfo> SignInAsync(TSignInPayload signIn
4159
{
4260
var result = await HostClient.PostAsync<AuthenticationOperationInfo>("/_auth/sign-in", signInPayload!, cancellationToken);
4361

62+
NotifyAuthenticationStateUpdated(result);
63+
4464
return result;
4565
}
4666
}
@@ -52,6 +72,8 @@ public async Task<AuthenticationOperationInfo> SignUpAsync(TSignUpPayload signUp
5272
{
5373
var result = await HostClient.PostAsync<AuthenticationOperationInfo>("/_auth/sign-up", signUpPayload!, cancellationToken);
5474

75+
NotifyAuthenticationStateUpdated(result);
76+
5577
return result;
5678
}
5779
}

src/BitzArt.Blazor.Auth.Server/Endpoints/MapAuthEndpointsExtension.Refresh.cs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
using Microsoft.AspNetCore.Http;
33
using Microsoft.AspNetCore.Mvc;
44
using Microsoft.AspNetCore.Routing;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using System.Diagnostics;
57
using System.Text.Json;
68

79
namespace BitzArt.Blazor.Auth.Server;
@@ -11,19 +13,29 @@ public static partial class MapAuthEndpointsExtension
1113
private static IEndpointRouteBuilder MapAuthRefreshEndpoint(this IEndpointRouteBuilder builder)
1214
{
1315
builder.MapPost("/_auth/refresh", async (
14-
[FromServices] IAuthenticationService authService,
16+
[FromServices] IServiceProvider serviceProvider,
1517
[FromServices] IHttpContextAccessor httpContextAccessor,
1618
CancellationToken cancellationToken = default) =>
1719
{
18-
var context = httpContextAccessor.HttpContext;
19-
using StreamReader reader = new(context!.Request.Body);
20+
var userService = serviceProvider.GetRequiredService<StaticUserService>();
21+
22+
var context = httpContextAccessor.HttpContext
23+
?? throw new InvalidOperationException("The HttpContext is not available.");
24+
25+
using StreamReader reader = new(context.Request.Body);
2026
var bodyAsString = await reader.ReadToEndAsync(cancellationToken);
21-
var refreshToken = JsonSerializer.Deserialize<string>(bodyAsString, Constants.JsonSerializerOptions);
2227

23-
var result = await authService.RefreshJwtPairAsync(refreshToken!, cancellationToken);
24-
var info = result.GetInfo();
28+
var refreshToken = string.IsNullOrWhiteSpace(bodyAsString)
29+
? null
30+
: JsonSerializer.Deserialize<string?>(bodyAsString, Constants.JsonSerializerOptions);
31+
32+
var result = refreshToken switch
33+
{
34+
null => await userService.RefreshJwtPairAsync(cancellationToken),
35+
string => await userService.RefreshJwtPairAsync(refreshToken, cancellationToken)
36+
};
2537

26-
return Results.Ok(info);
38+
return Results.Ok(result);
2739
});
2840

2941
return builder;

src/BitzArt.Blazor.Auth.Server/Endpoints/MapAuthEndpointsExtension.SignIn.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ private static IEndpointRouteBuilder MapAuthSignInEndpoint(this IEndpointRouteBu
2323
if (payloadType is null)
2424
return Results.BadRequest("The registered IAuthenticationService does not implement Sign-In functionality.");
2525

26-
var userService = serviceProvider.GetRequiredService<StaticUserService>()
27-
?? throw new UnreachableException();
26+
var userService = serviceProvider.GetRequiredService<StaticUserService>();
2827

2928
var context = httpContextAccessor.HttpContext
3029
?? throw new InvalidOperationException("The HttpContext is not available.");

src/BitzArt.Blazor.Auth.Server/Endpoints/MapAuthEndpointsExtension.SignUp.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ private static IEndpointRouteBuilder MapAuthSignUpEndpoint(this IEndpointRouteBu
2323
if (payloadType is null)
2424
return Results.BadRequest("The registered IAuthenticationService does not implement Sign-Up functionality.");
2525

26-
var userService = serviceProvider.GetRequiredService<StaticUserService>()
27-
?? throw new UnreachableException();
26+
var userService = serviceProvider.GetRequiredService<StaticUserService>();
2827

2928
var context = httpContextAccessor.HttpContext
3029
?? throw new InvalidOperationException("The HttpContext is not available.");

src/BitzArt.Blazor.Auth.Server/Extensions/AddUserServiceExtension.cs renamed to src/BitzArt.Blazor.Auth.Server/Extensions/AddUserServiceExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace BitzArt.Blazor.Auth.Server;
44

5-
internal static class AddUserServiceExtension
5+
internal static class AddUserServiceExtensions
66
{
77
public static IServiceCollection AddUserService(this IServiceCollection services, AuthenticationServiceSignature authServiceSignature)
88
{

src/BitzArt.Blazor.Auth.Server/Extensions/ServerSideAddBlazorAuthExtensions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,11 @@ public static IHostApplicationBuilder AddBlazorAuth<TAuthenticationService, TIde
4444
where TIdentityClaimsService : class, IIdentityClaimsService
4545
{
4646
var options = new BlazorAuthServerOptions();
47+
4748
configure?.Invoke(options);
49+
4850
builder.Services.AddSingleton(options);
51+
builder.Services.AddSingleton<BlazorAuthOptions>(options);
4952

5053
builder.AddBlazorCookies();
5154
builder.Services.AddScoped<IBlazorAuthLogger, BlazorAuthLogger>();

src/BitzArt.Blazor.Auth.Server/Options/BlazorAuthServerOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
/// <summary>
44
/// Options for the Blazor Auth Server.
55
/// </summary>
6-
public class BlazorAuthServerOptions
6+
public class BlazorAuthServerOptions : BlazorAuthOptions
77
{
88
/// <summary>
99
/// Allows the app to operate in a non-HTTPS environment.

src/BitzArt.Blazor.Auth.Server/Services/InteractiveUserService.cs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,17 @@ namespace BitzArt.Blazor.Auth.Server;
1010
internal class InteractiveUserService(
1111
IBlazorAuthLogger logger,
1212
NavigationManager navigation,
13-
IJSRuntime js) : IUserService
13+
IJSRuntime js) : IUserService, IAuthStateUpdateNotifier
1414
{
1515
private protected readonly IBlazorAuthLogger Logger = logger;
1616
private protected readonly NavigationManager Navigation = navigation;
1717
private protected readonly IJSRuntime Js = js;
1818

19+
public event IAuthStateUpdateNotifier.AuthenticationStateUpdatedEventHandler? AuthenticationStateUpdated;
20+
21+
protected void NotifyAuthenticationStateUpdated(AuthenticationOperationInfo? authInfo)
22+
=> AuthenticationStateUpdated?.Invoke(this, authInfo);
23+
1924
public async Task<AuthenticationState> GetAuthenticationStateAsync(CancellationToken cancellationToken = default)
2025
=> await DoWhileLogging(async ()
2126
=> await DoWithJsModule(async (module)
@@ -34,6 +39,25 @@ public async Task<AuthenticationState> GetAuthenticationStateAsync(CancellationT
3439
return new AuthenticationState(principal);
3540
}));
3641

42+
public async Task<AuthenticationOperationInfo> RefreshJwtPairAsync(CancellationToken cancellationToken = default)
43+
=> await DoWhileLogging(async ()
44+
=> await DoWithJsModule(async (module)
45+
=>
46+
{
47+
var baseUri = GetBaseUri();
48+
var url = $"{baseUri.TrimEnd('/')}/_auth/refresh";
49+
50+
var result = await module.InvokeAsync<AuthenticationOperationInfo>(
51+
"requestAsync",
52+
cancellationToken: cancellationToken,
53+
[url, HttpMethod.Post.Method, null, "json"])
54+
?? throw new InvalidOperationException("Failed to deserialize the authentication result info.");
55+
56+
NotifyAuthenticationStateUpdated(result);
57+
58+
return result;
59+
}));
60+
3761
public async Task<AuthenticationOperationInfo> RefreshJwtPairAsync(string refreshToken, CancellationToken cancellationToken = default)
3862
=> await DoWhileLogging(async ()
3963
=> await DoWithJsModule(async (module)
@@ -48,6 +72,8 @@ public async Task<AuthenticationOperationInfo> RefreshJwtPairAsync(string refres
4872
[url, HttpMethod.Post.Method, refreshToken, "json"])
4973
?? throw new InvalidOperationException("Failed to deserialize the authentication result info.");
5074

75+
NotifyAuthenticationStateUpdated(result);
76+
5177
return result;
5278
}));
5379

@@ -64,6 +90,8 @@ await module.InvokeVoidAsync(
6490
cancellationToken: cancellationToken,
6591
[url, HttpMethod.Post.Method, null, null]);
6692

93+
NotifyAuthenticationStateUpdated(null);
94+
6795
return true;
6896
}));
6997

0 commit comments

Comments
 (0)