diff --git a/src/ElectronNET.AspNet/Middleware/ElectronAuthenticationMiddleware.cs b/src/ElectronNET.AspNet/Middleware/ElectronAuthenticationMiddleware.cs index 9bb94e0..e93b837 100644 --- a/src/ElectronNET.AspNet/Middleware/ElectronAuthenticationMiddleware.cs +++ b/src/ElectronNET.AspNet/Middleware/ElectronAuthenticationMiddleware.cs @@ -3,27 +3,41 @@ namespace ElectronNET.AspNet.Middleware using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; + using Microsoft.Extensions.Logging; using ElectronNET.AspNet.Services; /// /// Middleware that validates authentication for all Electron requests. /// Checks for authentication cookie or token query parameter on first request. /// Sets HttpOnly cookie for subsequent requests. + /// + /// Security Model: + /// - First request includes token as query parameter (?token=guid) + /// - Middleware validates token and sets secure HttpOnly cookie + /// - Subsequent requests use cookie (no token in URL) + /// - Both HTTP endpoints and SignalR hub protected /// public class ElectronAuthenticationMiddleware { private readonly RequestDelegate _next; private readonly IElectronAuthenticationService _authService; + private readonly ILogger _logger; private const string AuthCookieName = "ElectronAuth"; - public ElectronAuthenticationMiddleware(RequestDelegate next, IElectronAuthenticationService authService) + public ElectronAuthenticationMiddleware( + RequestDelegate next, + IElectronAuthenticationService authService, + ILogger logger) { _next = next; _authService = authService; + _logger = logger; } public async Task InvokeAsync(HttpContext context) { + var path = context.Request.Path.Value; + // Check if authentication cookie exists var authCookie = context.Request.Cookies[AuthCookieName]; @@ -38,8 +52,10 @@ namespace ElectronNET.AspNet.Middleware else { // Invalid cookie - reject + _logger.LogWarning("Authentication failed: Invalid cookie for path {Path} from {RemoteIp}", + path, context.Connection.RemoteIpAddress); context.Response.StatusCode = 401; - await context.Response.WriteAsync("Unauthorized: Invalid authentication cookie"); + await context.Response.WriteAsync("Unauthorized: Invalid authentication"); return; } } @@ -52,6 +68,8 @@ namespace ElectronNET.AspNet.Middleware if (_authService.ValidateToken(token)) { // Valid token - set cookie for future requests + _logger.LogInformation("Authentication successful: Setting cookie for path {Path}", path); + context.Response.Cookies.Append(AuthCookieName, token, new CookieOptions { HttpOnly = true, // Prevent JavaScript access (XSS protection) @@ -64,9 +82,20 @@ namespace ElectronNET.AspNet.Middleware await _next(context); return; } + else + { + // Invalid token - reject + _logger.LogWarning("Authentication failed: Invalid token (prefix: {TokenPrefix}...) for path {Path} from {RemoteIp}", + token.Length > 8 ? token.Substring(0, 8) : token, path, context.Connection.RemoteIpAddress); + context.Response.StatusCode = 401; + await context.Response.WriteAsync("Unauthorized: Invalid authentication"); + return; + } } // Neither cookie nor valid token present - reject + _logger.LogWarning("Authentication failed: No cookie or token provided for path {Path} from {RemoteIp}", + path, context.Connection.RemoteIpAddress); context.Response.StatusCode = 401; await context.Response.WriteAsync("Unauthorized: Authentication required"); } diff --git a/src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetDotnetFirstSignalR.cs b/src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetDotnetFirstSignalR.cs index 5c0f067..343c0ac 100644 --- a/src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetDotnetFirstSignalR.cs +++ b/src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetDotnetFirstSignalR.cs @@ -113,10 +113,12 @@ namespace ElectronNET.AspNet.Runtime private void LaunchElectron() { - // Generate secure authentication token + // Generate secure authentication token (128-bit cryptographic random GUID) + // This token protects against unauthorized connections from other users on the same machine this.authenticationToken = Guid.NewGuid().ToString("N"); // 32 hex chars, no hyphens // Register token with authentication service for validation + // The middleware will validate this token on all HTTP and SignalR requests this.authenticationService.SetExpectedToken(this.authenticationToken); var isUnPacked = ElectronNetRuntime.StartupMethod.IsUnpackaged(); diff --git a/src/ElectronNET.Host/api/signalr-bridge.js b/src/ElectronNET.Host/api/signalr-bridge.js index 828cbfb..6987e08 100644 --- a/src/ElectronNET.Host/api/signalr-bridge.js +++ b/src/ElectronNET.Host/api/signalr-bridge.js @@ -78,7 +78,13 @@ class SignalRBridge { return true; } catch (err) { - console.error(`[SignalRBridge] Connection failed:`, err); + // Check if this is an authentication error + if (err.message && err.message.includes('401')) { + console.error(`[SignalRBridge] Authentication failed: The authentication token is invalid or missing.`); + console.error(`[SignalRBridge] Please ensure the --authtoken parameter is correctly passed to Electron.`); + } else { + console.error(`[SignalRBridge] Connection failed:`, err); + } this.isConnected = false; return false; }