Home > Software design >  Use other Singleton from within builder.Services.Configure()
Use other Singleton from within builder.Services.Configure()

Time:12-15

I'm building a .NET 7 MVC app that uses Azure AD for Authentication, but calls out to another API to add additional claims to the Identity.

This worked great when I defined the Claim Transformation statically, but I'd like to instead register the Claim Transformation as a singleton, so that it can manage it's own token lifetime to the API.

This is what the code looked like to add the claims when the transformation was static:

builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
builder.Services.Configure<MicrosoftIdentityOptions>(
OpenIdConnectDefaults.AuthenticationScheme, opt =>
{
opt.Events.OnTokenValidated = async context =>
{
if (context.Principal != null)

                {
                    
                    context.Principal = await ClaimsAPI.TransformAsync(context.Principal);
                }
            };
    
        });

This works, but the Claim Transformation class can't store a bearer jwt, and would need to get a fresh one every time, wasting a ton of resources.

this is the closest I've come to getting it to work as a singleton, but it causes plenty of issues

builder.Services.AddSingleton<ICLaimsAPI, ClaimsAPI>();

builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
builder.Services.Configure<MicrosoftIdentityOptions>(
        OpenIdConnectDefaults.AuthenticationScheme, opt =>
        {
            opt.Events.OnTokenValidated = async context =>
            {
                if (context.Principal != null)

                {
                    context.Principal = await builder.Services.BuildServiceProvider()
                                               .GetRequiredService<IClaimsAPI>()
                                               .TransformAsync(context.Principal);
                   
                }
            };

        });

This generates a seperate copy of each singleton, which doesn't really work for obvious reasons. How can I inject my service so that it adds the claims correctly?

EDIT: Solved!

I had to do some slight tweaks to @Acegambit's code. here is my working solution for postierity, just in case someone in the future needs to solve a similar problem.

builder.Services.AddSingleton<IClaimsAPI, ClaimsAPI>();
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
builder.Services.AddOptions().AddSingleton<IConfigureOptions<MicrosoftIdentityOptions>>(provider =>
        {
            var ClaimsAPI = provider.GetRequiredService<IClaimsAPI>();
            return new ConfigureNamedOptions<MicrosoftIdentityOptions>(OpenIdConnectDefaults.AuthenticationScheme, opt =>
                    {
                        opt.Events.OnTokenValidated = async context =>
                        {
                            if (context.Principal != null)

                            {
                                context.Principal = await ClaimsAPI.TransformAsync(context.Principal);
                            }
                        };

                    });
        });

CodePudding user response:

This took a little digging into the IServiceCollection extension methods. Looking at the implementation of Configure<TOptions> it really doesn't do a whole lot other than call .AddOptions() and register a singleton of type IConfigureOptions so I think you can pull out that code and do it yourself like so:

builder.Services.AddSingleton<IClaimsAPI, ClaimsAPI>();
builder.Services.AddOptions();
builder.Services.AddSingleton<IConfigureOptions<MicrosoftIdentityOptions>>(provider =>
{
    var claimsApi = provider.GetRequiredService<IClaimsAPI>();
    return new ConfigureNamedOptions<MicrosoftIdentityOptions>(string.Empty, options =>
    {
      // TODO: insert your logic to set the context.Principle here
      //        using the claimsApi that should resolve from the provider above
    });
});
  • Related