AzureAD / microsoft-identity-web

Helps creating protected web apps and web APIs with Microsoft identity platform and Azure AD B2C
MIT License
672 stars 208 forks source link

[Bug] Cannot resolve scoped service when Debug via Visual Studio on Windows - Works using Linux Container #2952

Open Johno-ACSLive opened 1 month ago

Johno-ACSLive commented 1 month ago

Microsoft.Identity.Web Library

Microsoft.Identity.Web.DownstreamApi

Microsoft.Identity.Web version

2.18.1

Web app

Sign-in users and call web APIs

Web API

Protected web APIs call downstream web APIs

Token cache serialization

In-memory caches

Description

Using Microsoft Identity Abstractions version 5.3.0 via Microsoft.Identity.Web.DownstreamApi version 2.18.1, the call to app.MapWebPubSubHub<webpubsub>("/eventhandler/{*path}"); triggers System.InvalidOperationException: 'Cannot resolve scoped service 'Microsoft.Identity.Abstractions.IDownstreamApi' from root provider.' only when debugging via Visual Studio (latest version - 17.9.6) on Windows.

When run in a Linux Container the app runs fine.

I initially logged the bug in the abstractions library https://github.com/AzureAD/microsoft-identity-abstractions-for-dotnet/issues/128

Reproduction steps

Debug an ASP.NET 8.0 application using multiple IDP's and Azure Web PubSub.

// IDP 1
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("EntraExternalID"))
                .EnableTokenAcquisitionToCallDownstreamApi()
                .AddDownstreamApi("ServiceA", builder.Configuration.GetSection("ServiceA"))
                .AddDownstreamApi("ServiceB", builder.Configuration.GetSection("ServiceB"))
                .AddInMemoryTokenCaches();

// IDP 2
builder.Services.AddAuthentication().AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("EntraID"), "EntraID");

// Add Web PubSub Service Client
builder.Services.AddWebPubSub(options =>
{
    var config = builder.Configuration.GetSection("Config").Get<Configuration<Config>>();
    options.ServiceEndpoint = new WebPubSubServiceEndpoint(config.ConnectionString);
}).AddWebPubSubServiceClient<webpubsub>();

// standard init in between e.g. var app = builder.Build();

// Further down map web pubsub event handler
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapWebPubSubHub<webpubsub>("/eventhandler/{*path}");
app.Run();

Error message

System.InvalidOperationException: 'Cannot resolve scoped service 'Microsoft.Identity.Abstractions.IDownstreamApi' from root provider.'

Id Web logs

No response

Relevant code snippets

As per reproduction steps.

Regression

No response

Expected behavior

Error is not observed when debugging on Windows via Visual Studio.

jmprieur commented 1 month ago

@Johno-ACSLive would AddWebPubSub use a singleton? if that's the case you might want to move it before AddMicrosoftIdentityWebApi()

Johno-ACSLive commented 1 month ago

@jmprieur I'm not sure, I would assume so. Even if that were the case, why is there inconsistent bahviour? I would expect the Linux container to fail to start or log the same error when the line containing app.MapWebPubSubHub<webpubsub>("/eventhandler/{*path}"); is run.

jasonshave commented 1 month ago

I have the same problem when trying to inject IDownstreamApi. No registered service...


builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("MyApi:Settings:AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddDownstreamApi("DownstreamApi", builder.Configuration.GetSection("MyApi:Settings:DownstreamApi"))
    .AddInMemoryTokenCaches();
jasonshave commented 1 month ago

I was able to resolve my issue, and have a new one, which I'll post somewhere else later. I have a singleton service which injects my typed HttpClient registered during startup. Within my typed HttpClient I was injecting the IDownstreamApi and also tried the IAuthorizationHeaderProvider which also gave me this same error. The issue is that these services are all scoped and since the typed client (transient) was coming from a singleton service, I had to first create a scope as follows:

// inject IServiceProvider in constructor of the class first
using var scope = serviceProvider.CreateScope();
var downloader = scope.ServiceProvider.GetRequiredService<IDownstreamApi>();
// use interface as the docs suggest...

@jmprieur, it might be worthwhile to consider the scenario where a multi-host environment is using this MSAL library to authenticate between services; specifically when customers create their own client library. For example, the documentation to authenticate inbound public/external clients to an API is well understood. When an API needs to connect to another API, it's very common for customers to create a client library with a named or typed HttpClient and this is where the downstream call to another API comes in. Typed http clients are transient services in .NET but these MSAL interfaces are scoped so there is definitely a case where customers could run into this issue.

Additionally, it would be helpful to have some guidance on how to handle token acquisition and renewal when fetching one manually. For example, I've noticed a typed client which has been given a token via IAuthorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync() will already have the header applied. When calling upon the typed client a second time, a check is necessary to make sure the token hasn't already been added to the authorization header.