AzureAD / microsoft-identity-web

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

Token acquisition in a AspNetCore Azure Function #2963

Open scrocquesel-ml150 opened 1 month ago

scrocquesel-ml150 commented 1 month ago

Microsoft.Identity.Web Library

Microsoft.Identity.Web

Microsoft.Identity.Web version

3.0.1

Web app

Sign-in users

Web API

Protected web APIs call downstream web APIs

Token cache serialization

In-memory caches

Description

In a Azure function with ASPNet support (ConfigureFunctionsWebApplication) but without authentication, requesting a token fails with error IDW10503: Cannot determine the cloud Instance. The provided authentication scheme was ''. Microsoft.Identity.Web inferred 'OpenIdConnect' as the authentication scheme. Available authentication schemes are ''. See https://aka.ms/id-web/authSchemes.

Reproduction steps

  1. Create an azure function with http trigger and configure a downstream api with managed identity authentication option

    var host = new HostBuilder()
        .ConfigureFunctionsWebApplication()
        .ConfigureServices((hostBuilderContext, services) =>
        {
            services
                .AddTokenAcquisition(true)
                .AddInMemoryTokenCaches()
                .AddHttpClient();
    
            services.AddDownstreamApi("test", configure => {
                configure.BaseUrl = "https://myapi.com";
                configure.RequestAppToken = true;
                configure.AcquireTokenOptions = new AcquireTokenOptions
                {
                    ManagedIdentity = new ManagedIdentityOptions {
                        UserAssignedClientId = null
                    }   
                };
                configure.Scopes = ["api://<guid>/.default"];
            })
        })
        .Build();
    
    host.Run();

Error message

