Created new authentication guide: - docs/SignalR-Authentication-Guide.md (500+ lines) - Complete threat model and security architecture - Flow diagrams and implementation details - Troubleshooting guide with common issues - FAQ covering design decisions and usage Updated SignalR implementation summary: - Added authentication & security section - Documented token flow and cookie management - Security properties and protection scope - Logging & monitoring guidelines - Multi-user testing procedures - Updated file changes summary and success metrics Documentation includes: - Architecture diagrams (ASCII art) - Code examples for all components - Step-by-step authentication flow - Security considerations and rationale - Common troubleshooting scenarios - Testing recommendations
19 KiB
SignalR Implementation Summary
This document summarizes the completed implementation of SignalR-based bidirectional communication in Electron.NET as an alternative to Socket.IO.
Overview
The SignalR implementation provides a modern, .NET-native alternative to Socket.IO for communication between the ASP.NET Core host and the Electron process. This new startup mode was designed specifically for Blazor Server applications where ASP.NET Core and Electron need tighter integration and lifecycle control.
Key Innovation: .NET-first startup with dynamic port assignment - ASP.NET Core starts first, binds to port 0 (letting Kestrel choose an available port), then launches Electron with the actual URL.
Primary Use Case
Blazor Server applications where:
- ASP.NET Core owns the application lifecycle
- Dynamic port binding is needed (no fixed port configuration)
- Modern SignalR infrastructure is preferred over Socket.IO
- Single process debugging is desired (.NET process controls Electron)
Implementation Phases (All Complete)
Phase 1: Core Infrastructure ✅
- Added new
StartupMethodenum values:UnpackagedDotnetFirstSignalRPackagedDotnetFirstSignalR
- Created
ElectronHubSignalR hub for bidirectional communication - Registered hub endpoint at
/electron-hub(separate from Blazor's/_blazorhub)
Phase 2: Runtime Controller ✅
- Created
RuntimeControllerAspNetDotnetFirstSignalR - Implemented logic to:
- Bind Kestrel to port 0
- Wait for Kestrel startup and capture actual port via
IServerAddressesFeature - Launch Electron with
--electronurlparameter - Wait for SignalR connection from Electron
- Transition to Ready state when connected
Phase 3: Electron/Node.js Side ✅
- Added
@microsoft/signalrnpm package dependency - Created SignalR connection module (
signalr-bridge.js) - Updated
main.jsto detect SignalR modes and connect to/electron-hub - Implemented Socket.IO-compatible interface for API compatibility
Phase 4: API Bridge Adaptation ✅
- Created
SignalRFacadeimplementingIFacadeinterface - Ensured existing Electron API classes work with SignalR
- Implemented type conversion helper for SignalR's JSON deserialization
- Event routing from both directions (.NET ↔ Electron)
Phase 5: Configuration & Extensions ✅
- Updated
WebHostBuilderExtensionsfor automatic SignalR configuration - Added startup mode detection via command-line flags
- Configured dynamic port binding (port 0) for SignalR modes
- Integrated with
UseElectron()API for seamless usage
Phase 6: Testing & Fixes ✅
- Created sample Blazor Server application
- Fixed multiple critical issues discovered during integration testing
- Cleaned up debug logging
- Added comprehensive code documentation
Key Components
1. SignalRFacade (src/ElectronNET.AspNet/Bridge/SignalRFacade.cs)
- Implements
IFacadeinterface to match Socket.IO facade API - Handles bidirectional event routing using
IHubContext<ElectronHub> - Includes
ConvertToType<T>helper for handling SignalR's JSON deserialization quirks - Critical fix: Handles
JsonElementand numeric type conversions (long → int)
2. ElectronHub (src/ElectronNET.AspNet/Hubs/ElectronHub.cs)
- SignalR hub for .NET ↔ Electron communication
- Key methods:
RegisterElectronClient()- Called by Electron on connectionElectronEvent(string, object[])- Receives events from Electron- Connection/disconnection handlers notify runtime controller
3. RuntimeControllerAspNetDotnetFirstSignalR
- Manages SignalR mode lifecycle
- Critical flow:
- Wait for ASP.NET server to start
- Capture dynamic port from
IServerAddressesFeature - Update
ElectronNetRuntime.AspNetWebPortwith actual port - Launch Electron with
--electronurlparameter - Wait for
electron-host-readysignal before calling app ready callback
4. SignalRBridge (src/ElectronNET.Host/api/signalr-bridge.js)
- JavaScript SignalR client that mimics Socket.IO interface
- Provides
on()andemit()methods for API compatibility - Critical fix: Event args passed as arrays, spread when calling handlers
- Uses
@microsoft/signalrnpm package
5. Main.js Startup (src/ElectronNET.Host/main.js)
- Detects SignalR mode via
--unpackeddotnetsignalror--dotnetpackedsignalrflags - Creates invisible keep-alive window (destroyed when first real window is created)
- Loads API modules then signals
electron-host-readyto .NET
Usage
Enable SignalR mode by passing the appropriate command-line flag:
# Unpacked mode (development)
dotnet run --unpackeddotnetsignalr
# Packed mode (production)
dotnet run --dotnetpackedsignalr
Or set environment variable (deprecated, flags preferred):
ELECTRON_USE_SIGNALR=true
In your ASP.NET Core Program.cs:
var builder = WebApplication.CreateBuilder(args);
// Add Electron.NET services
builder.Services.AddElectron();
// Configure Electron with SignalR mode
builder.WebHost.UseElectron(args, async () =>
{
var window = await Electron.WindowManager.CreateWindowAsync();
window.OnReadyToShow += () => window.Show();
});
var app = builder.Build();
app.UseRouting();
// Map SignalR hub for Electron communication
app.MapHub<ElectronHub>("/electron-hub");
app.MapRazorComponents<App>().AddInteractiveServerRenderMode();
app.Run();
Note: UseElectron() automatically detects SignalR mode and configures everything. The rest happens automatically:
- Port 0 binding (dynamic port assignment)
- Electron launch with actual URL
- SignalR connection establishment (WebSockets enabled automatically by MapHub)
- App ready callback execution
- Electron launch with actual URL
- SignalR connection establishment (WebSockets enabled automatically by
MapHub) - App ready callback execution
Architecture Decisions
Why .NET-First Startup?
SignalR mode uses .NET-first startup (vs. Electron-first in Socket.IO mode) because:
- No port scanning needed - .NET can pass the actual URL to Electron
- SignalR hub must be registered before Electron connects
- Simpler lifecycle - ASP.NET controls when Electron launches
- Better for Blazor Server - Blazor is already running when Electron starts
- Single process debugging - Developer debugs .NET process which owns Electron
Why IFacade Interface?
Introducing IFacade allows BridgeConnector.Socket to return either SocketIOFacade or SignalRFacade based on startup mode, ensuring existing API code works with both transport mechanisms without modification.
Why Keep-Alive Window?
Electron quits immediately on macOS if no windows exist. The keep-alive window ensures Electron stays running during the connection and API initialization phase. It's automatically destroyed when the first real window is created.
Why 'electron-host-ready' Signal?
Without this signal, .NET would call the app ready callback before Electron finished loading API modules, causing API calls to fail. The signal ensures proper initialization order:
- Electron connects to SignalR
- Electron loads all API modules (browserWindows, dialog, menu, etc.)
- Electron signals
electron-host-ready - .NET calls app ready callback
- App code can safely use Electron APIs
Blazor Server Considerations
SignalR Hub Coexistence
Blazor Server already uses SignalR for component communication (/_blazor hub). Our implementation:
- Uses separate endpoint (
/electron-hub) to avoid conflicts - Both hubs coexist on the same Kestrel server without interference
- No impact on Blazor's reconnection logic
- Compatible with hot reload scenarios
Lifecycle Integration
- Electron window creation happens after Blazor app is ready
UseElectron()callback fires when SignalR hub is connected- Blazor components can inject Electron services to control windows
- Proper disposal when Electron process exits
Development Experience
- Hot reload works for both Blazor and Electron integration
- F5 debugging works seamlessly
- No need to manually coordinate ports
- Single process to debug (.NET process owns the lifecycle)
Critical Fixes Applied
1. Race Condition: API Module Loading
Problem: .NET called app ready callback before Electron finished loading API modules.
Solution: Electron signals electron-host-ready after loading all API modules. .NET waits for this signal before calling the app ready callback.
2. Event Argument Mismatch
Problem: SignalR sent event data as nested arrays [[data]] instead of [data].
Solution:
- C#: Use explicit
object[] argsparameter (notparams) - JS: Always pass args as array:
invoke('ElectronEvent', eventName, args) - JS: Spread args when calling handlers:
handler(...argsArray)
3. Type Conversion Failures
Problem: SignalR deserializes JSON numbers as JsonElement or long, causing Once<int> handlers to fail silently.
Solution: SignalRFacade.ConvertToType<T> handles JsonElement deserialization and numeric conversions.
4. Window Shutdown Not Triggering Exit
Problem: Keep-alive window prevented window-all-closed event from firing.
Solution: Destroy keep-alive window when first real window is created using app.once('browser-window-created').
5. Dynamic Port Not Propagated
Problem: When using port 0, Kestrel assigns a dynamic port, but ElectronNetRuntime.AspNetWebPort was not updated.
Solution: Update AspNetWebPort after capturing port from IServerAddressesFeature in CapturePortAndLaunchElectron().
7. Blazor Static Files Not Loading
Problem: Blazor CSS and framework files returned 404 errors.
Solution:
- Added
app.UseStaticFiles()to serve wwwroot content - Fixed middleware order:
UseAntiforgery()must be betweenUseRouting()andUseEndpoints() - Updated scoped CSS asset reference to use lowercase name matching .NET 9+ convention
Authentication & Security
Token-Based Authentication (Multi-User Protection)
SignalR mode includes built-in authentication to prevent unauthorized connections in multi-user scenarios (e.g., Windows Server with Terminal Services/RDP).
Threat Model: On shared servers, multiple users can run the same application simultaneously. Without authentication, User A's Electron process could potentially connect to User B's ASP.NET backend.
Solution: Token-based authentication with secure cookies.
Authentication Flow
- .NET generates token: When launching Electron,
RuntimeControllerAspNetDotnetFirstSignalRgenerates a cryptographically secure GUID (128-bit entropy) - Token passed via command-line: Electron receives
--authtoken=<guid>parameter - Token appended to URLs:
- Initial page load:
http://localhost:PORT/?token=<guid> - SignalR connection:
http://localhost:PORT/electron-hub?token=<guid>
- Initial page load:
- Middleware validates token:
ElectronAuthenticationMiddlewarechecks every HTTP request - Cookie set on first request: After successful token validation, secure HttpOnly cookie is set
- Subsequent requests use cookie: No token in URLs after initial authentication
Security Properties
-
Cookie Settings:
HttpOnly: true (prevents JavaScript access, XSS protection)SameSite: Strict (prevents CSRF)Path: / (applies to all routes)Secure: false (localhost is HTTP, not HTTPS)IsEssential: true (required for app to function)- Lifetime: Session scope (expires when Electron closes)
-
Token Validation:
- Constant-time string comparison (prevents timing attacks)
- Token stored in singleton service (one per .NET instance)
- Never logged in full (only first 8 characters for debugging)
-
Protection Scope:
- All HTTP endpoints (Blazor pages, static files, API calls)
- SignalR hub connection (negotiate and all hub traffic)
- Both initial request and cookie-based requests validated
What This Protects Against
✅ Protected:
- Cross-user connections (User A → User B's backend)
- Port scanning attacks from other users
- Accidental connections from misconfigured processes
❌ NOT Protected Against (By Design):
- Malicious same-user processes with debugger access
- Process memory inspection tools (same privilege level)
- Command-line parameter visibility (same user can see all processes)
Rationale: Same-user attacks already have full access to process memory, files, and cookies. Token-based authentication focuses on cross-user isolation, which is the primary threat in multi-user environments.
Implementation Components
-
IElectronAuthenticationService (
src/ElectronNET.AspNet/Services/)- Singleton service storing expected token
- Thread-safe with lock-based validation
- Constant-time comparison to prevent timing attacks
-
ElectronAuthenticationMiddleware (
src/ElectronNET.AspNet/Middleware/)- Validates every HTTP request before routing
- Checks cookie first, then token query parameter
- Sets cookie on first valid token
- Returns 401 for invalid/missing authentication
- Structured logging for security monitoring
-
Token Generation (
RuntimeControllerAspNetDotnetFirstSignalR.cs)Guid.NewGuid().ToString("N")= 32 hex characters- Called in
LaunchElectron()method - Registered with authentication service immediately
-
Electron Integration (
main.js,signalr-bridge.js)- Extracts token from
--authtokenparameter - Stores in
global.authTokenfor module access - Appends to browser window URL and SignalR connection URL
- Extracts token from
Usage in Custom Applications
Authentication is enabled by default in SignalR mode. No additional configuration required beyond service registration:
var builder = WebApplication.CreateBuilder(args);
// Register authentication service (singleton)
builder.Services.AddSingleton<IElectronAuthenticationService, ElectronAuthenticationService>();
builder.Services.AddElectron();
var app = builder.Build();
// Register middleware BEFORE UseRouting()
app.UseMiddleware<ElectronAuthenticationMiddleware>();
app.UseRouting();
app.MapHub<ElectronHub>("/electron-hub");
app.MapRazorComponents<App>().AddInteractiveServerRenderMode();
app.Run();
The rest is automatic:
- Token generation happens when Electron launches
- Token validation happens on every request
- Cookie management is handled by the middleware
Logging & Monitoring
Authentication events are logged with structured logging:
Successful authentication:
[Information] Authentication successful: Setting cookie for path /
Failed authentication:
[Warning] Authentication failed: Invalid token (prefix: a3f8b2c1...) for path / from 127.0.0.1
[Warning] Authentication failed: No cookie or token provided for path /api/data from 127.0.0.1
[Warning] Authentication failed: Invalid cookie for path /_blazor from 127.0.0.1
SignalR connection failures:
[SignalRBridge] Authentication failed: The authentication token is invalid or missing.
[SignalRBridge] Please ensure the --authtoken parameter is correctly passed to Electron.
Log failed authentication attempts for security monitoring and troubleshooting.
Testing Multi-User Scenarios
To test authentication in multi-user environments:
-
Run as different Windows users:
# User A session dotnet run # User B session (different RDP/Terminal Services session) dotnet run -
Verify isolation: User A's Electron cannot access User B's backend
-
Check logs: Failed auth attempts should be logged
-
Monitor tokens: Each instance generates unique token
For development testing on single-user machines, simulate by running multiple instances and attempting to connect with wrong/missing tokens.
Backward Compatibility
This is a new optional startup mode - all existing modes continue to work unchanged:
PackagedElectronFirst- unchangedPackagedDotnetFirst- unchangedUnpackedElectronFirst- unchangedUnpackedDotnetFirst- unchanged
Existing applications do not need to change. SignalR mode is opt-in via command-line flags.
File Changes Summary
New Files:
src/ElectronNET.AspNet/Bridge/SignalRFacade.cs(225 lines)src/ElectronNET.AspNet/Hubs/ElectronHub.cs(108 lines)src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetDotnetFirstSignalR.cs(163 lines)src/ElectronNET.AspNet/Services/IElectronAuthenticationService.cs(20 lines)src/ElectronNET.AspNet/Services/ElectronAuthenticationService.cs(65 lines)src/ElectronNET.AspNet/Middleware/ElectronAuthenticationMiddleware.cs(105 lines)src/ElectronNET.Host/api/signalr-bridge.js(125 lines)
Modified Files:
src/ElectronNET.AspNet/API/WebHostBuilderExtensions.cs- Added SignalR service registrationsrc/ElectronNET.Host/main.js- Added SignalR startup flow and token extractionsrc/ElectronNET.Host/api/browserWindows.js- Token appended to window URLssrc/ElectronNET.Host/package.json- Added@microsoft/signalrdependencysrc/ElectronNET.Samples.BlazorSignalR/Program.cs- Sample with authentication
Total Changes: ~1,220 lines added
Testing Recommendations
- Test with dynamic port (port 0) to ensure URL propagation works
- Verify window-all-closed triggers app exit
- Test rapid window creation/destruction
- Verify reconnection behavior if SignalR connection drops
- Test with both packed and unpacked modes
- Verify API calls work correctly (especially those returning data)
Known Limitations
- Request-response pattern not yet implemented -
InvokeElectronApiis a placeholder. Current API calls use event-based pattern. - TouchBar API not yet supported on macOS SignalR mode
- SignalR automatic reconnection may cause issues with pending API calls (needs circuit breaker pattern)
Future Enhancements
- Request-response pattern - Implement proper async/await pattern for API calls that return values
- Metrics/diagnostics - Add SignalR connection health monitoring
- Circuit breaker - Handle reconnection scenarios gracefully
- Integration tests - Comprehensive test suite for SignalR mode
- Performance benchmarks - Compare SignalR vs Socket.IO performance
Success Metrics
The implementation is considered complete and functional:
- ✅ .NET starts first with dynamic port (port 0)
- ✅ Electron launches with actual URL
- ✅ SignalR connection establishes successfully
- ✅ API modules load before app ready callback
- ✅ Window creation works from .NET
- ✅ Window shutdown triggers app exit
- ✅ Blazor Server pages load with correct styling
- ✅ Both SignalR hubs coexist (Electron + Blazor)
- ✅ Clean codebase with minimal debug logging
- ✅ Comprehensive inline documentation
- ✅ Token-based authentication for multi-user scenarios
- ✅ Secure cookie-based session management
- ✅ Structured logging for security monitoring
- ✅ Protection against cross-user connection attempts