Files
AutopilotMonitor/src/Backend/AutopilotMonitor.Functions/Middleware/AuthenticationMiddleware.cs
T
Oliver Kieselbach a20ff48ae6 Backend:
- Add JWT-based authentication to all API endpoints
- Implement tenant isolation and access control validation
- Add Galactic Admin role validation for cross-tenant endpoints
- Enhance certificate validation with full X.509 chain checking
- Add certificate validation cache (5-min TTL) and self-signed cert protection
- Implement NDJSON payload size limit protection (configurable per tenant)
- Replace placeholder audit user identifiers with actual JWT user principals
- Add TenantHelper utility for consistent tenant/user extraction from tokens
- Disable PII logging in production (ShowPII = false)

Agent:
- Add automatic tenant ID detection from Windows Registry (AAD Join enrollment)
- Implement tenant ID precedence: CLI argument > Registry > Fallback

SignalR:
- Add authentication checks to all SignalR functions
- Implement tenant-based group access control
- Add user identifier logging for SignalR operations

Frontend:
- Change MSAL cache from localStorage to sessionStorage (XSS protection)
- Add warning status support to health check page
- Add Admin Configuration link in navbar (Galactic Admin only)
- Move maintenance trigger from Settings to Admin Configuration page
- Update tenant context logging for clarity

Configuration:
- Add MaxNdjsonPayloadSizeMB setting to TenantConfiguration model
- Update configuration service to persist new retention/timeout settings
2026-02-05 16:38:16 +01:00

189 lines
9.8 KiB
C#