System.InvalidOperationException:
   at Microsoft.Identity.Web.TokenAcquisitionAspnetCoreHost.GetOptions (Microsoft.Identity.Web.TokenAcquisition, Version=3.0.1.0, Culture=neutral, PublicKeyToken=0a613f4dd989e8ae)
   at Microsoft.Identity.Web.TokenAcquisition+<GetAuthenticationResultForAppAsync>d__17.MoveNext (Microsoft.Identity.Web.TokenAcquisition, Version=3.0.1.0, Culture=neutral, PublicKeyToken=0a613f4dd989e8ae)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1+ConfiguredTaskAwaiter.GetResult (System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider+<CreateAuthorizationHeaderAsync>d__4.MoveNext (Microsoft.Identity.Web.TokenAcquisition, Version=3.0.1.0, Culture=neutral, PublicKeyToken=0a613f4dd989e8ae)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1+ConfiguredTaskAwaiter.GetResult (System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at Microsoft.Identity.Web.DownstreamApi+<UpdateRequestAsync>d__19.MoveNext (Microsoft.Identity.Web.DownstreamApi, Version=3.0.1.0, Culture=neutral, PublicKeyToken=0a613f4dd989e8ae)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at Microsoft.Identity.Web.DownstreamApi+<CallApiInternalAsync>d__18.MoveNext (Microsoft.Identity.Web.DownstreamApi, Version=3.0.1.0, Culture=neutral, PublicKeyToken=0a613f4dd989e8ae)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult (System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at Identity.Triggers.TestCall+<Run>d__4.MoveNext (identity, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null: /home/scrocquesel/source/lowcode/identity/identity/Triggers/TestCall.cs:39)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult (System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at identity.DirectFunctionExecutor+<ExecuteAsync>d__3.MoveNext (identity, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null: /home/scrocquesel/source/lowcode/identity/identity/obj/Release/net8.0/Microsoft.Azure.Functions.Worker.Sdk.Generators/Microsoft.Azure.Functions.Worker.Sdk.Generators.FunctionExecutorGenerator/GeneratedFunctionExecutor.g.cs:38)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at Microsoft.Azure.Functions.Worker.OutputBindings.OutputBindingsMiddleware+<Invoke>d__0.MoveNext (Microsoft.Azure.Functions.Worker.Core, Version=1.18.0.0, Culture=neutral, PublicKeyToken=551316b6919f366c: D:\a\_work\1\s\src\DotNetWorker.Core\OutputBindings\OutputBindingsMiddleware.cs:13)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore.FunctionsHttpProxyingMiddleware+<Invoke>d__5.MoveNext (Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore, Version=1.3.2.0, Culture=neutral, PublicKeyToken=551316b6919f366c: /mnt/vss/_work/1/s/extensions/Worker.Extensions.Http.AspNetCore/src/FunctionsMiddleware/FunctionsHttpProxyingMiddleware.cs:54)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
   at Microsoft.Azure.Functions.Worker.FunctionsApplication+<InvokeFunctionAsync>d__10.MoveNext (Microsoft.Azure.Functions.Worker.Core, Version=1.18.0.0, Culture=neutral, PublicKeyToken=551316b6919f366c: D:\a\_work\1\s\src\DotNetWorker.Core\FunctionsApplication.cs:89)

Id Web logs

No response

Relevant code snippets

var response = await _downstreamApi.CallApiForAppAsync("test", options => options.RelativePath = "api/v1/test");

Regression

No response

Expected behavior

I should be able to obtain a token.

The error occurs because AddTokenAcquisition internally detects a registered service of type Microsoft.AspNetCore.Authentication.IAuthenticationService, which is registered by ConfigureFunctionsWebApplication.

Is there an authentication configuration to be added even if it is not used to authenticate the incoming request? I guess this could be also used to describe a confidential client configuration for local development (with certificate or secret).

scrocquesel-ml150 commented 1 month ago

Adding a fictitous configuration works.

 services.AddAuthentication()
    .AddMicrosoftIdentityWebApi(hostBuilderContext.Configuration,  jwtBearerScheme: "ForTokenForAppAcquisitionOnly")
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddDownstreamApi("test", configure => {
        // [...]
        configure.AcquireTokenOptions = new AcquireTokenOptions
        {
            // [...]
            AuthenticationOptionsName = "ForTokenForAppAcquisitionOnly"
        }

The issue is that despite I don't need a confidential client configuration (and I don't have one either in Entra), I must set a value for required options.

 "AzureAd": {
        "Instance": "https://invalid.but.not.used",
        "TenantId": "invalid but not used",
        "ClientId": "invalid but not used"
    },
scrocquesel-ml150 commented 1 month ago

https://github.com/AzureAD/microsoft-identity-web/blob/ed97b54d8f4738c9bebe699491a43b5d586cae5a/src/Microsoft.Identity.Web.TokenAcquisition/ServiceCollectionExtensions.cs#L45

I think that if the forceSdk is exposed on AddTokenAcquisition, I would be able to configure the app with:

services
    .Configure<MicrosoftIdentityApplicationOptions>(hostBuilderContext.Configuration.GetSection("AzureAd"))
    .AddTokenAcquisition(isTokenAcquisitionSingleton: true, disableAspNetCoreAuthenticationServiceSupport: true)
    .AddInMemoryTokenCaches()
    .AddHttpClient();

I can then configure the Azure Function like a non-protected API, with either a managed identity on AcquireTokenOptions or an AzureAd confidential client.

JoshLozensky commented 3 weeks ago

@scrocquesel-ml150 could you try adding options.AcquireTokenOptions.AuthenticationOptionsName = ""

to your call var response = await _downstreamApi.CallApiForAppAsync("test", options => options.RelativePath = "api/v1/test");

scrocquesel-ml150 commented 2 weeks ago

@scrocquesel-ml150 could you try adding options.AcquireTokenOptions.AuthenticationOptionsName = ""

to your call var response = await _downstreamApi.CallApiForAppAsync("test", options => options.RelativePath = "api/v1/test");

This doesn't work,

Exception while executing function: Functions.TestNew Result: Failure
Exception: System.InvalidOperationException: IDW10503: Cannot determine the cloud Instance. The provided authentication scheme was ''. Microsoft.Identity.Web inferred 'OpenIdConnect' as the authentication scheme. Available authentication schemes are ''. See https://aka.ms/id-web/authSchemes. 
   at Microsoft.Identity.Web.TokenAcquisitionAspnetCoreHost.GetOptions(String authenticationScheme, String& effectiveAuthenticationScheme)
   at Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForAppAsync(String scope, String authenticationScheme, String tenant, TokenAcquisitionOptions tokenAcquisitionOptions)
   at Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.CreateAuthorizationHeaderAsync(IEnumerable`1 scopes, AuthorizationHeaderProviderOptions downstreamApiOptions, ClaimsPrincipal claimsPrincipal, CancellationToken cancellationToken)
   at Microsoft.Identity.Web.DownstreamApi.UpdateRequestAsync(HttpRequestMessage httpRequestMessage, HttpContent content, DownstreamApiOptions effectiveOptions, Boolean appToken, ClaimsPrincipal user, CancellationToken cancellationToken)
   at Microsoft.Identity.Web.DownstreamApi.CallApiInternalAsync(String serviceName, DownstreamApiOptions effectiveOptions, Boolean appToken, HttpContent content, ClaimsPrincipal user, CancellationToken cancellationToken)

Even adding a default (un-named option) MicrosoftIdentityApplicationOptions.

What I did is to add a named MicrosoftIdentityApplicationOptions and use its name as the AuthenticationOptionsName. I still have to provide a value for MicrosoftIdentityApplicationOptions.Instance despite I'm requesting an AppToken with managed identity but I can live with that. Thought, I'm in between protected app and daemon app for configuration and wiki doesn't address this edge case.