Loading src/api/Identity/AuthenticationServiceExtensions.cs +40 −65 Original line number Diff line number Diff line Loading @@ -4,10 +4,11 @@ using System.Security.Claims; using System.Security.Principal; using System.Text.Json; using Cals.Authentication; using Cals.Visor.Application.Identity; using Cals.Visor.Infrastructure.Contexts; using Cals.Visor.Models; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using Microsoft.IdentityModel.Protocols; Loading @@ -18,67 +19,36 @@ namespace Cals.Visor.Api.Identity; public static class AuthenticationServiceExtensions { public static IServiceCollection AddAppAuthentication( this IServiceCollection services, IConfiguration configuration, IHostEnvironment environment, string? devUsername = null, string? defaultAuthenticationScheme = null) public static IServiceCollection AddAppAuthentication(this IServiceCollection services) { if (string.IsNullOrEmpty(defaultAuthenticationScheme)) defaultAuthenticationScheme = environment.IsDevelopment() ? DevAuthenticationDefaults.AuthenticationScheme : JwtBearerDefaults.AuthenticationScheme; AuthenticationBuilder authBuilder = services.AddAuthentication(defaultAuthenticationScheme); // Always add JWT support authBuilder.AddJwtBearer(options => { ConfigureJwtBearer(options, configuration); }); // Add Dev authentication only in Development if (environment.IsDevelopment()) { authBuilder.AddDevAuthentication(options => { options.Claims = GetDevClaimsForUser(devUsername); }); } return services; } private static void ConfigureJwtBearer(JwtBearerOptions options, IConfiguration configuration) services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.Authority = configuration["Jwt:Authority"]; options.Audience = configuration["Jwt:Audience"]; options.MetadataAddress = $"{options.Authority}/.well-known/openid-configuration"; //ConfigureJwtBearer(options, configuration); options.TokenValidationParameters = new TokenValidationParameters { ValidateAudience = true, ValidateIssuer = true ValidateAudience = false // options.Audience is not automatically pulled out of IConfiguration }; var configManager = new ConfigurationManager<OpenIdConnectConfiguration>( options.MetadataAddress, new OpenIdConnectConfigurationRetriever() ); options.Events = new JwtBearerEvents { OnAuthenticationFailed = context => { Console.WriteLine($"JWT authentication failed: {context.Exception.Message}"); return Task.CompletedTask; }, OnTokenValidated = async context => { await HandleTokenValidatedAsync(context, configManager); // Ensure ConfigurationManager is accessed safely during request processing IConfigurationManager<OpenIdConnectConfiguration>? configurationManager = options.ConfigurationManager; if (configurationManager == null) { context.Fail("Configuration Manager is not available."); return; } await HandleTokenValidatedAsync(context, configurationManager); } }; }); return services; } private static async Task HandleTokenValidatedAsync( Loading Loading @@ -179,29 +149,34 @@ public static class AuthenticationServiceExtensions if (context.Principal == null || context.Principal.Identity == null) throw new Exception("No principal in request"); IIdentity? identity = context.Principal.Identity; ClaimsIdentity? identity = (ClaimsIdentity)context.Principal.Identity; // attach all the claims retrieved from the user info endpoint foreach (KeyValuePair<string, object> kvp in userInfo) { identity.AddClaim(new Claim(kvp.Key, kvp.Value?.ToString() ?? "")); } WiscEduUser oidcIdentity = new WiscEduOidcUser(context.Principal); WiscEduUser oidcIdentity = new WiscEduOidcUser(identity); // Extract the unique ID from the OIDC identity if (oidcIdentity.Pvi == null) throw new Exception($"OIDC unique identifier not found (expected pvi)."); // Resolve your application user service ApplicationDbContext dbContext = context.HttpContext.RequestServices .GetRequiredService<ApplicationDbContext>(); IUserRepository userRepository = context.HttpContext.RequestServices .GetRequiredService<IUserRepository>(); // Look up the internal user AppUser? user = await dbContext.Users.FirstOrDefaultAsync(u => u.UniqueID == oidcIdentity.Pvi); AppUser? user = await userRepository.GetByUniqueIdAsync(oidcIdentity.Pvi); // If user does not exist, create it user ??= oidcIdentity.ToAppUser(); dbContext.Add(user); await dbContext.SaveChangesAsync(); await userRepository.AddAsync(user).ConfigureAwait(false); // Add the app-specific internal user id claim if (user.AppUserId.HasValue) ((ClaimsIdentity)context.Principal.Identity).AddClaim(new Claim(VisorClaimTypes.Id, user.AppUserId.Value.ToString())); identity.AddClaim(new Claim(VisorClaimTypes.Id, user.AppUserId.Value.ToString())); } private static List<Claim> GetDevClaimsForUser(string? username) Loading src/api/Program.cs +3 −1 Original line number Diff line number Diff line Loading @@ -3,11 +3,13 @@ using System.Security.Cryptography.X509Certificates; using Azure.Core; using Azure.Identity; using Cals.Visor.Api.Authorization; using Cals.Visor.Api.Correlation; using Cals.Visor.Api.Identity; using Cals.Visor.Api.Middleware; using Cals.Visor.Application; using Cals.Visor.Application.Identity; using Cals.Visor.Infrastructure; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Caching.Memory; using Microsoft.OpenApi.Models; Loading @@ -19,7 +21,7 @@ WebApplicationBuilder builder = WebApplication.CreateBuilder(args); ConfigureAzureKeyVault(builder); builder.Services.AddAppAuthentication(builder.Configuration, builder.Environment, "ewdieckman"); builder.Services.AddAppAuthentication(); // Enable lowercase routes for controllers builder.Services.AddRouting(options => Loading src/api/appsettings.json +13 −3 Original line number Diff line number Diff line Loading @@ -14,9 +14,19 @@ "https://visor.it.cals.wisc.edu", "https://staging.visor.it.cals.wisc.edu" ], "Jwt": { "Authentication": { "Schemes": { "Bearer": { "Authority": "https://login.wisc.edu", "Audience": "https://login.wisc.edu" "Audience": "https://login.wisc.edu", "ValidAudiences": [ "https://login.wisc.edu" ], "ValidIssuers": [ "https://login.wisc.edu" ] } } }, "EmailSettings": { "MailServer": "smtp.wiscmail.wisc.edu", Loading src/application/Application.csproj +1 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ <ItemGroup> <ProjectReference Include="..\models\Models.csproj" /> <ProjectReference Include="..\nexus\Cals.Nexus.csproj" /> </ItemGroup> </Project> Loading
src/api/Identity/AuthenticationServiceExtensions.cs +40 −65 Original line number Diff line number Diff line Loading @@ -4,10 +4,11 @@ using System.Security.Claims; using System.Security.Principal; using System.Text.Json; using Cals.Authentication; using Cals.Visor.Application.Identity; using Cals.Visor.Infrastructure.Contexts; using Cals.Visor.Models; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using Microsoft.IdentityModel.Protocols; Loading @@ -18,67 +19,36 @@ namespace Cals.Visor.Api.Identity; public static class AuthenticationServiceExtensions { public static IServiceCollection AddAppAuthentication( this IServiceCollection services, IConfiguration configuration, IHostEnvironment environment, string? devUsername = null, string? defaultAuthenticationScheme = null) public static IServiceCollection AddAppAuthentication(this IServiceCollection services) { if (string.IsNullOrEmpty(defaultAuthenticationScheme)) defaultAuthenticationScheme = environment.IsDevelopment() ? DevAuthenticationDefaults.AuthenticationScheme : JwtBearerDefaults.AuthenticationScheme; AuthenticationBuilder authBuilder = services.AddAuthentication(defaultAuthenticationScheme); // Always add JWT support authBuilder.AddJwtBearer(options => { ConfigureJwtBearer(options, configuration); }); // Add Dev authentication only in Development if (environment.IsDevelopment()) { authBuilder.AddDevAuthentication(options => { options.Claims = GetDevClaimsForUser(devUsername); }); } return services; } private static void ConfigureJwtBearer(JwtBearerOptions options, IConfiguration configuration) services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.Authority = configuration["Jwt:Authority"]; options.Audience = configuration["Jwt:Audience"]; options.MetadataAddress = $"{options.Authority}/.well-known/openid-configuration"; //ConfigureJwtBearer(options, configuration); options.TokenValidationParameters = new TokenValidationParameters { ValidateAudience = true, ValidateIssuer = true ValidateAudience = false // options.Audience is not automatically pulled out of IConfiguration }; var configManager = new ConfigurationManager<OpenIdConnectConfiguration>( options.MetadataAddress, new OpenIdConnectConfigurationRetriever() ); options.Events = new JwtBearerEvents { OnAuthenticationFailed = context => { Console.WriteLine($"JWT authentication failed: {context.Exception.Message}"); return Task.CompletedTask; }, OnTokenValidated = async context => { await HandleTokenValidatedAsync(context, configManager); // Ensure ConfigurationManager is accessed safely during request processing IConfigurationManager<OpenIdConnectConfiguration>? configurationManager = options.ConfigurationManager; if (configurationManager == null) { context.Fail("Configuration Manager is not available."); return; } await HandleTokenValidatedAsync(context, configurationManager); } }; }); return services; } private static async Task HandleTokenValidatedAsync( Loading Loading @@ -179,29 +149,34 @@ public static class AuthenticationServiceExtensions if (context.Principal == null || context.Principal.Identity == null) throw new Exception("No principal in request"); IIdentity? identity = context.Principal.Identity; ClaimsIdentity? identity = (ClaimsIdentity)context.Principal.Identity; // attach all the claims retrieved from the user info endpoint foreach (KeyValuePair<string, object> kvp in userInfo) { identity.AddClaim(new Claim(kvp.Key, kvp.Value?.ToString() ?? "")); } WiscEduUser oidcIdentity = new WiscEduOidcUser(context.Principal); WiscEduUser oidcIdentity = new WiscEduOidcUser(identity); // Extract the unique ID from the OIDC identity if (oidcIdentity.Pvi == null) throw new Exception($"OIDC unique identifier not found (expected pvi)."); // Resolve your application user service ApplicationDbContext dbContext = context.HttpContext.RequestServices .GetRequiredService<ApplicationDbContext>(); IUserRepository userRepository = context.HttpContext.RequestServices .GetRequiredService<IUserRepository>(); // Look up the internal user AppUser? user = await dbContext.Users.FirstOrDefaultAsync(u => u.UniqueID == oidcIdentity.Pvi); AppUser? user = await userRepository.GetByUniqueIdAsync(oidcIdentity.Pvi); // If user does not exist, create it user ??= oidcIdentity.ToAppUser(); dbContext.Add(user); await dbContext.SaveChangesAsync(); await userRepository.AddAsync(user).ConfigureAwait(false); // Add the app-specific internal user id claim if (user.AppUserId.HasValue) ((ClaimsIdentity)context.Principal.Identity).AddClaim(new Claim(VisorClaimTypes.Id, user.AppUserId.Value.ToString())); identity.AddClaim(new Claim(VisorClaimTypes.Id, user.AppUserId.Value.ToString())); } private static List<Claim> GetDevClaimsForUser(string? username) Loading
src/api/Program.cs +3 −1 Original line number Diff line number Diff line Loading @@ -3,11 +3,13 @@ using System.Security.Cryptography.X509Certificates; using Azure.Core; using Azure.Identity; using Cals.Visor.Api.Authorization; using Cals.Visor.Api.Correlation; using Cals.Visor.Api.Identity; using Cals.Visor.Api.Middleware; using Cals.Visor.Application; using Cals.Visor.Application.Identity; using Cals.Visor.Infrastructure; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Caching.Memory; using Microsoft.OpenApi.Models; Loading @@ -19,7 +21,7 @@ WebApplicationBuilder builder = WebApplication.CreateBuilder(args); ConfigureAzureKeyVault(builder); builder.Services.AddAppAuthentication(builder.Configuration, builder.Environment, "ewdieckman"); builder.Services.AddAppAuthentication(); // Enable lowercase routes for controllers builder.Services.AddRouting(options => Loading
src/api/appsettings.json +13 −3 Original line number Diff line number Diff line Loading @@ -14,9 +14,19 @@ "https://visor.it.cals.wisc.edu", "https://staging.visor.it.cals.wisc.edu" ], "Jwt": { "Authentication": { "Schemes": { "Bearer": { "Authority": "https://login.wisc.edu", "Audience": "https://login.wisc.edu" "Audience": "https://login.wisc.edu", "ValidAudiences": [ "https://login.wisc.edu" ], "ValidIssuers": [ "https://login.wisc.edu" ] } } }, "EmailSettings": { "MailServer": "smtp.wiscmail.wisc.edu", Loading
src/application/Application.csproj +1 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ <ItemGroup> <ProjectReference Include="..\models\Models.csproj" /> <ProjectReference Include="..\nexus\Cals.Nexus.csproj" /> </ItemGroup> </Project>