Loading src/api/Program.cs +92 −5 Original line number Diff line number Diff line using System.Net.Http.Headers; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; using Azure.Core; using Azure.Identity; Loading @@ -12,6 +14,9 @@ using Cals.Visor.Infrastructure; using Hellang.Middleware.ProblemDetails; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Caching.Memory; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; using Quartz; Loading @@ -19,7 +24,7 @@ using Quartz; #pragma warning disable SA1005 #pragma warning disable SA1512 // Single-line comments should not be followed by blank line var builder = WebApplication.CreateBuilder(args); WebApplicationBuilder builder = WebApplication.CreateBuilder(args); ConfigureAzureKeyVault(builder); Loading @@ -39,6 +44,11 @@ builder.Services ValidateIssuer = true }; ConfigurationManager<OpenIdConnectConfiguration> configurationManager = new( options.MetadataAddress, new OpenIdConnectConfigurationRetriever() ); // Optional event logging for debugging options.Events = new JwtBearerEvents { Loading @@ -47,10 +57,86 @@ builder.Services Console.WriteLine($"JWT authentication failed: {context.Exception.Message}"); return Task.CompletedTask; }, OnTokenValidated = context => OnTokenValidated = async context => { Console.WriteLine($"JWT token valid for: {context.Principal?.Identity?.Name}"); return Task.CompletedTask; IMemoryCache cache = context.HttpContext.RequestServices.GetRequiredService<IMemoryCache>(); ILogger logger = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>() .CreateLogger("JwtBearer"); // Handle both token types string? accessToken = context.SecurityToken switch { System.IdentityModel.Tokens.Jwt.JwtSecurityToken jwt => jwt.RawData, Microsoft.IdentityModel.JsonWebTokens.JsonWebToken json => json.EncodedToken, _ => null }; if (string.IsNullOrEmpty(accessToken)) { logger.LogWarning("No access token found in SecurityToken."); return; } // Check cache first if (!cache.TryGetValue(accessToken, out Dictionary<string, object>? userInfo)) { // Fetch the discovery document OpenIdConnectConfiguration config = await configurationManager.GetConfigurationAsync(context.HttpContext.RequestAborted); string userInfoEndpoint = config.UserInfoEndpoint; if (string.IsNullOrEmpty(userInfoEndpoint)) { logger.LogError("UserInfo endpoint not found in discovery document."); context.Fail("UserInfo endpoint unavailable."); return; } try { using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); HttpResponseMessage response = await http.GetAsync(userInfoEndpoint); response.EnsureSuccessStatusCode(); Dictionary<string, object>? json = await response.Content.ReadFromJsonAsync<Dictionary<string, object>>(); userInfo = json ?? new(); // Cache until token expiration (use exp claim if present) string? expClaim = context.Principal?.FindFirst("exp")?.Value; if (!string.IsNullOrEmpty(expClaim) && long.TryParse(expClaim, out long expUnix)) { DateTimeOffset expiresAt = DateTimeOffset.FromUnixTimeSeconds(expUnix); cache.Set(accessToken, userInfo, expiresAt); } else { cache.Set(accessToken, userInfo, TimeSpan.FromMinutes(5)); // fallback } logger.LogInformation("Fetched and cached userinfo for token."); } catch (Exception ex) { logger.LogError(ex, "Failed to fetch userinfo."); context.Fail("Could not retrieve user info."); return; } } // Attach claims from userinfo to the identity if (userInfo != null) { var identity = (ClaimsIdentity)context.Principal!.Identity!; foreach (KeyValuePair<string, object> kvp in userInfo) { identity.AddClaim(new Claim(kvp.Key, kvp.Value?.ToString() ?? "")); } } await Task.CompletedTask; } }; Loading Loading @@ -99,7 +185,8 @@ builder.Services.AddAuthorizationBuilder() builder.Services.AddControllers(); builder.Services.AddInMemoryCaching(); //builder.Services.AddInMemoryCaching(); builder.Services.AddMemoryCache(); builder.Services.AddHttpContextAccessor(); Loading Loading
src/api/Program.cs +92 −5 Original line number Diff line number Diff line using System.Net.Http.Headers; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; using Azure.Core; using Azure.Identity; Loading @@ -12,6 +14,9 @@ using Cals.Visor.Infrastructure; using Hellang.Middleware.ProblemDetails; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Caching.Memory; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; using Quartz; Loading @@ -19,7 +24,7 @@ using Quartz; #pragma warning disable SA1005 #pragma warning disable SA1512 // Single-line comments should not be followed by blank line var builder = WebApplication.CreateBuilder(args); WebApplicationBuilder builder = WebApplication.CreateBuilder(args); ConfigureAzureKeyVault(builder); Loading @@ -39,6 +44,11 @@ builder.Services ValidateIssuer = true }; ConfigurationManager<OpenIdConnectConfiguration> configurationManager = new( options.MetadataAddress, new OpenIdConnectConfigurationRetriever() ); // Optional event logging for debugging options.Events = new JwtBearerEvents { Loading @@ -47,10 +57,86 @@ builder.Services Console.WriteLine($"JWT authentication failed: {context.Exception.Message}"); return Task.CompletedTask; }, OnTokenValidated = context => OnTokenValidated = async context => { Console.WriteLine($"JWT token valid for: {context.Principal?.Identity?.Name}"); return Task.CompletedTask; IMemoryCache cache = context.HttpContext.RequestServices.GetRequiredService<IMemoryCache>(); ILogger logger = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>() .CreateLogger("JwtBearer"); // Handle both token types string? accessToken = context.SecurityToken switch { System.IdentityModel.Tokens.Jwt.JwtSecurityToken jwt => jwt.RawData, Microsoft.IdentityModel.JsonWebTokens.JsonWebToken json => json.EncodedToken, _ => null }; if (string.IsNullOrEmpty(accessToken)) { logger.LogWarning("No access token found in SecurityToken."); return; } // Check cache first if (!cache.TryGetValue(accessToken, out Dictionary<string, object>? userInfo)) { // Fetch the discovery document OpenIdConnectConfiguration config = await configurationManager.GetConfigurationAsync(context.HttpContext.RequestAborted); string userInfoEndpoint = config.UserInfoEndpoint; if (string.IsNullOrEmpty(userInfoEndpoint)) { logger.LogError("UserInfo endpoint not found in discovery document."); context.Fail("UserInfo endpoint unavailable."); return; } try { using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); HttpResponseMessage response = await http.GetAsync(userInfoEndpoint); response.EnsureSuccessStatusCode(); Dictionary<string, object>? json = await response.Content.ReadFromJsonAsync<Dictionary<string, object>>(); userInfo = json ?? new(); // Cache until token expiration (use exp claim if present) string? expClaim = context.Principal?.FindFirst("exp")?.Value; if (!string.IsNullOrEmpty(expClaim) && long.TryParse(expClaim, out long expUnix)) { DateTimeOffset expiresAt = DateTimeOffset.FromUnixTimeSeconds(expUnix); cache.Set(accessToken, userInfo, expiresAt); } else { cache.Set(accessToken, userInfo, TimeSpan.FromMinutes(5)); // fallback } logger.LogInformation("Fetched and cached userinfo for token."); } catch (Exception ex) { logger.LogError(ex, "Failed to fetch userinfo."); context.Fail("Could not retrieve user info."); return; } } // Attach claims from userinfo to the identity if (userInfo != null) { var identity = (ClaimsIdentity)context.Principal!.Identity!; foreach (KeyValuePair<string, object> kvp in userInfo) { identity.AddClaim(new Claim(kvp.Key, kvp.Value?.ToString() ?? "")); } } await Task.CompletedTask; } }; Loading Loading @@ -99,7 +185,8 @@ builder.Services.AddAuthorizationBuilder() builder.Services.AddControllers(); builder.Services.AddInMemoryCaching(); //builder.Services.AddInMemoryCaching(); builder.Services.AddMemoryCache(); builder.Services.AddHttpContextAccessor(); Loading