feat(ui/auth): redirect to /login when the session times out
Previously a user idling past the 30-minute cookie expiry stayed parked on a stale page until they tried to navigate. The auth cookie's UTC expiry is now also stamped onto an expires_at claim at sign-in, and a SessionExpiry component mounted in MainLayout schedules a delay until expiry + 2s grace, then force-loads /login — at which point the standard cookie middleware confirms the session is gone and serves the login page.
This commit is contained in:
@@ -44,12 +44,15 @@ public static class AuthEndpoints
|
||||
// Map LDAP groups to roles
|
||||
var roleMappingResult = await roleMapper.MapGroupsToRolesAsync(authResult.Groups ?? []);
|
||||
|
||||
var expiresAt = DateTimeOffset.UtcNow.AddMinutes(30);
|
||||
|
||||
// Build claims from LDAP auth + role mapping
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(ClaimTypes.Name, authResult.Username ?? username),
|
||||
new(JwtTokenService.DisplayNameClaimType, authResult.DisplayName ?? username),
|
||||
new(JwtTokenService.UsernameClaimType, authResult.Username ?? username),
|
||||
new("expires_at", expiresAt.ToUnixTimeSeconds().ToString()),
|
||||
};
|
||||
|
||||
foreach (var role in roleMappingResult.Roles)
|
||||
@@ -74,7 +77,7 @@ public static class AuthEndpoints
|
||||
new AuthenticationProperties
|
||||
{
|
||||
IsPersistent = true,
|
||||
ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(30)
|
||||
ExpiresUtc = expiresAt
|
||||
});
|
||||
|
||||
context.Response.Redirect("/");
|
||||
|
||||
@@ -25,3 +25,5 @@
|
||||
@* Global host for IDialogService. One instance per layout renders all confirm/prompt
|
||||
dialogs raised via IDialogService.ConfirmAsync / PromptAsync. *@
|
||||
<DialogHost />
|
||||
|
||||
<SessionExpiry />
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
@implements IDisposable
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
@code {
|
||||
private CancellationTokenSource? _cts;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var auth = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
if (auth.User.Identity?.IsAuthenticated != true) return;
|
||||
|
||||
var claim = auth.User.FindFirst("expires_at")?.Value;
|
||||
if (!long.TryParse(claim, out var unix)) return;
|
||||
|
||||
var remaining = DateTimeOffset.FromUnixTimeSeconds(unix) - DateTimeOffset.UtcNow;
|
||||
if (remaining <= TimeSpan.Zero)
|
||||
{
|
||||
Navigation.NavigateTo("/login", forceLoad: true);
|
||||
return;
|
||||
}
|
||||
|
||||
_cts = new CancellationTokenSource();
|
||||
_ = ScheduleRedirectAsync(remaining + TimeSpan.FromSeconds(2), _cts.Token);
|
||||
}
|
||||
|
||||
private async Task ScheduleRedirectAsync(TimeSpan delay, CancellationToken token)
|
||||
{
|
||||
try { await Task.Delay(delay, token); }
|
||||
catch (TaskCanceledException) { return; }
|
||||
await InvokeAsync(() => Navigation.NavigateTo("/login", forceLoad: true));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user