From 08abd96751bb26ea7fd976e8937e84aa607a31b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Zieli=C5=84ski?= Date: Sat, 6 Dec 2025 00:36:22 +0100 Subject: [PATCH] SignalR FIX --- .claude/project-context.md | 26 ++++++++++++++++- .../Components/Layout/MainLayout.razor | 28 +++++++++++++++++-- DiunaBI.UI.Shared/Services/AuthService.cs | 16 ++++++++--- 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/.claude/project-context.md b/.claude/project-context.md index 42e71e6..7c51156 100644 --- a/.claude/project-context.md +++ b/.claude/project-context.md @@ -1,10 +1,34 @@ # DiunaBI Project Context > This file is auto-generated for Claude Code to quickly understand the project structure. -> Last updated: 2025-12-05 +> Last updated: 2025-12-06 ## RECENT CHANGES (This Session) +**SignalR Authentication Token Flow Fix (Dec 6, 2025):** +- ✅ **TokenProvider Population** - Fixed `TokenProvider.Token` never being set with JWT, causing 401 Unauthorized on SignalR connections +- ✅ **AuthService Token Management** - Injected `TokenProvider` into `AuthService` and set token in 3 key places: + - `ValidateWithBackendAsync()` - on fresh Google login + - `CheckAuthenticationAsync()` - on session restore from localStorage + - `ClearAuthenticationAsync()` - clear token on logout +- ✅ **SignalR Initialization Timing** - Moved SignalR initialization from `MainLayout.OnInitializedAsync` to after authentication completes +- ✅ **Event-Driven Architecture** - `MainLayout` now subscribes to `AuthenticationStateChanged` event to initialize SignalR when user authenticates +- ✅ **Session Restore Support** - `CheckAuthenticationAsync()` now fires `AuthenticationStateChanged` event to initialize SignalR on page refresh +- Root cause: SignalR was initialized before authentication, so JWT token was empty during connection setup +- Solution: Initialize SignalR only after token is available via event subscription +- Files modified: [AuthService.cs](DiunaBI.UI.Shared/Services/AuthService.cs), [MainLayout.razor](DiunaBI.UI.Shared/Components/Layout/MainLayout.razor) +- Status: SignalR authentication working for both fresh login and restored sessions + +**SignalR Authentication DI Fix (Dec 6, 2025):** +- ✅ **TokenProvider Registration** - Added missing `TokenProvider` service registration in DI container +- ✅ **EntityChangeHubService Scope Fix** - Changed from singleton to scoped to support user-specific JWT tokens +- ✅ **Bug Fix** - Resolved `InvalidOperationException` preventing app from starting after SignalR authentication was added +- Root cause: Singleton service (`EntityChangeHubService`) cannot depend on scoped service (`TokenProvider`) in DI +- Solution: Made `EntityChangeHubService` scoped so each user session has its own authenticated SignalR connection +- Files modified: [ServiceCollectionExtensions.cs](DiunaBI.UI.Shared/Extensions/ServiceCollectionExtensions.cs) + +--- + **Security Audit & Hardening (Dec 5, 2025):** - ✅ **JWT Token Validation** - Enabled issuer/audience validation in [Program.cs](DiunaBI.API/Program.cs), fixed config key mismatch in [JwtTokenService.cs](DiunaBI.API/Services/JwtTokenService.cs) - ✅ **API Key Security** - Created [ApiKeyAuthAttribute.cs](DiunaBI.API/Attributes/ApiKeyAuthAttribute.cs) with X-API-Key header auth, constant-time comparison diff --git a/DiunaBI.UI.Shared/Components/Layout/MainLayout.razor b/DiunaBI.UI.Shared/Components/Layout/MainLayout.razor index 3f1bab8..64df459 100644 --- a/DiunaBI.UI.Shared/Components/Layout/MainLayout.razor +++ b/DiunaBI.UI.Shared/Components/Layout/MainLayout.razor @@ -2,7 +2,9 @@ @using DiunaBI.UI.Shared.Services @inject AppConfig AppConfig @inject EntityChangeHubService HubService +@inject AuthService AuthService @inherits LayoutComponentBase +@implements IDisposable @@ -55,10 +57,30 @@ private bool _drawerOpen = true; private DrawerVariant _drawerVariant = DrawerVariant.Persistent; - protected override async Task OnInitializedAsync() + protected override void OnInitialized() { - // Initialize SignalR connection when layout loads - await HubService.InitializeAsync(); + // Subscribe to authentication state changes + AuthService.AuthenticationStateChanged += OnAuthenticationStateChanged; + + // If already authenticated (e.g., from restored session), initialize SignalR + if (AuthService.IsAuthenticated) + { + _ = HubService.InitializeAsync(); + } + } + + private async void OnAuthenticationStateChanged(bool isAuthenticated) + { + if (isAuthenticated) + { + Console.WriteLine("🔐 MainLayout: User authenticated, initializing SignalR..."); + await HubService.InitializeAsync(); + } + } + + public void Dispose() + { + AuthService.AuthenticationStateChanged -= OnAuthenticationStateChanged; } private MudTheme _theme = new MudTheme() diff --git a/DiunaBI.UI.Shared/Services/AuthService.cs b/DiunaBI.UI.Shared/Services/AuthService.cs index 1298326..212d46e 100644 --- a/DiunaBI.UI.Shared/Services/AuthService.cs +++ b/DiunaBI.UI.Shared/Services/AuthService.cs @@ -15,16 +15,18 @@ public class AuthService { private readonly HttpClient _httpClient; private readonly IJSRuntime _jsRuntime; + private readonly TokenProvider _tokenProvider; private bool? _isAuthenticated; private UserInfo? _userInfo = null; private string? _apiToken; public event Action? AuthenticationStateChanged; - public AuthService(HttpClient httpClient, IJSRuntime jsRuntime) + public AuthService(HttpClient httpClient, IJSRuntime jsRuntime, TokenProvider tokenProvider) { _httpClient = httpClient; _jsRuntime = jsRuntime; + _tokenProvider = tokenProvider; } public bool IsAuthenticated => _isAuthenticated ?? false; @@ -44,6 +46,7 @@ public class AuthService if (result != null) { _apiToken = result.Token; + _tokenProvider.Token = result.Token; // Set token for SignalR _userInfo = new UserInfo { Id = result.Id, @@ -104,6 +107,7 @@ public class AuthService if (_isAuthenticated.Value && !string.IsNullOrEmpty(userInfoJson)) { _apiToken = token; + _tokenProvider.Token = token; // Set token for SignalR _userInfo = JsonSerializer.Deserialize(userInfoJson); // Restore header @@ -111,14 +115,17 @@ public class AuthService new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _apiToken); Console.WriteLine($"✅ Session restored: {_userInfo?.Email}"); + + // Notify that authentication state changed (for SignalR initialization) + AuthenticationStateChanged?.Invoke(true); } else { Console.WriteLine("❌ No valid session"); } - + Console.WriteLine($"=== AuthService.CheckAuthenticationAsync END (authenticated={_isAuthenticated}) ==="); - + return _isAuthenticated.Value; } catch (Exception ex) @@ -139,11 +146,12 @@ public class AuthService await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", "user_info"); _apiToken = null; + _tokenProvider.Token = null; // Clear token for SignalR _isAuthenticated = false; _userInfo = null; _httpClient.DefaultRequestHeaders.Authorization = null; - + Console.WriteLine("✅ Authentication cleared"); AuthenticationStateChanged?.Invoke(false); }