-
Notifications
You must be signed in to change notification settings - Fork 258
unable to set a different service principal for calling downstream APIs using the .EnableTokenAcquisitionToCallDownstreamApi(Action<ConfidentialClientApplicationOptions>) extension method #3758
Description
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
- 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"
},
- Add a second identity section, using a different clientId
"serviceApiIdentity": {
"ClientId": "f92e45b6-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"ClientSecret": "FlL8..."
},
- optionally add a downstreamApi config section
"downstreamServiceApi": {
"baseUrl": "https://epservice-api-test.azurewebsites.net/api/",
"scopes": ["api://26116cee-xxxx-xxxx-xxxx-xxxxxxxxxxxx/.default"]
},
- 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();
- 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();
- 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