Signing fix

This commit is contained in:
Oliver Kieselbach
2026-02-05 22:02:12 +01:00
parent 557dd22c53
commit 064319334d
4 changed files with 45 additions and 81 deletions
@@ -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);
@@ -32,9 +32,10 @@ public static class TenantHelper
/// <summary>
/// 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)
/// </summary>
/// <param name="req">The HTTP request</param>
/// <returns>The tenant ID from the JWT token</returns>
@@ -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");
@@ -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<AuthenticationMiddleware> _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<string, IConfigurationManager<OpenIdConnectConfiguration>> _configManagerCache;
@@ -36,7 +36,7 @@ public class AuthenticationMiddleware : IFunctionsWorkerMiddleware
{
_logger = logger;
_configuration = configuration;
_tokenHandler = new JsonWebTokenHandler();
_tokenHandler = new JwtSecurityTokenHandler();
_configManagerCache = new Dictionary<string, IConfigurationManager<OpenIdConnectConfiguration>>();
// 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<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
// 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://<clientId>/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
@@ -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://<backend-client-id>/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`
],
};