Skip to content

unable to set a different service principal for calling downstream APIs using the .EnableTokenAcquisitionToCallDownstreamApi(Action<ConfidentialClientApplicationOptions>) extension method #3758

@rplaetinck

Description

@rplaetinck

Microsoft.Identity.Web Library

Microsoft.Identity.Web

Microsoft.Identity.Web version

4.6.0

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

Project type: React SPA with .net Core (.net 10)
Microsoft.Web.Identity version 4.6.0

In the controller project, I'm trying to allow token acquisition for a downstream API using the EnableTokenAcquisitionToCallDownstreamApi(Action) overloaded extension method, using a different service principal (client id) as the one defined in the "azureAd" config section

The ClientId defined in the alternate configuration section is discarded during the actual token acquisition and the original client id configured in the "AzureAd" config section is used.

Stepping through the code, it seems as if in some of the MergeOptions calls, some of the PostConfiguration loops are effectively overwriting the config data from the alternate configuration section.

appsettings.json

"AzureAd": {
  "Instance": "https://login.microsoftonline.com/",
  "TenantId": "5cae08cc-xxxx-xxxx-xxxx-xxxxxxxxxxxx", 
  "ClientId": "dc5cf146-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
},
"serviceApiIdentity": {
  "ClientId": "f92e45b6-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "ClientSecret": "FlL8..."
},
"downstreamServiceApi": {
  "baseUrl": "https://epservice-api-test.azurewebsites.net/api/",
  "scopes": ["api://26116cee-xxxx-xxxx-xxxx-xxxxxxxxxxxx/.default"]
},

program.cs

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi(o =>
    {
        builder.Configuration.Bind("serviceApiIdentity", o);
    })
    .AddDownstreamApi("ServiceApi", builder.Configuration.GetSection("downstreamServiceApi"))
    .AddInMemoryTokenCaches();

additional service registered on the service collection for calling the downstream API

builder.Services.AddTransient<ServiceApiBridge>();
scope for downstream api:

"api://26116cee-xxxx-xxxx-xxxx-xxxxxxxxxxxx/.default"

service calling the downstream API

public class ServiceApiBridge(ILogger<ServiceApiBridge> logger, IDownstreamApi serviceApi, ITokenAcquisition tokenAcquisition)
{
    public async Task<JsonObject> GetWorkOrderData(string workorderNumber)
    {
        try
        {
	    // simply trying to retrieve an access token
            var t = tokenAcquisition.GetAccessTokenForAppAsync(Environment.GetEnvironmentVariable("service_api_scope")??"").GetAwaiter().GetResult();

	    // alternatively, trying to use the downstreamApi extension
	    var response = serviceApi.CallApiForAppAsync("ServiceApi", options =>
            {
                options.RelativePath = $"workorder/loadworkorderstatus/{workorderNumber}";
                options.ExtraHeaderParameters = new Dictionary<string, string>
                {
                      { "API-SUBSCRIPTION-KEY", "XYZ..." }
                };
                options.RequestAppToken = true;
             }).GetAwaiter().GetResult();

             return new JsonObject();
	}
	catch (Exception ex)
        {
    		logger.LogError(ex, "Error fetching work order data for {WorkOrderNumber}", workorderNumber);
    		throw;
	}
    }
}

both tokenAcquisition.GetAccessTokenForAppAsync and serviceApi.CallApiForAppAsync return the same error, indicating an Authentication configuration issue

