'How do you "inject" '/.well-known/jwks.json' into AddJwtBearer during ASPNET startup/configuration?

We are using Auth0 for Authentication using one of their boiler plate configuration setups in the Startup of ASPNET:

        string domain = $"https://{configuration["Auth0:Domain"]}/";
    AuthenticationServiceCollectionExtensions.AddAuthentication(services, (string) JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
                
            options.Authority = domain;
            options.Audience = configuration["Auth0:Audience"];
            // If the access token does not have a `sub` claim, `User.Identity.Name` will be `null`. Map it to a different claim by setting the NameClaimType below.
            options.TokenValidationParameters = new TokenValidationParameters
            {
                NameClaimType = ClaimTypes.NameIdentifier
            };
        });

We have a multi-threaded client that spins up a lot of requests at once. Because of this we sometimes get an error in Auth0 of 'Rate Limit On API - You passed the limit of allowed calls to '/.well-known/jwks.json'. The suggested correction for this is to "cache '/.well-known/jwks.json'", but they provide no example on how to do that.

I have tried a few of the tangential samples found on stackoverflow and others in order to override the configuration manager for authentication, but each ends in a different deadend/error I can't figure out.

Does anyone have a working example of how to "inject" a cached version of '/.well-known/jwks.json' into the authentication middleware?

FYI: This is all running on AWS lambda, so the ability for AWS Lambda to spin up at least 20 instances of ASPNET that hits the rate limit exception is not only plausible, but alread occurring in our system testing. I can't really/don't want to rate limit Lambda because then we'll get gateway timeouts from API Gateway.

FYI2: I have already rate limited the client to 20 executing requests in order to work around it, but would really like to solve this problem at the root cause.

One example I have used gets me a "invalid_issuer" error:

using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Http;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Amazon.Lambda.Core;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.IdentityModel.Logging;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;

namespace YadaYada.Web;

public static class JwtAuth
{
    public static IServiceCollection AddJwtAuth(this IServiceCollection services, IConfiguration configuration)
    {
        //services.AddSingleton<IConfigurationRetriever<OpenIdConnectConfiguration>, Xyz>();
        //services.AddSingleton<IConfigurationManager<OpenIdConnectConfiguration>, CachingConfigurationManager>(provider => new CachingConfigurationManager("https://tjb.auth0.com/.well-known/jwks.json", provider.GetRequiredService<IConfigurationRetriever<OpenIdConnectConfiguration>>()));

        //https://stackoverflow.com/questions/66865872/ioexception-idx20807-unable-to-retrieve-document-from-system-string-httpre

        string domain = $"https://{configuration["Auth0:Domain"]}";
        AuthenticationServiceCollectionExtensions.AddAuthentication(services, (string) JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options => 
            {
                options.Authority = domain;
                options.Audience = configuration["Auth0:Audience"];
                // If the access token does not have a `sub` claim, `User.Identity.Name` will be `null`. Map it to a different claim by setting the NameClaimType below.
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = ClaimTypes.NameIdentifier,
                    ValidIssuer = domain
                    
                };
                
                options.SetJwksOptions();
                




            });
        return services;
    }
}
public static class JwksExtension
{
    public static void SetJwksOptions(this JwtBearerOptions options)
    {
        LambdaLogger.Log(nameof(SetJwksOptions));

        var httpClient = new HttpClient(options.BackchannelHttpHandler ?? new HttpClientHandler())
        {
            Timeout = options.BackchannelTimeout,
            MaxResponseContentBufferSize = 1024 * 1024 * 10 // 10 MB 
        };

        options.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>("https://tjb.auth0.com/.well-known/jwks.json", new JwksRetrieverNew(), new HttpDocumentRetriever(httpClient) { RequireHttps = options.RequireHttpsMetadata });
    }
}

public class JwksRetrieverNew : IConfigurationRetriever<OpenIdConnectConfiguration>
{
    public Task<OpenIdConnectConfiguration> GetConfigurationAsync(string address, IDocumentRetriever retriever, CancellationToken cancel)
    {
        LambdaLogger.Log(nameof(GetConfigurationAsync));
        return GetAsync(address, retriever, cancel);
    }

    /// <summary>
    /// Retrieves a populated <see cref="OpenIdConnectConfiguration"/> given an address and an <see cref="IDocumentRetriever"/>.
    /// </summary>
    /// <param name="address">address of the jwks uri.</param>
    /// <param name="retriever">the <see cref="IDocumentRetriever"/> to use to read the jwks</param>
    /// <param name="cancel"><see cref="CancellationToken"/>.</param>
    /// <returns>A populated <see cref="OpenIdConnectConfiguration"/> instance.</returns>
    public static async Task<OpenIdConnectConfiguration> GetAsync(string address, IDocumentRetriever retriever, CancellationToken cancel)
    {
        LambdaLogger.Log(nameof(GetAsync));
        if (string.IsNullOrWhiteSpace(address))
            throw LogHelper.LogArgumentNullException(nameof(address));

        if (retriever == null)
            throw LogHelper.LogArgumentNullException(nameof(retriever));

        var doc = await retriever.GetDocumentAsync(address, cancel).ConfigureAwait(false);
        LambdaLogger.Log($"IDX21811: Deserializing the string: '{doc}' obtained from metadata endpoint into openIdConnectConfiguration object.");
        var jwks = new JsonWebKeySet(doc);
        LambdaLogger.Log(jwks.ToString());

        var openIdConnectConfiguration = new OpenIdConnectConfiguration()
        {
            JsonWebKeySet = jwks,
            JwksUri = address,
        };
        foreach (var securityKey in jwks.GetSigningKeys())
        {
            LambdaLogger.Log(securityKey.ToString());

            openIdConnectConfiguration.SigningKeys.Add(securityKey);
        }

        return openIdConnectConfiguration;
    }
}


Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source