Commit 1995e802 authored by Eric Dieckman's avatar Eric Dieckman
Browse files

refactor jwtbearer to include more automation, as well as registering user in database

parent 5ef0fed8
Loading
Loading
Loading
Loading
+40 −65
Original line number Diff line number Diff line
@@ -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;
@@ -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(
@@ -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)
+3 −1
Original line number Diff line number Diff line
@@ -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;
@@ -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 =>
+13 −3
Original line number Diff line number Diff line
@@ -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",
+1 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@

	<ItemGroup>
		<ProjectReference Include="..\models\Models.csproj" />
		<ProjectReference Include="..\nexus\Cals.Nexus.csproj" />
	</ItemGroup>

</Project>