Reproduction steps

  1. setup the appsettings.json with an AzureAd section, specifying Instance, TenantId and ClientId```
"AzureAd": {
  "Instance": "https://login.microsoftonline.com/",
  "TenantId": "5cae08cc-xxxx-xxxx-xxxx-xxxxxxxxxxxx", 
  "ClientId": "dc5cf146-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
},
  1. Add a second identity section, using a different clientId
"serviceApiIdentity": {
  "ClientId": "f92e45b6-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "ClientSecret": "FlL8..."
},
  1. optionally add a downstreamApi config section
"downstreamServiceApi": {
  "baseUrl": "https://epservice-api-test.azurewebsites.net/api/",
  "scopes": ["api://26116cee-xxxx-xxxx-xxxx-xxxxxxxxxxxx/.default"]
},
  1. Add Authentication services to the service collection, including the EnableTokenAcquisitiontoCallDownstreamApi(Action) extension method
builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi(o =>
    {
        builder.Configuration.Bind("serviceApiIdentity", o);
    })
    .AddDownstreamApi("ServiceApi", builder.Configuration.GetSection("downstreamServiceApi"))
    .AddInMemoryTokenCaches();
  1. in a controller or additional service, add code to obtain a token for, or call the downstream API
// simply trying to retrieve an access token
var t = tokenAcquisition.GetAccessTokenForAppAsync(Environment.GetEnvironmentVariable("service_api_scope")??"").GetAwaiter().GetResult();

	    // alternatively, trying to use the downstreamApi extension
	    var response = serviceApi.CallApiForAppAsync("ServiceApi", options =>
            {
                options.RelativePath = $"workorder/loadworkorderstatus/{workorderNumber}";
                options.ExtraHeaderParameters = new Dictionary<string, string>
                {
                      { "API-SUBSCRIPTION-KEY", "XYZ..." }
                };
                options.RequestAppToken = true;
             }).GetAwaiter().GetResult();


  1. trying to get the token or calling the downstream api should result in the error

Error message

MSAL.NetCore.4.83.1.0.MsalServiceException:
ErrorCode: invalid_client
Microsoft.Identity.Client.MsalServiceException: A configuration issue is preventing authentication - check the error message from the server for details. You can modify the configuration in the application registration portal. See https://aka.ms/msal-net-invalid-client for details. Original exception: AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'dc5cf146-7c12-445c-968c-26b0666a2a34'. Trace ID: def29c15-c270-46cd-9dd5-f00552360e00 Correlation ID: 20ff07f8-64d8-453f-b14b-741c7b7bd852 Timestamp: 2026-03-25 18:42:00Z
at Microsoft.Identity.Client.OAuth2.OAuth2Client.ThrowServerException(HttpResponse response, RequestContext requestContext)
at Microsoft.Identity.Client.OAuth2.OAuth2Client.CreateResponse[T](HttpResponse response, RequestContext requestContext)
at Microsoft.Identity.Client.OAuth2.OAuth2Client.ExecuteRequestAsync[T](Uri endPoint, HttpMethod method, RequestContext requestContext, Boolean expectErrorsOn200OK, Boolean addCommonHeaders, IList1 onBeforePostRequestHandlers) at Microsoft.Identity.Client.OAuth2.TokenClient.SendHttpAndClearTelemetryAsync(String tokenEndpoint, ILoggerAdapter logger) at Microsoft.Identity.Client.OAuth2.TokenClient.SendHttpAndClearTelemetryAsync(String tokenEndpoint, ILoggerAdapter logger) at Microsoft.Identity.Client.OAuth2.TokenClient.SendTokenRequestAsync(IDictionary2 additionalBodyParameters, String scopeOverride, String tokenEndpointOverride, CancellationToken cancellationToken)
at Microsoft.Identity.Client.Internal.Requests.RequestBase.SendTokenRequestAsync(IDictionary2 additionalBodyParameters, CancellationToken cancellationToken) at Microsoft.Identity.Client.Internal.Requests.ClientCredentialRequest.GetAccessTokenAsync(CancellationToken cancellationToken, ILoggerAdapter logger) at Microsoft.Identity.Client.Internal.Requests.ClientCredentialRequest.GetAccessTokenAsync(CancellationToken cancellationToken, ILoggerAdapter logger) at Microsoft.Identity.Client.Internal.Requests.ClientCredentialRequest.ExecuteAsync(CancellationToken cancellationToken) at Microsoft.Identity.Client.Internal.Requests.RequestBase.<>c__DisplayClass11_1.<<RunAsync>b__1>d.MoveNext() --- End of stack trace from previous location --- at Microsoft.Identity.Client.Utils.StopwatchService.MeasureCodeBlockAsync(Func1 codeBlock)
at Microsoft.Identity.Client.Internal.Requests.RequestBase.RunAsync(CancellationToken cancellationToken)
at Microsoft.Identity.Client.ApiConfig.Executors.ConfidentialClientExecutor.ExecuteAsync(AcquireTokenCommonParameters commonParameters, AcquireTokenForClientParameters clientParameters, CancellationToken cancellationToken)
at Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForAppInternalAsync(String scope, String authenticationScheme, String tenant, TokenAcquisitionOptions tokenAcquisitionOptions, Int32 retryCount)
at Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForAppAsync(String scope, String authenticationScheme, String tenant, TokenAcquisitionOptions tokenAcquisitionOptions)
at Microsoft.Identity.Web.TokenAcquisition.GetAccessTokenForAppAsync(String scope, String authenticationScheme, String tenant, TokenAcquisitionOptions tokenAcquisitionOptions)

Id Web logs

No response

Relevant code snippets

appsettings.json
========================
"AzureAd": {
  "Instance": "https://login.microsoftonline.com/",
  "TenantId": "5cae08cc-xxxx-xxxx-xxxx-xxxxxxxxxxxx", 
  "ClientId": "dc5cf146-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
},
"serviceApiIdentity": {
  "ClientId": "f92e45b6-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "ClientSecret": "FlL8..."
},
"downstreamServiceApi": {
  "baseUrl": "https://epservice-api-test.azurewebsites.net/api/",
  "scopes": ["api://26116cee-xxxx-xxxx-xxxx-xxxxxxxxxxxx/.default"]
},


scope for downstream api:
==========================
"api://26116cee-xxxx-xxxx-xxxx-xxxxxxxxxxxx/.default"


program.cs
==========================
builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi(o =>
    {
        builder.Configuration.Bind("serviceApiIdentity", o);
    })
    .AddDownstreamApi("ServiceApi", builder.Configuration.GetSection("downstreamServiceApi"))
    .AddInMemoryTokenCaches();

additional service registered on the service collection for calling the downstream API
======================================
builder.Services.AddTransient<ServiceApiBridge>();


service calling the downstream API
======================================

public class ServiceApiBridge(ILogger<ServiceApiBridge> logger, ITokenAcquisition tokenAcquisition)
{
    public async Task<JsonObject> GetWorkOrderData(string workorderNumber)
    {
        try
        {
	    // simply trying to retrieve an access token
            var t = tokenAcquisition.GetAccessTokenForAppAsync(Environment.GetEnvironmentVariable("service_api_scope")??"").GetAwaiter().GetResult();

	    // alternatively, trying to use the downstreamApi extension
	    var response = serviceApi.CallApiForAppAsync("ServiceApi", options =>
            {
                options.RelativePath = $"workorder/loadworkorderstatus/{workorderNumber}";
                options.ExtraHeaderParameters = new Dictionary<string, string>
                {
                      { "API-SUBSCRIPTION-KEY", "XYZ..." }
                };
                options.RequestAppToken = true;
             }).GetAwaiter().GetResult();

             return new JsonObject();
	}
	catch (Exception ex)
        {
    		logger.LogError(ex, "Error fetching work order data for {WorkOrderNumber}", workorderNumber);
    		throw;
	}
    }
}

Regression

No response

Expected behavior

Using the EnableTokenAcquisitionToCallDownstreamApi(Action) extension method, it should be possible to call downstream APIs with a different service principal

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions