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`
],
};