Files
Electron.NET/docs/SignalR-Authentication-Guide.md
Pierre Arnaud 1c0b9378d2 Phase 6: Add comprehensive authentication documentation
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
2026-01-30 22:39:45 +01:00

584 lines
20 KiB
Markdown

# Electron.NET SignalR Authentication Guide
This guide explains the token-based authentication system implemented for SignalR mode in Electron.NET, designed to protect applications in multi-user environments.
## Table of Contents
1. [Overview](#overview)
2. [Threat Model](#threat-model)
3. [Authentication Architecture](#authentication-architecture)
4. [Implementation Details](#implementation-details)
5. [Security Properties](#security-properties)
6. [Troubleshooting](#troubleshooting)
7. [FAQ](#faq)
## Overview
Electron.NET's SignalR mode includes built-in authentication to ensure that only the Electron process spawned by a specific .NET instance can connect to that instance's HTTP and SignalR endpoints.
**When is this important?**
- Multi-user Windows Server environments (Terminal Services, RDP)
- Shared development machines with multiple users
- Any scenario where multiple users run the same application simultaneously
**What does it protect against?**
- User A's Electron process connecting to User B's .NET backend
- Unauthorized port scanning and connection attempts from other users
- Accidental misconfigurations causing cross-user connections
## Threat Model
### The Problem
When multiple users run the same Electron.NET application on a shared server:
1. Each .NET process binds to a TCP port (e.g., `http://localhost:58971`)
2. TCP ports are visible to **all users** on the machine
3. Without authentication, User A's Electron could connect to User B's backend
```
┌─────────────────────────────────────────────────┐
│ Windows Server (Terminal Services) │
├─────────────────────────────────────────────────┤
│ User A Session │
│ ├─ .NET Process (localhost:58971) │
│ └─ Electron Process │
│ │
│ User B Session │
│ ├─ .NET Process (localhost:61234) │
│ └─ Electron Process (could connect to 58971) │ ❌ Prevent this!
└─────────────────────────────────────────────────┘
```
### The Solution
Each .NET process generates a unique authentication token when launching Electron. Only requests with the correct token are allowed to connect.
```
┌─────────────────────────────────────────────────┐
│ User A Session │
│ ├─ .NET (token: abc123...) │
│ └─ Electron (has token abc123...) │ ✅ Authenticated
│ │
│ User B Session │
│ ├─ .NET (token: xyz789...) │
│ └─ Electron (has token xyz789...) │ ✅ Authenticated
│ │
│ ❌ User B's Electron → User A's .NET │
│ (lacks token abc123...) │ ❌ Rejected (401)
└─────────────────────────────────────────────────┘
```
## Authentication Architecture
### Flow Diagram
```
┌──────────────┐ ┌─────────────────┐
│ .NET Process │ │ Electron Process│
└──────┬───────┘ └────────┬────────┘
│ │
│ 1. Generate Token (GUID) │
│ Token: a3f8b2c1d4e5... │
│ │
│ 2. Launch Electron │
│ --authtoken=a3f8b2c1d4e5... │
│────────────────────────────────────────────────────>│
│ │
│ │ 3. Extract Token
│ │ global.authToken = ...
│ │
│ │ 4. Initial Page Load
│<────────────────────────────────────────────────────│
│ GET /?token=a3f8b2c1d4e5... │
│ │
│ 5. Validate Token │
│ ✓ Valid → Set Cookie │
│────────────────────────────────────────────────────>│
│ HTTP 200 + Set-Cookie: ElectronAuth=... │
│ │
│ │ 6. SignalR Connection
│<────────────────────────────────────────────────────│
│ GET /electron-hub?token=a3f8b2c1d4e5... │
│ │
│ 7. Validate Token, Set Cookie │
│────────────────────────────────────────────────────>│
│ HTTP 200 + Set-Cookie │
│ │
│ │ 8. Subsequent Requests
│<────────────────────────────────────────────────────│
│ GET /api/data │
│ Cookie: ElectronAuth=a3f8b2c1d4e5... │
│ │
│ 9. Validate Cookie │
│────────────────────────────────────────────────────>│
│ HTTP 200 (authenticated) │
│ │
```
### Key Steps
1. **Token Generation**: .NET generates 128-bit cryptographic random GUID
2. **Command-Line Passing**: Token passed to Electron via `--authtoken` parameter
3. **Token Extraction**: Electron stores token in `global.authToken`
4. **URL Appending**: Token appended to initial page and SignalR connection URLs
5. **Middleware Validation**: Every HTTP request validated by middleware
6. **Cookie Setting**: Valid token results in secure HttpOnly cookie
7. **Cookie-Based Requests**: Subsequent requests use cookie (no token in URL)
## Implementation Details
### 1. Token Generation (.NET)
**File**: `src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetDotnetFirstSignalR.cs`
```csharp
private void LaunchElectron()
{
// Generate secure authentication token (128-bit cryptographic random GUID)
this.authenticationToken = Guid.NewGuid().ToString("N"); // 32 hex chars
// Register token with authentication service
this.authenticationService.SetExpectedToken(this.authenticationToken);
// Launch Electron with token
var args = $"--unpackeddotnetsignalr --electronurl={this.actualUrl} --authtoken={this.authenticationToken}";
this.electronProcess = new ElectronProcessActive(isUnPacked, ElectronNetRuntime.ElectronExecutable, args, this.port.Value);
_ = this.electronProcess.Start();
}
```
**Token Format**: 32-character hexadecimal string (e.g., `a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c5`)
### 2. Authentication Service (.NET)
**File**: `src/ElectronNET.AspNet/Services/ElectronAuthenticationService.cs`
```csharp
public class ElectronAuthenticationService : IElectronAuthenticationService
{
private string _expectedToken;
private readonly object _lock = new object();
public void SetExpectedToken(string token)
{
lock (_lock)
{
_expectedToken = token;
}
}
public bool ValidateToken(string token)
{
if (string.IsNullOrEmpty(token))
return false;
lock (_lock)
{
if (string.IsNullOrEmpty(_expectedToken))
return false;
// Constant-time comparison prevents timing attacks
return ConstantTimeEquals(token, _expectedToken);
}
}
private static bool ConstantTimeEquals(string a, string b)
{
if (a == null || b == null || a.Length != b.Length)
return false;
var result = 0;
for (int i = 0; i < a.Length; i++)
{
result |= a[i] ^ b[i];
}
return result == 0;
}
}
```
**Key Features**:
- Thread-safe with lock
- Constant-time comparison (prevents timing attacks)
- Singleton lifetime (one per .NET instance)
### 3. Authentication Middleware (.NET)
**File**: `src/ElectronNET.AspNet/Middleware/ElectronAuthenticationMiddleware.cs`
```csharp
public async Task InvokeAsync(HttpContext context)
{
// Check if authentication cookie exists
var authCookie = context.Request.Cookies["ElectronAuth"];
if (!string.IsNullOrEmpty(authCookie))
{
if (_authService.ValidateToken(authCookie))
{
await _next(context);
return;
}
else
{
_logger.LogWarning("Invalid cookie for path {Path}", context.Request.Path);
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Unauthorized: Invalid authentication");
return;
}
}
// No cookie - check for token in query string
var token = context.Request.Query["token"].ToString();
if (!string.IsNullOrEmpty(token) && _authService.ValidateToken(token))
{
// Valid token - set cookie for future requests
context.Response.Cookies.Append("ElectronAuth", token, new CookieOptions
{
HttpOnly = true,
SameSite = SameSiteMode.Strict,
Path = "/",
Secure = false, // localhost is HTTP
IsEssential = true
});
await _next(context);
return;
}
// Reject - no valid cookie or token
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Unauthorized: Authentication required");
}
```
**Middleware Order** (IMPORTANT):
```csharp
app.UseMiddleware<ElectronAuthenticationMiddleware>(); // ← FIRST
app.UseRouting();
app.UseStaticFiles();
app.UseAntiforgery();
app.MapHub<ElectronHub>("/electron-hub");
app.MapRazorComponents<App>();
```
### 4. Token Extraction (Electron)
**File**: `src/ElectronNET.Host/main.js`
```javascript
// Extract authentication token from command-line
if (app.commandLine.hasSwitch('authtoken')) {
global.authToken = app.commandLine.getSwitchValue('authtoken');
}
```
### 5. Token in URLs (Electron)
**Initial Page Load** (`src/ElectronNET.Host/api/browserWindows.js`):
```javascript
// Append token to window URL
if (global.authToken) {
const separator = electronUrl.includes('?') ? '&' : '?';
electronUrl = `${electronUrl}${separator}token=${global.authToken}`;
}
window.loadURL(electronUrl);
```
**SignalR Connection** (`src/ElectronNET.Host/api/signalr-bridge.js`):
```javascript
async connect() {
// Append token to SignalR hub URL
const connectionUrl = this.authToken ?
`${this.hubUrl}?token=${this.authToken}` :
this.hubUrl;
this.connection = new signalR.HubConnectionBuilder()
.withUrl(connectionUrl)
.build();
await this.connection.start();
}
```
### 6. Service Registration (Application)
**File**: Your application's `Program.cs`
```csharp
var builder = WebApplication.CreateBuilder(args);
// Register authentication service as 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.Run();
```
## Security Properties
### Cookie Configuration
| Property | Value | Purpose |
|----------|-------|---------|
| `HttpOnly` | `true` | Prevents JavaScript access (XSS protection) |
| `SameSite` | `Strict` | Prevents CSRF attacks |
| `Path` | `/` | Cookie sent with all requests |
| `Secure` | `false` | Cannot use on localhost HTTP |
| `IsEssential` | `true` | Required for app to function |
| **Lifetime** | Session | Expires when Electron closes |
### Token Properties
- **Entropy**: 128 bits (2^128 possible values)
- **Format**: 32 hexadecimal characters (GUID without hyphens)
- **Generation**: `Guid.NewGuid()` uses cryptographically secure RNG
- **Lifetime**: Entire application session
- **Uniqueness**: Each .NET instance generates unique token
### What's Protected
**All HTTP endpoints**:
- Blazor Server pages (`/`, `/counter`, etc.)
- Static files (`/css/app.css`, `/js/script.js`)
- API endpoints (custom controllers)
- SignalR hub (`/electron-hub`)
**Both transport modes**:
- Initial token-based authentication (query parameter)
- Cookie-based subsequent requests
**Cross-user isolation**:
- Different users = different tokens
- Invalid token = 401 Unauthorized
### What's NOT Protected Against
**Same-user attacks** (by design):
- Process memory inspection
- Debugger attachment
- Command-line parameter visibility
**Rationale**: A malicious process running as the same user already has full access to:
- Process memory (can read token from RAM)
- Cookies (stored in Electron's data directory)
- All files owned by the user
Token-based authentication focuses on **cross-user isolation**, not same-user security.
## Troubleshooting
### Problem: 401 Unauthorized on Initial Load
**Symptoms**:
```
GET / HTTP/1.1
Response: 401 Unauthorized
```
**Possible Causes**:
1. Token not passed to Electron
2. Token not appended to URL
3. Middleware rejecting valid token
**Debugging Steps**:
1. Check Electron command-line:
```javascript
console.log('Auth Token:', global.authToken);
```
Should print 32-character hex string.
2. Check URL in browser window:
```javascript
console.log('Loading URL:', electronUrl);
```
Should include `?token=<guid>` parameter.
3. Check .NET logs:
```
[Warning] Authentication failed: Invalid token (prefix: a3f8b2c1...)
```
4. Verify middleware registration:
```csharp
app.UseMiddleware<ElectronAuthenticationMiddleware>(); // Before UseRouting()
```
### Problem: SignalR Connection Fails
**Symptoms**:
```
[SignalRBridge] Authentication failed: The authentication token is invalid or missing.
```
**Possible Causes**:
1. Token not passed to `SignalRBridge` constructor
2. Token not appended to hub URL
3. Cookie not being sent
**Debugging Steps**:
1. Check token passed to SignalRBridge:
```javascript
console.log('SignalRBridge token:', this.authToken);
```
2. Check connection URL:
```javascript
console.log('Hub URL:', connectionUrl);
```
Should be `http://localhost:PORT/electron-hub?token=<guid>`.
3. Enable verbose logging:
```javascript
.configureLogging(signalR.LogLevel.Debug)
```
### Problem: Cookie Not Persisting
**Symptoms**: Every request includes token in URL, cookie never set.
**Possible Causes**:
1. Cookie settings incompatible with browser
2. Middleware not setting cookie
3. Response headers not sent
**Debugging Steps**:
1. Check response headers:
```
Set-Cookie: ElectronAuth=a3f8b2c1...; Path=/; HttpOnly; SameSite=Strict
```
2. Check subsequent requests:
```
Cookie: ElectronAuth=a3f8b2c1...
```
3. Enable middleware logging:
```csharp
_logger.LogInformation("Setting cookie for path {Path}", path);
```
### Problem: Token Visible in Process List
**Observation**:
```powershell
wmic process where "name='electron.exe'" get commandline
...--authtoken=a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c5...
```
**Is this a problem?** No, by design.
**Explanation**:
- Command-line parameters are visible to same-user processes
- This is acceptable because:
- Same user already has access to process memory
- Same user can read cookies from Electron's data directory
- Token-based auth protects against **other users**, not same-user processes
**If you need same-user protection**: Use OS-level access controls (file permissions, process isolation) or consider named pipes with ACLs.
## FAQ
### Q: Why use tokens instead of Process ID validation?
**A**: PIDs can be recycled and reused, making validation unreliable. Additionally:
- PIDs don't provide cryptographic security
- Parent-child validation is platform-specific
- Adds complexity without meaningful security benefit
Token-based authentication provides:
- Cryptographic randomness (128-bit entropy)
- Simple cross-platform implementation
- No race conditions or PID recycling issues
### Q: Why not use HTTPS with certificates?
**A**: Localhost doesn't support HTTPS certificates easily:
- Self-signed certificates trigger browser warnings
- Certificate management adds complexity
- Token-based auth provides equivalent security for localhost IPC
### Q: Can I disable authentication?
**A**: Not recommended, but possible by removing middleware registration:
```csharp
// Remove this line:
// app.UseMiddleware<ElectronAuthenticationMiddleware>();
```
**Warning**: Only do this if:
- Application runs on single-user machines only
- No Terminal Services / RDP access
- You understand the security implications
### Q: Does this work with hot reload?
**A**: Yes, cookie persists across hot reload as long as Electron process keeps running.
### Q: What about multiple Electron windows?
**A**: All windows in the same Electron process share cookies automatically. Authentication works seamlessly across multiple windows.
### Q: How do I test authentication?
**Test 1 - Happy Path**:
1. Run application normally
2. Check logs for "Authentication successful"
3. Verify cookie is set (DevTools → Application → Cookies)
4. Subsequent requests should not include token in URL
**Test 2 - Invalid Token**:
1. Modify token in browser URL: `?token=invalid`
2. Should receive 401 Unauthorized
3. Check logs for "Authentication failed: Invalid token"
**Test 3 - No Token**:
1. Open browser manually to `http://localhost:PORT/`
2. Should receive 401 Unauthorized
3. Check logs for "Authentication failed: No cookie or token"
**Test 4 - Multi-User** (Windows Server/Terminal Services):
1. Launch app as User A
2. In User B session, try to connect to User A's port
3. Should receive 401 Unauthorized
### Q: What about packaged applications?
**A**: Authentication works identically in packaged mode. The `--authtoken` parameter is included in the packaged Electron executable.
### Q: Can I customize the cookie name?
**A**: Yes, modify `AuthCookieName` constant in `ElectronAuthenticationMiddleware.cs`:
```csharp
private const string AuthCookieName = "MyCustomCookieName";
```
## Summary
Electron.NET's token-based authentication provides:
**Security**: 128-bit entropy, constant-time comparison, secure cookies
**Simplicity**: Automatic token generation and validation
**Compatibility**: Works with Blazor, SignalR, and static files
**Monitoring**: Structured logging for security events
**Multi-User**: Cross-user isolation on shared servers
The authentication system is **enabled by default** in SignalR mode and requires minimal configuration. For most applications, simply register the services and middleware - everything else happens automatically.
For additional help or questions, see the [SignalR Implementation Summary](SignalR-Implementation-Summary.md) or open an issue on GitHub.