From 064319334dc02e71748070ebca8a448a6c720ea8 Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Thu, 5 Feb 2026 22:02:12 +0100 Subject: [PATCH] Signing fix --- .../Functions/AuthFunction.cs | 9 +- .../Helpers/TenantHelper.cs | 13 ++- .../Middleware/AuthenticationMiddleware.cs | 96 ++++++------------- .../autopilot-monitor-web/lib/msalConfig.ts | 8 +- 4 files changed, 45 insertions(+), 81 deletions(-) diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/AuthFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/AuthFunction.cs index 99709d87..29559853 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/AuthFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/AuthFunction.cs @@ -37,24 +37,17 @@ public class AuthFunction { var principal = context.GetUser(); - _logger.LogInformation($"GetCurrentUser - Principal is null: {principal == null}"); - if (principal == null) { - _logger.LogWarning("GetCurrentUser - No ClaimsPrincipal found, returning Unauthorized"); + _logger.LogWarning("GetCurrentUser - No authentication found"); return req.CreateResponse(HttpStatusCode.Unauthorized); } - _logger.LogInformation($"GetCurrentUser - Principal identity authenticated: {principal.Identity?.IsAuthenticated}, Claims count: {principal.Claims.Count()}"); - _logger.LogInformation($"GetCurrentUser - All claims: {string.Join(", ", principal.Claims.Select(c => $"{c.Type}={c.Value}"))}"); - var tenantId = principal.GetTenantId(); var upn = principal.GetUserPrincipalName(); var displayName = principal.GetDisplayName(); var objectId = principal.GetObjectId(); - _logger.LogInformation($"GetCurrentUser - UPN: {upn}, ObjectId: {objectId}, TenantId: {tenantId}"); - // Check if user is galactic admin var isGalacticAdmin = await _galacticAdminService.IsGalacticAdminAsync(upn); diff --git a/src/Backend/AutopilotMonitor.Functions/Helpers/TenantHelper.cs b/src/Backend/AutopilotMonitor.Functions/Helpers/TenantHelper.cs index f80bc6f5..80251654 100644 --- a/src/Backend/AutopilotMonitor.Functions/Helpers/TenantHelper.cs +++ b/src/Backend/AutopilotMonitor.Functions/Helpers/TenantHelper.cs @@ -32,9 +32,10 @@ public static class TenantHelper /// /// Extracts the tenant ID from the authenticated user's JWT token claims. - /// Uses the Azure AD tenant ID ('tid' claim) which identifies which customer/organization owns the data. + /// Uses the Azure AD tenant ID claim which identifies which customer/organization owns the data. + /// Supports both v1.0 and v2.0 tokens. /// - /// Normal Users: Can only see sessions with their own tenant ID (from JWT 'tid' claim) + /// Normal Users: Can only see sessions with their own tenant ID (from JWT token) /// /// The HTTP request /// The tenant ID from the JWT token @@ -65,8 +66,12 @@ public static class TenantHelper throw new UnauthorizedAccessException("User is not authenticated. JWT token required."); } - // Extract tenant ID from JWT 'tid' claim (Azure AD tenant ID) - var tenantIdClaim = user.FindFirst("tid")?.Value; + // Extract tenant ID from JWT token + // v2.0 tokens: "tid" claim + // v1.0 tokens: "http://schemas.microsoft.com/identity/claims/tenantid" claim + var tenantIdClaim = user.FindFirst("tid")?.Value ?? + user.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid")?.Value; + if (string.IsNullOrEmpty(tenantIdClaim)) { throw new UnauthorizedAccessException("Tenant ID (tid) claim not found in token"); diff --git a/src/Backend/AutopilotMonitor.Functions/Middleware/AuthenticationMiddleware.cs b/src/Backend/AutopilotMonitor.Functions/Middleware/AuthenticationMiddleware.cs index 53adf9cc..1daf8b84 100644 --- a/src/Backend/AutopilotMonitor.Functions/Middleware/AuthenticationMiddleware.cs +++ b/src/Backend/AutopilotMonitor.Functions/Middleware/AuthenticationMiddleware.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; -using Microsoft.IdentityModel.JsonWebTokens; +using System.IdentityModel.Tokens.Jwt; using System.Net; using System.Security.Claims; @@ -20,7 +20,7 @@ public class AuthenticationMiddleware : IFunctionsWorkerMiddleware { private readonly ILogger _logger; private readonly IConfiguration _configuration; - private readonly JsonWebTokenHandler _tokenHandler; + private readonly JwtSecurityTokenHandler _tokenHandler; // Cache configuration managers per tenant to avoid repeated OIDC metadata fetches private readonly Dictionary> _configManagerCache; @@ -36,7 +36,7 @@ public class AuthenticationMiddleware : IFunctionsWorkerMiddleware { _logger = logger; _configuration = configuration; - _tokenHandler = new JsonWebTokenHandler(); + _tokenHandler = new JwtSecurityTokenHandler(); _configManagerCache = new Dictionary>(); // Disable PII logging in production for security @@ -48,27 +48,18 @@ public class AuthenticationMiddleware : IFunctionsWorkerMiddleware 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 jwtToken = _tokenHandler.ReadJwtToken(token); + var tenantId = jwtToken.Claims.FirstOrDefault(c => c.Type == "tid")?.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"); @@ -111,14 +102,7 @@ public class AuthenticationMiddleware : IFunctionsWorkerMiddleware 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().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 + // Set up token validation parameters with OIDC signing keys var validationParameters = new TokenValidationParameters { // For multi-tenant, validate issuer format but accept any tenant @@ -137,46 +121,30 @@ public class AuthenticationMiddleware : IFunctionsWorkerMiddleware ValidAudiences = new[] { _configuration["AzureAd:ClientId"], - "https://graph.microsoft.com", - "00000003-0000-0000-c000-000000000000" + $"api://{_configuration["AzureAd:ClientId"]}" }, 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) + // Signature validation enabled + // Token is requested for backend API (api:///access_as_user) + ValidateIssuerSigningKey = true, + RequireSignedTokens = true, + + // Use key resolver for dynamic key resolution + IssuerSigningKeyResolver = (token, securityToken, kid, validationParameters) => + { + var matchedKeys = openIdConfig.SigningKeys.Where(k => k.KeyId == kid).ToList(); + return matchedKeys.Any() ? matchedKeys : openIdConfig.SigningKeys; + }, + + // Set algorithm validators to ensure we only accept RS256 + ValidAlgorithms = new[] { "RS256" } }; - _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) + // Validate the token using JwtSecurityTokenHandler (synchronous API) + try { - var principal = new ClaimsPrincipal(validationResult.ClaimsIdentity); + var principal = _tokenHandler.ValidateToken(token, validationParameters, out var validatedToken); // Get user identifier for logging (same logic as TenantHelper.GetUserIdentifier) var userIdentifier = principal.FindFirst("upn")?.Value ?? @@ -187,30 +155,26 @@ public class AuthenticationMiddleware : IFunctionsWorkerMiddleware principal.FindFirst("name")?.Value ?? "Unknown"; - _logger.LogInformation($"[Auth Middleware] Token validated successfully. Claims count: {principal.Claims.Count()}"); - _logger.LogInformation($"[Auth Middleware] User: {userIdentifier}, Authenticated: {principal.Identity?.IsAuthenticated}"); + _logger.LogInformation($"[Auth Middleware] ✅ Authenticated: {userIdentifier}"); // Set the principal on both the HTTP context AND the FunctionContext // This is critical for Azure Functions Isolated Worker (.NET 8) httpContext.User = principal; context.Items["ClaimsPrincipal"] = principal; } - else + catch (SecurityTokenException ex) { - _logger.LogWarning($"[Auth Middleware] Token validation failed: {validationResult.Exception?.Message}"); - if (validationResult.Exception != null) - { - throw validationResult.Exception; - } + _logger.LogWarning($"[Auth Middleware] ❌ Token validation failed: {ex.Message}"); + throw; } } catch (SecurityTokenValidationException ex) { - _logger.LogWarning($"[Auth Middleware] Token validation failed: {ex.Message}"); + _logger.LogWarning($"[Auth Middleware] ❌ Token validation failed: {ex.Message}"); } catch (Exception ex) { - _logger.LogError(ex, "[Auth Middleware] Error validating token"); + _logger.LogError(ex, "[Auth Middleware] ❌ Error validating token"); } } else diff --git a/src/Web/autopilot-monitor-web/lib/msalConfig.ts b/src/Web/autopilot-monitor-web/lib/msalConfig.ts index 00c8a799..86eb5a9c 100644 --- a/src/Web/autopilot-monitor-web/lib/msalConfig.ts +++ b/src/Web/autopilot-monitor-web/lib/msalConfig.ts @@ -67,12 +67,14 @@ export const loginRequest: RedirectRequest = { /** * Scopes for accessing the backend API - * For now, using User.Read to get basic user info - * TODO: Later expose an API in App Registration and use custom scopes + * IMPORTANT: Backend API must be exposed in Azure AD App Registration with this scope + * Format: api:///access_as_user + * For testing, you can also use User.Read but disable signature validation in backend */ export const apiRequest = { scopes: [ - "User.Read", + // Option 1 (recommended): Use backend API scope after exposing API in Azure AD + `api://${process.env.NEXT_PUBLIC_BACKEND_CLIENT_ID}/access_as_user` ], };