Mise en place de l'authentification Keycloak
This commit is contained in:
@@ -10,14 +10,14 @@ public sealed class BrowserBridge(IJSRuntime jsRuntime)
|
||||
public ValueTask SetBodyStateAsync(string? page, string? bodyClass)
|
||||
=> jsRuntime.InvokeVoidAsync("chesscubingPage.setBodyState", page, bodyClass ?? string.Empty);
|
||||
|
||||
public ValueTask<string?> ReadMatchJsonAsync()
|
||||
=> jsRuntime.InvokeAsync<string?>("chesscubingStorage.getMatchState", MatchStore.StorageKey, MatchStore.WindowNameKey);
|
||||
public ValueTask<string?> ReadMatchJsonAsync(string storageKey, string windowNameKey)
|
||||
=> jsRuntime.InvokeAsync<string?>("chesscubingStorage.getMatchState", storageKey, windowNameKey);
|
||||
|
||||
public ValueTask WriteMatchJsonAsync(string json)
|
||||
=> jsRuntime.InvokeVoidAsync("chesscubingStorage.setMatchState", MatchStore.StorageKey, MatchStore.WindowNameKey, json);
|
||||
public ValueTask WriteMatchJsonAsync(string storageKey, string windowNameKey, string json)
|
||||
=> jsRuntime.InvokeVoidAsync("chesscubingStorage.setMatchState", storageKey, windowNameKey, json);
|
||||
|
||||
public ValueTask ClearMatchAsync()
|
||||
=> jsRuntime.InvokeVoidAsync("chesscubingStorage.clearMatchState", MatchStore.StorageKey);
|
||||
public ValueTask ClearMatchAsync(string storageKey, string windowNameKey)
|
||||
=> jsRuntime.InvokeVoidAsync("chesscubingStorage.clearMatchState", storageKey, windowNameKey);
|
||||
|
||||
public ValueTask<bool> PlayCubePhaseAlertAsync()
|
||||
=> jsRuntime.InvokeAsync<bool>("chesscubingAudio.playCubePhaseAlert");
|
||||
|
||||
53
ChessCubing.App/Services/KeycloakAccountFactory.cs
Normal file
53
ChessCubing.App/Services/KeycloakAccountFactory.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
|
||||
|
||||
namespace ChessCubing.App.Services;
|
||||
|
||||
public sealed class KeycloakAccountFactory(IAccessTokenProviderAccessor accessor)
|
||||
: AccountClaimsPrincipalFactory<RemoteUserAccount>(accessor)
|
||||
{
|
||||
public override async ValueTask<ClaimsPrincipal> CreateUserAsync(
|
||||
RemoteUserAccount account,
|
||||
RemoteAuthenticationUserOptions options)
|
||||
{
|
||||
var user = await base.CreateUserAsync(account, options);
|
||||
if (user.Identity is not ClaimsIdentity identity || !identity.IsAuthenticated)
|
||||
{
|
||||
return user;
|
||||
}
|
||||
|
||||
var roleClaimType = options.RoleClaim ?? "role";
|
||||
var existingRoles = identity.FindAll(roleClaimType)
|
||||
.Select(claim => claim.Value)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var role in ReadRealmRoles(account))
|
||||
{
|
||||
if (existingRoles.Add(role))
|
||||
{
|
||||
identity.AddClaim(new Claim(roleClaimType, role));
|
||||
}
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> ReadRealmRoles(RemoteUserAccount account)
|
||||
{
|
||||
if (!account.AdditionalProperties.TryGetValue("realm_access", out var realmAccessValue) ||
|
||||
realmAccessValue is not JsonElement realmAccessElement ||
|
||||
realmAccessElement.ValueKind != JsonValueKind.Object ||
|
||||
!realmAccessElement.TryGetProperty("roles", out var rolesElement) ||
|
||||
rolesElement.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return rolesElement.EnumerateArray()
|
||||
.Where(item => item.ValueKind == JsonValueKind.String)
|
||||
.Select(item => item.GetString())
|
||||
.OfType<string>();
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,10 @@ using ChessCubing.App.Models;
|
||||
|
||||
namespace ChessCubing.App.Services;
|
||||
|
||||
public sealed class MatchStore(BrowserBridge browser)
|
||||
public sealed class MatchStore(BrowserBridge browser, UserSession userSession)
|
||||
{
|
||||
public const string StorageKey = "chesscubing-arena-state-v2";
|
||||
public const string WindowNameKey = "chesscubing-arena-state-v2:";
|
||||
public const string StorageKeyPrefix = "chesscubing-arena-state-v3";
|
||||
public const string WindowNameKeyPrefix = "chesscubing-arena-state-v3";
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
@@ -16,6 +16,8 @@ public sealed class MatchStore(BrowserBridge browser)
|
||||
|
||||
private bool _dirty;
|
||||
private long _lastPersistedAt;
|
||||
private string? _activeStorageKey;
|
||||
private string? _activeWindowNameKey;
|
||||
|
||||
public MatchState? Current { get; private set; }
|
||||
|
||||
@@ -23,6 +25,7 @@ public sealed class MatchStore(BrowserBridge browser)
|
||||
|
||||
public async Task EnsureLoadedAsync()
|
||||
{
|
||||
var storageScope = await SyncStorageScopeAsync();
|
||||
if (IsLoaded)
|
||||
{
|
||||
return;
|
||||
@@ -30,7 +33,7 @@ public sealed class MatchStore(BrowserBridge browser)
|
||||
|
||||
try
|
||||
{
|
||||
var raw = await browser.ReadMatchJsonAsync();
|
||||
var raw = await browser.ReadMatchJsonAsync(storageScope.StorageKey, storageScope.WindowNameKey);
|
||||
if (!string.IsNullOrWhiteSpace(raw))
|
||||
{
|
||||
var parsed = JsonSerializer.Deserialize<MatchState>(raw, JsonOptions);
|
||||
@@ -61,6 +64,7 @@ public sealed class MatchStore(BrowserBridge browser)
|
||||
|
||||
public async Task SaveAsync()
|
||||
{
|
||||
var storageScope = await SyncStorageScopeAsync();
|
||||
if (!IsLoaded)
|
||||
{
|
||||
return;
|
||||
@@ -68,14 +72,14 @@ public sealed class MatchStore(BrowserBridge browser)
|
||||
|
||||
if (Current is null)
|
||||
{
|
||||
await browser.ClearMatchAsync();
|
||||
await browser.ClearMatchAsync(storageScope.StorageKey, storageScope.WindowNameKey);
|
||||
_dirty = false;
|
||||
_lastPersistedAt = MatchEngine.NowUnixMs();
|
||||
return;
|
||||
}
|
||||
|
||||
var json = JsonSerializer.Serialize(Current, JsonOptions);
|
||||
await browser.WriteMatchJsonAsync(json);
|
||||
await browser.WriteMatchJsonAsync(storageScope.StorageKey, storageScope.WindowNameKey, json);
|
||||
_dirty = false;
|
||||
_lastPersistedAt = MatchEngine.NowUnixMs();
|
||||
}
|
||||
@@ -97,10 +101,27 @@ public sealed class MatchStore(BrowserBridge browser)
|
||||
|
||||
public async Task ClearAsync()
|
||||
{
|
||||
var storageScope = await SyncStorageScopeAsync();
|
||||
Current = null;
|
||||
IsLoaded = true;
|
||||
_dirty = false;
|
||||
_lastPersistedAt = MatchEngine.NowUnixMs();
|
||||
await browser.ClearMatchAsync();
|
||||
await browser.ClearMatchAsync(storageScope.StorageKey, storageScope.WindowNameKey);
|
||||
}
|
||||
|
||||
private async ValueTask<UserStorageScope> SyncStorageScopeAsync()
|
||||
{
|
||||
var storageScope = await userSession.GetStorageScopeAsync();
|
||||
if (_activeStorageKey is not null &&
|
||||
(_activeStorageKey != storageScope.StorageKey || _activeWindowNameKey != storageScope.WindowNameKey))
|
||||
{
|
||||
Current = null;
|
||||
IsLoaded = false;
|
||||
_dirty = false;
|
||||
}
|
||||
|
||||
_activeStorageKey = storageScope.StorageKey;
|
||||
_activeWindowNameKey = storageScope.WindowNameKey;
|
||||
return storageScope;
|
||||
}
|
||||
}
|
||||
|
||||
39
ChessCubing.App/Services/UserSession.cs
Normal file
39
ChessCubing.App/Services/UserSession.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
|
||||
namespace ChessCubing.App.Services;
|
||||
|
||||
public sealed class UserSession(AuthenticationStateProvider authenticationStateProvider)
|
||||
{
|
||||
public async ValueTask<UserStorageScope> GetStorageScopeAsync()
|
||||
{
|
||||
var authState = await authenticationStateProvider.GetAuthenticationStateAsync();
|
||||
var userId = ResolveUserId(authState.User);
|
||||
|
||||
return new UserStorageScope(
|
||||
$"{MatchStore.StorageKeyPrefix}:{userId}",
|
||||
$"{MatchStore.WindowNameKeyPrefix}:{userId}:");
|
||||
}
|
||||
|
||||
private static string ResolveUserId(ClaimsPrincipal user)
|
||||
{
|
||||
if (user.Identity?.IsAuthenticated != true)
|
||||
{
|
||||
return "anonymous";
|
||||
}
|
||||
|
||||
var rawIdentifier = user.FindFirst("sub")?.Value
|
||||
?? user.FindFirst(ClaimTypes.NameIdentifier)?.Value
|
||||
?? user.FindFirst("preferred_username")?.Value
|
||||
?? user.Identity?.Name;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(rawIdentifier))
|
||||
{
|
||||
return "authenticated";
|
||||
}
|
||||
|
||||
return Uri.EscapeDataString(rawIdentifier.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct UserStorageScope(string StorageKey, string WindowNameKey);
|
||||
Reference in New Issue
Block a user