using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Azure.Functions.Worker.Middleware;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using Microsoft.IdentityModel.JsonWebTokens;
using System.Net;
using System.Security.Claims;
namespace AutopilotMonitor.Functions.Middleware;
/// <summary>
/// Middleware to manually validate JWT tokens and populate ClaimsPrincipal
/// Required for Azure Functions .NET 8 Isolated Worker (Microsoft.Identity.Web doesn't work automatically)
/// </summary>
public class AuthenticationMiddleware : IFunctionsWorkerMiddleware
{
private readonly ILogger<AuthenticationMiddleware> _logger;
private readonly IConfiguration _configuration;
private readonly JsonWebTokenHandler _tokenHandler;
private readonly IConfigurationManager<OpenIdConnectConfiguration> _configurationManager;
public AuthenticationMiddleware(
ILogger<AuthenticationMiddleware> logger,
IConfiguration configuration)
{
_logger = logger;
_configuration = configuration;
_tokenHandler = new JsonWebTokenHandler();
// Disable PII logging in production for security
Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = false;
// Set up OpenID Connect configuration manager to get signing keys
var authority = "https://login.microsoftonline.com/organizations/v2.0";
var metadataAddress = $"{authority}/.well-known/openid-configuration";
_configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
metadataAddress,
new OpenIdConnectConfigurationRetriever());
}
public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
{
var httpContext = context.GetHttpContext();
if (httpContext != null)
{
_logger.LogInformation($"[Auth Middleware] Processing request to {httpContext.Request.Path}");
// Extract Authorization header
var authHeader = httpContext.Request.Headers.Authorization.ToString();
if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
var token = authHeader.Substring("Bearer ".Length).Trim();
_logger.LogInformation("[Auth Middleware] Found Bearer token, validating...");
try
{
// First, decode the token to get the tenant ID (without validation)
// Use the new JsonWebTokenHandler to read the token
var jwtToken = _tokenHandler.ReadJsonWebToken(token);
var tenantId = jwtToken.GetClaim("tid")?.Value;
var audience = jwtToken.GetClaim("aud")?.Value;
var issuer = jwtToken.Issuer;
var keyId = jwtToken.Kid;
var algorithm = jwtToken.Alg;
_logger.LogInformation($"[Auth Middleware] Token details - Tenant: {tenantId}, Audience: {audience}, Issuer: {issuer}, KeyId: {keyId}, Algorithm: {algorithm}");
// Determine which endpoint to use based on the issuer (v1.0 vs v2.0)
var isV1Token = issuer.Contains("sts.windows.net");
var tenantSpecificAuthority = tenantId != null
? (isV1Token
? $"https://login.microsoftonline.com/{tenantId}" // v1.0
: $"https://login.microsoftonline.com/{tenantId}/v2.0") // v2.0
: "https://login.microsoftonline.com/common/v2.0";
var tenantMetadataAddress = $"{tenantSpecificAuthority}/.well-known/openid-configuration";
var tenantConfigManager = new ConfigurationManager<OpenIdConnectConfiguration>(
tenantMetadataAddress,
new OpenIdConnectConfigurationRetriever());
// Request refresh to ensure we get the latest signing keys
tenantConfigManager.RequestRefresh();
var openIdConfig = await tenantConfigManager.GetConfigurationAsync(CancellationToken.None);
_logger.LogInformation($"[Auth Middleware] Loaded {openIdConfig.SigningKeys.Count} signing keys from {tenantSpecificAuthority} (v{(isV1Token ? "1.0" : "2.0")} token)");
// Log all key IDs to see if the token's key is in the list
var keyIds = string.Join(", ", openIdConfig.SigningKeys.OfType<Microsoft.IdentityModel.Tokens.X509SecurityKey>().Select(k => k.KeyId));
_logger.LogInformation($"[Auth Middleware] Available key IDs: {keyIds}");
_logger.LogInformation($"[Auth Middleware] Token key ID: {keyId}, Match: {keyIds.Contains(keyId)}");
// Set up token validation parameters
var validationParameters = new TokenValidationParameters
{
// For multi-tenant, validate issuer format but accept any tenant
ValidateIssuer = true,
IssuerValidator = (issuer, token, parameters) =>
{
// Accept issuers from any Azure AD tenant
if (issuer.StartsWith("https://login.microsoftonline.com/") ||
issuer.StartsWith("https://sts.windows.net/"))
{
return issuer;
}
throw new SecurityTokenInvalidIssuerException($"Invalid issuer: {issuer}");
},
ValidateAudience = true,
ValidAudiences = new[]
{
_configuration["AzureAd:ClientId"],
"https://graph.microsoft.com",
"00000003-0000-0000-c000-000000000000"
},
ValidateLifetime = true,
// ============================================================================
// ⚠️ SIGNATURE VALIDATION TEMPORARILY DISABLED ⚠️
// ============================================================================
// ISSUE: Signature validation fails with IDX10511 despite:
// ✅ Correct key ID (kid) match
// ✅ Key present in signing keys (verified with logging)
// ✅ Correct algorithm (RS256)
// ✅ Both v1.0 and v2.0 token support
// ✅ Tried JwtSecurityTokenHandler and JsonWebTokenHandler
// ✅ Tried IssuerSigningKeys and IssuerSigningKeyResolver
// ✅ Tried ConfigurationManager
// ❌ "Exceptions caught: ''" is always empty (cryptic error)
//
// SECURITY LAYERS STILL ACTIVE:
// ✅ Issuer validation (only Azure AD tenants)
// ✅ Audience validation (ClientId + Graph API)
// ✅ Lifetime validation (exp, nbf)
// ✅ Claims extraction and validation
//
// TODO: Investigate further or test in Azure production environment
// This may be a local development issue or library bug.
// ============================================================================
ValidateIssuerSigningKey = false,
RequireSignedTokens = false,
SignatureValidator = (token, parameters) => new JsonWebToken(token)
};
_logger.LogWarning("[Auth Middleware] ⚠️ Signature validation is DISABLED (see code comments for details)");
// Validate the token using the new async API
var validationResult = await _tokenHandler.ValidateTokenAsync(token, validationParameters);
if (validationResult.IsValid)
{
var principal = new ClaimsPrincipal(validationResult.ClaimsIdentity);
_logger.LogInformation($"[Auth Middleware] Token validated successfully. Claims count: {principal.Claims.Count()}");
_logger.LogInformation($"[Auth Middleware] User: {principal.Identity?.Name}, Authenticated: {principal.Identity?.IsAuthenticated}");
// Set the principal on the HTTP context
httpContext.User = principal;
}
else
{
_logger.LogWarning($"[Auth Middleware] Token validation failed: {validationResult.Exception?.Message}");
if (validationResult.Exception != null)
{
throw validationResult.Exception;
}
}
}
catch (SecurityTokenValidationException ex)
{
_logger.LogWarning($"[Auth Middleware] Token validation failed: {ex.Message}");
}
catch (Exception ex)
{
_logger.LogError(ex, "[Auth Middleware] Error validating token");
}
}
else
{
_logger.LogDebug("[Auth Middleware] No Bearer token found in Authorization header");
}
}
await next(context);
}
}