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 register the Claim Transformation as a singleton instead so that it can manage its 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
});
});
CodePudding user response:
There's already an answer but I figure it would be good to show how options has evolved to make this scenario a bit more terse:
builder.Services.AddOptions<MicrosoftIdentityOptions>(OpenIdConnectDefaults.AuthenticationScheme)
.Configure<IClaimsAPI>((options, claimsApi) =>
{
options.Events = new()
{
OnTokenValidated = context =>
{
context.Principal = claimsApi.Transform(context.Principal);
return Task.CompletedTask;
}
};
});