Loading src/api/Cals.Visor.Api.xml +87 −0 Original line number Diff line number Diff line Loading @@ -51,6 +51,93 @@ Enables Visor user with the given options. </summary> </member> <member name="T:Cals.Visor.Api.Identity.DevAuthenticationDefaults"> <summary> Default values for Shibboleth authentication middleware </summary> </member> <member name="F:Cals.Visor.Api.Identity.DevAuthenticationDefaults.AuthenticationScheme"> <summary> Default value for <see cref="P:Microsoft.AspNetCore.Authentication.AuthenticationScheme.Name"/>. </summary> </member> <member name="F:Cals.Visor.Api.Identity.DevAuthenticationDefaults.DisplayName"> <summary> Default value for <see cref="P:Microsoft.AspNetCore.Authentication.AuthenticationScheme.DisplayName"/>. </summary> </member> <member name="F:Cals.Visor.Api.Identity.DevAuthenticationDefaults.Issuer"> <summary> Default value for <see cref="P:Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions.ClaimsIssuer"/>. </summary> </member> <member name="M:Cals.Visor.Api.Identity.DevAuthenticationExtensions.AddDevAuthentication(Microsoft.AspNetCore.Authentication.AuthenticationBuilder)"> <summary> Enables dev authentication using the default scheme <see cref="F:Cals.Visor.Api.Identity.DevAuthenticationDefaults.AuthenticationScheme"/> </summary> <para> Manually configuring claims for JWT authentication </para> <param name="builder">The <see cref="T:Microsoft.AspNetCore.Authentication.AuthenticationBuilder"/>.</param> <returns>A reference to <paramref name="builder"/> after the operation has completed.</returns> </member> <member name="M:Cals.Visor.Api.Identity.DevAuthenticationExtensions.AddDevAuthentication(Microsoft.AspNetCore.Authentication.AuthenticationBuilder,System.String)"> <summary> Enables dev authentication using a pre-defined scheme. <para> Manually configuring claims for JWT authentication </para> </summary> <param name="builder">The <see cref="T:Microsoft.AspNetCore.Authentication.AuthenticationBuilder"/>.</param> <param name="authenticationScheme">The authentication scheme.</param> <returns>A reference to <paramref name="builder"/> after the operation has completed.</returns> </member> <member name="M:Cals.Visor.Api.Identity.DevAuthenticationExtensions.AddDevAuthentication(Microsoft.AspNetCore.Authentication.AuthenticationBuilder,System.Action{Cals.Visor.Api.Identity.DevAuthenticationOptions})"> <summary> Enables dev authentication using the default scheme <see cref="F:Cals.Visor.Api.Identity.DevAuthenticationDefaults.AuthenticationScheme"/> <para> Manually configuring claims for JWT authentication </para> </summary> <param name="builder">The <see cref="T:Microsoft.AspNetCore.Authentication.AuthenticationBuilder"/>.</param> <param name="configureOptions">A delegate that allows configuring <see cref="T:Cals.Visor.Api.Identity.DevAuthenticationOptions"/>.</param> <returns>A reference to <paramref name="builder"/> after the operation has completed.</returns> </member> <member name="M:Cals.Visor.Api.Identity.DevAuthenticationExtensions.AddDevAuthentication(Microsoft.AspNetCore.Authentication.AuthenticationBuilder,System.String,System.Action{Cals.Visor.Api.Identity.DevAuthenticationOptions})"> <summary> Enables UW Shibboleth authentication using the specified scheme. <para> Manually configuring claims for JWT authentication </para> </summary> <param name="builder">The <see cref="T:Microsoft.AspNetCore.Authentication.AuthenticationBuilder"/>.</param> <param name="authenticationScheme">The authentication scheme.</param> <param name="configureOptions">A delegate that allows configuring <see cref="T:Cals.Visor.Api.Identity.DevAuthenticationOptions"/>.</param> <returns>A reference to <paramref name="builder"/> after the operation has completed.</returns> </member> <member name="M:Cals.Visor.Api.Identity.DevAuthenticationExtensions.AddDevAuthentication(Microsoft.AspNetCore.Authentication.AuthenticationBuilder,System.String,System.String,System.Action{Cals.Visor.Api.Identity.DevAuthenticationOptions})"> <summary> Enables UW Shibboleth authentication using the specified scheme. <para> Manually configuring claims for JWT authentication </para> </summary> <param name="builder">The <see cref="T:Microsoft.AspNetCore.Authentication.AuthenticationBuilder"/>.</param> <param name="authenticationScheme">The authentication scheme.</param> <param name="displayName">The display name for the authentication handler.</param> <param name="configureOptions">A delegate that allows configuring <see cref="T:Cals.Visor.Api.Identity.DevAuthenticationOptions"/>.</param> <returns>A reference to <paramref name="builder"/> after the operation has completed.</returns> </member> <member name="T:Cals.Visor.Api.Identity.DevAuthenticationOptions"> <summary> Configuration options for <see cref="T:Cals.Visor.Api.Identity.DevAuthenticationHandler"/>. </summary> </member> <member name="P:Cals.Visor.Api.Identity.DevAuthenticationOptions.TokenExpiration"> <summary> Length of time a token is valid </summary> </member> <member name="T:Cals.Visor.Api.Identity.IdentityMiddleware"> <summary> Middleware to add this app user's identity to the ClaimsPrincipal. Loading src/api/Identity/AuthenticationServiceExtensions.cs 0 → 100644 +240 −0 Original line number Diff line number Diff line using System.IdentityModel.Tokens.Jwt; using System.Net.Http.Headers; using System.Security.Claims; using System.Text.Json; using Cals.Authentication; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Caching.Memory; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; 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) { if (string.IsNullOrEmpty(defaultAuthenticationScheme)) defaultAuthenticationScheme = environment.IsDevelopment() ? DevAuthenticationDefaults.AuthenticationScheme : JwtBearerDefaults.AuthenticationScheme; var 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) { options.Authority = configuration["Jwt:Authority"]; options.Audience = configuration["Jwt:Audience"]; options.MetadataAddress = $"{options.Authority}/.well-known/openid-configuration"; options.TokenValidationParameters = new TokenValidationParameters { ValidateAudience = true, ValidateIssuer = true }; 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); } }; } private static async Task HandleTokenValidatedAsync( TokenValidatedContext context, IConfigurationManager<OpenIdConnectConfiguration> configurationManager) { var cache = context.HttpContext.RequestServices.GetRequiredService<IMemoryCache>(); var logger = context.HttpContext.RequestServices .GetRequiredService<ILoggerFactory>() .CreateLogger("JwtBearer"); string? accessToken = context.SecurityToken switch { 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; } if (!cache.TryGetValue(accessToken, out Dictionary<string, object>? userInfo)) { userInfo = await FetchAndCacheUserInfoAsync(accessToken, cache, configurationManager, context, logger); if (userInfo == null) { context.Fail("Failed to retrieve user info."); return; } } AttachUserInfoClaims(context, userInfo!); } private static async Task<Dictionary<string, object>?> FetchAndCacheUserInfoAsync( string accessToken, IMemoryCache cache, IConfigurationManager<OpenIdConnectConfiguration> configurationManager, TokenValidatedContext context, ILogger logger) { try { var config = await configurationManager.GetConfigurationAsync(context.HttpContext.RequestAborted); string userInfoEndpoint = config.UserInfoEndpoint; if (string.IsNullOrEmpty(userInfoEndpoint)) { logger.LogError("UserInfo endpoint not found in discovery document."); return null; } using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); var response = await http.GetAsync(userInfoEndpoint); response.EnsureSuccessStatusCode(); var userInfo = await response.Content.ReadFromJsonAsync<Dictionary<string, object>>() ?? new(); CacheUserInfo(cache, accessToken, userInfo, context); logger.LogInformation("Fetched and cached userinfo for token."); return userInfo; } catch (Exception ex) { logger.LogError(ex, "Failed to fetch userinfo."); return null; } } private static void CacheUserInfo( IMemoryCache cache, string accessToken, Dictionary<string, object> userInfo, TokenValidatedContext context) { 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)); } } private static void AttachUserInfoClaims( TokenValidatedContext context, Dictionary<string, object> userInfo) { var identity = (ClaimsIdentity)context.Principal!.Identity!; foreach (var kvp in userInfo) { identity.AddClaim(new Claim(kvp.Key, kvp.Value?.ToString() ?? "")); } } private static List<Claim> GetDevClaimsForUser(string? username) { string issuedBy = "https://localhost"; // Default identity (fallback) var defaultClaims = new List<Claim> { new(WiscEduClaimTypes.FirstName, "Dev"), new(WiscEduClaimTypes.LastName, "User"), new(WiscEduClaimTypes.Name, "Dev User"), new(WiscEduClaimTypes.NetId, "jdoe"), new(WiscEduClaimTypes.Email, "it_apps@cals.wisc.edu"), new(JwtRegisteredClaimNames.Iss, issuedBy) }; // Select user-specific claims return username?.ToLowerInvariant() switch { "ewdieckman" => [ new(WiscEduClaimTypes.FirstName, "Eric"), new(WiscEduClaimTypes.LastName, "Dieckman"), new(WiscEduClaimTypes.Name, "Eric Dieckman"), new(WiscEduClaimTypes.NetId, "ewdieckman"), new(WiscEduClaimTypes.Email, "eric.dieckman@wisc.edu"), new(WiscEduClaimTypes.Pvi, "UW106Z480"), new(WiscEduClaimTypes.PrincipalName, "ewdieckman@wisc.edu"), new(WiscEduClaimTypes.Group, CreateGroupJson( [ "uw:domain:cals.wisc.edu:apps:developers", "uw:domain:cals.wisc.edu:apps:visor:admin", "uw:domain:cals.wisc.edu:apps:visor:users" ] )), new(JwtRegisteredClaimNames.Iss, issuedBy) ], "slien5" => [ new(WiscEduClaimTypes.FirstName, "Sarah"), new(WiscEduClaimTypes.LastName, "Lien"), new(WiscEduClaimTypes.Name, "Sarah Lien"), new(WiscEduClaimTypes.NetId, "slien5"), new(WiscEduClaimTypes.Email, "sarah.lien@wisc.edu"), new(WiscEduClaimTypes.Pvi, "UW121Z750"), new(WiscEduClaimTypes.PrincipalName, "slien5@wisc.edu"), new(WiscEduClaimTypes.Group, CreateGroupJson( [ "uw:domain:cals.wisc.edu:apps:visor:users" ] )), new(JwtRegisteredClaimNames.Iss, issuedBy) ], _ => defaultClaims }; } private static string CreateGroupJson(List<string> groups) { return JsonSerializer.Serialize(groups); } } src/api/Identity/DevAuthenticationDefaults.cs 0 → 100644 +25 −0 Original line number Diff line number Diff line using Microsoft.AspNetCore.Authentication; namespace Cals.Visor.Api.Identity; /// <summary> /// Default values for Shibboleth authentication middleware /// </summary> public static class DevAuthenticationDefaults { /// <summary> /// Default value for <see cref="AuthenticationScheme.Name"/>. /// </summary> public const string AuthenticationScheme = "DevAuthentication"; /// <summary> /// Default value for <see cref="AuthenticationScheme.DisplayName"/>. /// </summary> public const string DisplayName = "DevAuthentication"; /// <summary> /// Default value for <see cref="AuthenticationSchemeOptions.ClaimsIssuer"/>. /// </summary> public const string Issuer = "DevAuthentication"; } src/api/Identity/DevAuthenticationExtensions.cs 0 → 100644 +77 −0 Original line number Diff line number Diff line using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Options; namespace Cals.Visor.Api.Identity; public static class DevAuthenticationExtensions { /// <summary> /// Enables dev authentication using the default scheme <see cref="DevAuthenticationDefaults.AuthenticationScheme"/> /// </summary> /// <para> /// Manually configuring claims for JWT authentication /// </para> /// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param> /// <returns>A reference to <paramref name="builder"/> after the operation has completed.</returns> public static AuthenticationBuilder AddDevAuthentication(this AuthenticationBuilder builder) { return builder.AddDevAuthentication(DevAuthenticationDefaults.AuthenticationScheme, _ => { }); } /// <summary> /// Enables dev authentication using a pre-defined scheme. /// <para> /// Manually configuring claims for JWT authentication /// </para> /// </summary> /// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param> /// <param name="authenticationScheme">The authentication scheme.</param> /// <returns>A reference to <paramref name="builder"/> after the operation has completed.</returns> public static AuthenticationBuilder AddDevAuthentication(this AuthenticationBuilder builder, string authenticationScheme) => builder.AddDevAuthentication(authenticationScheme, _ => { }); /// <summary> /// Enables dev authentication using the default scheme <see cref="DevAuthenticationDefaults.AuthenticationScheme"/> /// <para> /// Manually configuring claims for JWT authentication /// </para> /// </summary> /// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param> /// <param name="configureOptions">A delegate that allows configuring <see cref="DevAuthenticationOptions"/>.</param> /// <returns>A reference to <paramref name="builder"/> after the operation has completed.</returns> public static AuthenticationBuilder AddDevAuthentication(this AuthenticationBuilder builder, Action<DevAuthenticationOptions> configureOptions) => builder.AddDevAuthentication(DevAuthenticationDefaults.AuthenticationScheme, configureOptions); /// <summary> /// Enables UW Shibboleth authentication using the specified scheme. /// <para> /// Manually configuring claims for JWT authentication /// </para> /// </summary> /// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param> /// <param name="authenticationScheme">The authentication scheme.</param> /// <param name="configureOptions">A delegate that allows configuring <see cref="DevAuthenticationOptions"/>.</param> /// <returns>A reference to <paramref name="builder"/> after the operation has completed.</returns> public static AuthenticationBuilder AddDevAuthentication(this AuthenticationBuilder builder, string authenticationScheme, Action<DevAuthenticationOptions> configureOptions) => builder.AddDevAuthentication(authenticationScheme, displayName: null, configureOptions: configureOptions); /// <summary> /// Enables UW Shibboleth authentication using the specified scheme. /// <para> /// Manually configuring claims for JWT authentication /// </para> /// </summary> /// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param> /// <param name="authenticationScheme">The authentication scheme.</param> /// <param name="displayName">The display name for the authentication handler.</param> /// <param name="configureOptions">A delegate that allows configuring <see cref="DevAuthenticationOptions"/>.</param> /// <returns>A reference to <paramref name="builder"/> after the operation has completed.</returns> public static AuthenticationBuilder AddDevAuthentication(this AuthenticationBuilder builder, string authenticationScheme, string? displayName, Action<DevAuthenticationOptions> configureOptions) { return builder.AddScheme<DevAuthenticationOptions, DevAuthenticationHandler>(authenticationScheme, displayName, configureOptions); } } src/api/Identity/DevAuthenticationHandler.cs 0 → 100644 +38 −0 Original line number Diff line number Diff line using System.Security.Claims; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Options; namespace Cals.Visor.Api.Identity; public class DevAuthenticationHandler : AuthenticationHandler<DevAuthenticationOptions> { private readonly TimeProvider _timeProvider; public DevAuthenticationHandler( IOptionsMonitor<DevAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, TimeProvider timeProvider) : base(options, logger, encoder) => _timeProvider = timeProvider; protected override Task<AuthenticateResult> HandleAuthenticateAsync() { List<Claim> claims = Options.Claims ?? [ new Claim(ClaimTypes.Name, "default-dev-user") ]; // Example: add issued-at claim using TimeProvider DateTime issuedAt = _timeProvider.GetUtcNow().UtcDateTime; claims.Add(new Claim("iat", issuedAt.ToString("O"))); claims.Add(new Claim("exp", (issuedAt + Options.TokenExpiration).ToString("O"))); var identity = new ClaimsIdentity(claims, Scheme.Name); var principal = new ClaimsPrincipal(identity); var ticket = new AuthenticationTicket(principal, Scheme.Name); return Task.FromResult(AuthenticateResult.Success(ticket)); } } Loading
src/api/Cals.Visor.Api.xml +87 −0 Original line number Diff line number Diff line Loading @@ -51,6 +51,93 @@ Enables Visor user with the given options. </summary> </member> <member name="T:Cals.Visor.Api.Identity.DevAuthenticationDefaults"> <summary> Default values for Shibboleth authentication middleware </summary> </member> <member name="F:Cals.Visor.Api.Identity.DevAuthenticationDefaults.AuthenticationScheme"> <summary> Default value for <see cref="P:Microsoft.AspNetCore.Authentication.AuthenticationScheme.Name"/>. </summary> </member> <member name="F:Cals.Visor.Api.Identity.DevAuthenticationDefaults.DisplayName"> <summary> Default value for <see cref="P:Microsoft.AspNetCore.Authentication.AuthenticationScheme.DisplayName"/>. </summary> </member> <member name="F:Cals.Visor.Api.Identity.DevAuthenticationDefaults.Issuer"> <summary> Default value for <see cref="P:Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions.ClaimsIssuer"/>. </summary> </member> <member name="M:Cals.Visor.Api.Identity.DevAuthenticationExtensions.AddDevAuthentication(Microsoft.AspNetCore.Authentication.AuthenticationBuilder)"> <summary> Enables dev authentication using the default scheme <see cref="F:Cals.Visor.Api.Identity.DevAuthenticationDefaults.AuthenticationScheme"/> </summary> <para> Manually configuring claims for JWT authentication </para> <param name="builder">The <see cref="T:Microsoft.AspNetCore.Authentication.AuthenticationBuilder"/>.</param> <returns>A reference to <paramref name="builder"/> after the operation has completed.</returns> </member> <member name="M:Cals.Visor.Api.Identity.DevAuthenticationExtensions.AddDevAuthentication(Microsoft.AspNetCore.Authentication.AuthenticationBuilder,System.String)"> <summary> Enables dev authentication using a pre-defined scheme. <para> Manually configuring claims for JWT authentication </para> </summary> <param name="builder">The <see cref="T:Microsoft.AspNetCore.Authentication.AuthenticationBuilder"/>.</param> <param name="authenticationScheme">The authentication scheme.</param> <returns>A reference to <paramref name="builder"/> after the operation has completed.</returns> </member> <member name="M:Cals.Visor.Api.Identity.DevAuthenticationExtensions.AddDevAuthentication(Microsoft.AspNetCore.Authentication.AuthenticationBuilder,System.Action{Cals.Visor.Api.Identity.DevAuthenticationOptions})"> <summary> Enables dev authentication using the default scheme <see cref="F:Cals.Visor.Api.Identity.DevAuthenticationDefaults.AuthenticationScheme"/> <para> Manually configuring claims for JWT authentication </para> </summary> <param name="builder">The <see cref="T:Microsoft.AspNetCore.Authentication.AuthenticationBuilder"/>.</param> <param name="configureOptions">A delegate that allows configuring <see cref="T:Cals.Visor.Api.Identity.DevAuthenticationOptions"/>.</param> <returns>A reference to <paramref name="builder"/> after the operation has completed.</returns> </member> <member name="M:Cals.Visor.Api.Identity.DevAuthenticationExtensions.AddDevAuthentication(Microsoft.AspNetCore.Authentication.AuthenticationBuilder,System.String,System.Action{Cals.Visor.Api.Identity.DevAuthenticationOptions})"> <summary> Enables UW Shibboleth authentication using the specified scheme. <para> Manually configuring claims for JWT authentication </para> </summary> <param name="builder">The <see cref="T:Microsoft.AspNetCore.Authentication.AuthenticationBuilder"/>.</param> <param name="authenticationScheme">The authentication scheme.</param> <param name="configureOptions">A delegate that allows configuring <see cref="T:Cals.Visor.Api.Identity.DevAuthenticationOptions"/>.</param> <returns>A reference to <paramref name="builder"/> after the operation has completed.</returns> </member> <member name="M:Cals.Visor.Api.Identity.DevAuthenticationExtensions.AddDevAuthentication(Microsoft.AspNetCore.Authentication.AuthenticationBuilder,System.String,System.String,System.Action{Cals.Visor.Api.Identity.DevAuthenticationOptions})"> <summary> Enables UW Shibboleth authentication using the specified scheme. <para> Manually configuring claims for JWT authentication </para> </summary> <param name="builder">The <see cref="T:Microsoft.AspNetCore.Authentication.AuthenticationBuilder"/>.</param> <param name="authenticationScheme">The authentication scheme.</param> <param name="displayName">The display name for the authentication handler.</param> <param name="configureOptions">A delegate that allows configuring <see cref="T:Cals.Visor.Api.Identity.DevAuthenticationOptions"/>.</param> <returns>A reference to <paramref name="builder"/> after the operation has completed.</returns> </member> <member name="T:Cals.Visor.Api.Identity.DevAuthenticationOptions"> <summary> Configuration options for <see cref="T:Cals.Visor.Api.Identity.DevAuthenticationHandler"/>. </summary> </member> <member name="P:Cals.Visor.Api.Identity.DevAuthenticationOptions.TokenExpiration"> <summary> Length of time a token is valid </summary> </member> <member name="T:Cals.Visor.Api.Identity.IdentityMiddleware"> <summary> Middleware to add this app user's identity to the ClaimsPrincipal. Loading
src/api/Identity/AuthenticationServiceExtensions.cs 0 → 100644 +240 −0 Original line number Diff line number Diff line using System.IdentityModel.Tokens.Jwt; using System.Net.Http.Headers; using System.Security.Claims; using System.Text.Json; using Cals.Authentication; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Caching.Memory; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; 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) { if (string.IsNullOrEmpty(defaultAuthenticationScheme)) defaultAuthenticationScheme = environment.IsDevelopment() ? DevAuthenticationDefaults.AuthenticationScheme : JwtBearerDefaults.AuthenticationScheme; var 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) { options.Authority = configuration["Jwt:Authority"]; options.Audience = configuration["Jwt:Audience"]; options.MetadataAddress = $"{options.Authority}/.well-known/openid-configuration"; options.TokenValidationParameters = new TokenValidationParameters { ValidateAudience = true, ValidateIssuer = true }; 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); } }; } private static async Task HandleTokenValidatedAsync( TokenValidatedContext context, IConfigurationManager<OpenIdConnectConfiguration> configurationManager) { var cache = context.HttpContext.RequestServices.GetRequiredService<IMemoryCache>(); var logger = context.HttpContext.RequestServices .GetRequiredService<ILoggerFactory>() .CreateLogger("JwtBearer"); string? accessToken = context.SecurityToken switch { 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; } if (!cache.TryGetValue(accessToken, out Dictionary<string, object>? userInfo)) { userInfo = await FetchAndCacheUserInfoAsync(accessToken, cache, configurationManager, context, logger); if (userInfo == null) { context.Fail("Failed to retrieve user info."); return; } } AttachUserInfoClaims(context, userInfo!); } private static async Task<Dictionary<string, object>?> FetchAndCacheUserInfoAsync( string accessToken, IMemoryCache cache, IConfigurationManager<OpenIdConnectConfiguration> configurationManager, TokenValidatedContext context, ILogger logger) { try { var config = await configurationManager.GetConfigurationAsync(context.HttpContext.RequestAborted); string userInfoEndpoint = config.UserInfoEndpoint; if (string.IsNullOrEmpty(userInfoEndpoint)) { logger.LogError("UserInfo endpoint not found in discovery document."); return null; } using var http = new HttpClient(); http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); var response = await http.GetAsync(userInfoEndpoint); response.EnsureSuccessStatusCode(); var userInfo = await response.Content.ReadFromJsonAsync<Dictionary<string, object>>() ?? new(); CacheUserInfo(cache, accessToken, userInfo, context); logger.LogInformation("Fetched and cached userinfo for token."); return userInfo; } catch (Exception ex) { logger.LogError(ex, "Failed to fetch userinfo."); return null; } } private static void CacheUserInfo( IMemoryCache cache, string accessToken, Dictionary<string, object> userInfo, TokenValidatedContext context) { 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)); } } private static void AttachUserInfoClaims( TokenValidatedContext context, Dictionary<string, object> userInfo) { var identity = (ClaimsIdentity)context.Principal!.Identity!; foreach (var kvp in userInfo) { identity.AddClaim(new Claim(kvp.Key, kvp.Value?.ToString() ?? "")); } } private static List<Claim> GetDevClaimsForUser(string? username) { string issuedBy = "https://localhost"; // Default identity (fallback) var defaultClaims = new List<Claim> { new(WiscEduClaimTypes.FirstName, "Dev"), new(WiscEduClaimTypes.LastName, "User"), new(WiscEduClaimTypes.Name, "Dev User"), new(WiscEduClaimTypes.NetId, "jdoe"), new(WiscEduClaimTypes.Email, "it_apps@cals.wisc.edu"), new(JwtRegisteredClaimNames.Iss, issuedBy) }; // Select user-specific claims return username?.ToLowerInvariant() switch { "ewdieckman" => [ new(WiscEduClaimTypes.FirstName, "Eric"), new(WiscEduClaimTypes.LastName, "Dieckman"), new(WiscEduClaimTypes.Name, "Eric Dieckman"), new(WiscEduClaimTypes.NetId, "ewdieckman"), new(WiscEduClaimTypes.Email, "eric.dieckman@wisc.edu"), new(WiscEduClaimTypes.Pvi, "UW106Z480"), new(WiscEduClaimTypes.PrincipalName, "ewdieckman@wisc.edu"), new(WiscEduClaimTypes.Group, CreateGroupJson( [ "uw:domain:cals.wisc.edu:apps:developers", "uw:domain:cals.wisc.edu:apps:visor:admin", "uw:domain:cals.wisc.edu:apps:visor:users" ] )), new(JwtRegisteredClaimNames.Iss, issuedBy) ], "slien5" => [ new(WiscEduClaimTypes.FirstName, "Sarah"), new(WiscEduClaimTypes.LastName, "Lien"), new(WiscEduClaimTypes.Name, "Sarah Lien"), new(WiscEduClaimTypes.NetId, "slien5"), new(WiscEduClaimTypes.Email, "sarah.lien@wisc.edu"), new(WiscEduClaimTypes.Pvi, "UW121Z750"), new(WiscEduClaimTypes.PrincipalName, "slien5@wisc.edu"), new(WiscEduClaimTypes.Group, CreateGroupJson( [ "uw:domain:cals.wisc.edu:apps:visor:users" ] )), new(JwtRegisteredClaimNames.Iss, issuedBy) ], _ => defaultClaims }; } private static string CreateGroupJson(List<string> groups) { return JsonSerializer.Serialize(groups); } }
src/api/Identity/DevAuthenticationDefaults.cs 0 → 100644 +25 −0 Original line number Diff line number Diff line using Microsoft.AspNetCore.Authentication; namespace Cals.Visor.Api.Identity; /// <summary> /// Default values for Shibboleth authentication middleware /// </summary> public static class DevAuthenticationDefaults { /// <summary> /// Default value for <see cref="AuthenticationScheme.Name"/>. /// </summary> public const string AuthenticationScheme = "DevAuthentication"; /// <summary> /// Default value for <see cref="AuthenticationScheme.DisplayName"/>. /// </summary> public const string DisplayName = "DevAuthentication"; /// <summary> /// Default value for <see cref="AuthenticationSchemeOptions.ClaimsIssuer"/>. /// </summary> public const string Issuer = "DevAuthentication"; }
src/api/Identity/DevAuthenticationExtensions.cs 0 → 100644 +77 −0 Original line number Diff line number Diff line using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Options; namespace Cals.Visor.Api.Identity; public static class DevAuthenticationExtensions { /// <summary> /// Enables dev authentication using the default scheme <see cref="DevAuthenticationDefaults.AuthenticationScheme"/> /// </summary> /// <para> /// Manually configuring claims for JWT authentication /// </para> /// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param> /// <returns>A reference to <paramref name="builder"/> after the operation has completed.</returns> public static AuthenticationBuilder AddDevAuthentication(this AuthenticationBuilder builder) { return builder.AddDevAuthentication(DevAuthenticationDefaults.AuthenticationScheme, _ => { }); } /// <summary> /// Enables dev authentication using a pre-defined scheme. /// <para> /// Manually configuring claims for JWT authentication /// </para> /// </summary> /// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param> /// <param name="authenticationScheme">The authentication scheme.</param> /// <returns>A reference to <paramref name="builder"/> after the operation has completed.</returns> public static AuthenticationBuilder AddDevAuthentication(this AuthenticationBuilder builder, string authenticationScheme) => builder.AddDevAuthentication(authenticationScheme, _ => { }); /// <summary> /// Enables dev authentication using the default scheme <see cref="DevAuthenticationDefaults.AuthenticationScheme"/> /// <para> /// Manually configuring claims for JWT authentication /// </para> /// </summary> /// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param> /// <param name="configureOptions">A delegate that allows configuring <see cref="DevAuthenticationOptions"/>.</param> /// <returns>A reference to <paramref name="builder"/> after the operation has completed.</returns> public static AuthenticationBuilder AddDevAuthentication(this AuthenticationBuilder builder, Action<DevAuthenticationOptions> configureOptions) => builder.AddDevAuthentication(DevAuthenticationDefaults.AuthenticationScheme, configureOptions); /// <summary> /// Enables UW Shibboleth authentication using the specified scheme. /// <para> /// Manually configuring claims for JWT authentication /// </para> /// </summary> /// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param> /// <param name="authenticationScheme">The authentication scheme.</param> /// <param name="configureOptions">A delegate that allows configuring <see cref="DevAuthenticationOptions"/>.</param> /// <returns>A reference to <paramref name="builder"/> after the operation has completed.</returns> public static AuthenticationBuilder AddDevAuthentication(this AuthenticationBuilder builder, string authenticationScheme, Action<DevAuthenticationOptions> configureOptions) => builder.AddDevAuthentication(authenticationScheme, displayName: null, configureOptions: configureOptions); /// <summary> /// Enables UW Shibboleth authentication using the specified scheme. /// <para> /// Manually configuring claims for JWT authentication /// </para> /// </summary> /// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param> /// <param name="authenticationScheme">The authentication scheme.</param> /// <param name="displayName">The display name for the authentication handler.</param> /// <param name="configureOptions">A delegate that allows configuring <see cref="DevAuthenticationOptions"/>.</param> /// <returns>A reference to <paramref name="builder"/> after the operation has completed.</returns> public static AuthenticationBuilder AddDevAuthentication(this AuthenticationBuilder builder, string authenticationScheme, string? displayName, Action<DevAuthenticationOptions> configureOptions) { return builder.AddScheme<DevAuthenticationOptions, DevAuthenticationHandler>(authenticationScheme, displayName, configureOptions); } }
src/api/Identity/DevAuthenticationHandler.cs 0 → 100644 +38 −0 Original line number Diff line number Diff line using System.Security.Claims; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Options; namespace Cals.Visor.Api.Identity; public class DevAuthenticationHandler : AuthenticationHandler<DevAuthenticationOptions> { private readonly TimeProvider _timeProvider; public DevAuthenticationHandler( IOptionsMonitor<DevAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, TimeProvider timeProvider) : base(options, logger, encoder) => _timeProvider = timeProvider; protected override Task<AuthenticateResult> HandleAuthenticateAsync() { List<Claim> claims = Options.Claims ?? [ new Claim(ClaimTypes.Name, "default-dev-user") ]; // Example: add issued-at claim using TimeProvider DateTime issuedAt = _timeProvider.GetUtcNow().UtcDateTime; claims.Add(new Claim("iat", issuedAt.ToString("O"))); claims.Add(new Claim("exp", (issuedAt + Options.TokenExpiration).ToString("O"))); var identity = new ClaimsIdentity(claims, Scheme.Name); var principal = new ClaimsPrincipal(identity); var ticket = new AuthenticationTicket(principal, Scheme.Name); return Task.FromResult(AuthenticateResult.Success(ticket)); } }