Skip to content

Concurrency issue with MergedOptions "No ClientId was specified" #3750

@maxackley

Description

@maxackley

Microsoft.Identity.Web Library

Microsoft.Identity.Web

Microsoft.Identity.Web version

4.5.0

Web app

Not Applicable

Web API

Not Applicable

Token cache serialization

Not Applicable

Description

The symptom of this bug is a "no client ID" error when making concurrent requests using the GraphServiceClient. The reason for this seems to be that the GraphServiceClient instances all share a singleton instance of the internal MergedOptions class using the ConcurrentDictionary in MergedOptionStore and the ConfidentialClientApplicationOptions property initialization is not thread-safe.

The MergedOptions class has a ConfidentialClientApplicationOptions property, and in the getter it will:

  • Check if the _confidentialClientApplicationOptions class variable is null
  • If true, it will:
    • Assign the _confidentialClientApplicationOptions class variable to a new instance
    • Begin updating values on the _confidentialClientApplicationOptions instance
    • Return the_confidentialClientApplicationOptions class variable

The problem seemingly would occur when:

  • Two threads access the ConfidentialClientApplicationOptions property on the singleton MergedOptions at nearly the same time
  • The first thread has seen the class variable is null, assigned a new instances to that class variable, but not completed updating values
  • The second thread accesses the property and returns the class variable before the values have been updated

The result is the second thread would return an empty ConfidentialClientApplicationOptions instance.

Reproduction steps

This is a concurrency race condition, so there's not a way to reproduce this consistently across different machines. However, it should be possible by kicking off two threads that make a call to the GraphServiceClient that will initialize MergedOptions and then adjusting the offset of their execution times.

Error message

Error:

"ErrorType": "Microsoft.Identity.Client.MsalClientException",
"ErrorMessage": "No ClientId was specified. ",
"IsNonRetriable": false,
"Properties": {
  "ErrorCode": "no_client_id"
}

Stack trace:

   at Microsoft.Identity.Client.AbstractApplicationBuilder.Validate()
   at Microsoft.Identity.Client.ConfidentialClientApplicationBuilder.Validate()
   at Microsoft.Identity.Client.AbstractApplicationBuilder.BuildConfiguration()
   at Microsoft.Identity.Client.ConfidentialClientApplicationBuilder.BuildConcrete()
   at Microsoft.Identity.Client.ConfidentialClientApplicationBuilder.Build()
   at Microsoft.Identity.Web.TokenAcquisition.BuildConfidentialClientApplicationAsync(MergedOptions mergedOptions)
   at Microsoft.Identity.Web.TokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(MergedOptions mergedOptions)
   at Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForAppAsync(String scope, String authenticationScheme, String tenant, TokenAcquisitionOptions tokenAcquisitionOptions)
   at Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.CreateAuthorizationHeaderAsync(IEnumerable scopes, AuthorizationHeaderProviderOptions downstreamApiOptions, ClaimsPrincipal claimsPrincipal, CancellationToken cancellationToken)
   at Microsoft.Identity.Web.GraphAuthenticationProvider.AuthenticateRequestAsync(RequestInformation request, Dictionary additionalAuthenticationContext, CancellationToken cancellationToken)
   at Microsoft.Kiota.Http.HttpClientLibrary.HttpClientRequestAdapter.GetHttpResponseMessageAsync(RequestInformation requestInfo, CancellationToken cancellationToken, Activity activityForAttributes, String claims, Boolean isStreamResponse)
   at Microsoft.Kiota.Http.HttpClientLibrary.HttpClientRequestAdapter.SendAsync[ModelType](RequestInformation requestInfo, ParsableFactory factory, Dictionary errorMapping, CancellationToken cancellationToken)
   at Microsoft.Graph.Drives.Item.Items.Item.DriveItemItemRequestBuilder.GetAsync(Action requestConfiguration, CancellationToken cancellationToken)
   at Cosmos.Entities.Items.CosmosGraphServiceClient.GetDriveItemAsync(String driveId, String itemId) in /src/Cosmos/Cosmos.Entities.Items/CosmosGraphServiceClient.cs:line 258
   at Cosmos.Entities.Items.DriveItemContext.GetItemByIdAsync(String id) in /src/Cosmos/Cosmos.Entities.Items/DriveItemContext.cs:line 144
   at Cosmos.Entities.Items.ItemCollectionContext.GetItemAsync(ItemRef itemRef) in /src/Cosmos/Cosmos.Entities.Items/ItemCollectionContext.cs:line 24
   at Cosmos.Data.DataSources.StaticFile.WorkbookProvider.GetWorkbook(String tenantId, String environmentId, ItemRef itemRef) in /src/Cosmos/Cosmos.Data.DataSources/StaticFile/WorkbookProvider.cs:line 77
   at Cosmos.Data.DataSources.StaticFile.StaticFileDataSource.GetTablePager(SelectTableQuery selectTableQuery, Int32 pageSize) in /src/Cosmos/Cosmos.Data.DataSources/StaticFile/StaticFileDataSource.cs:line 85
   at Cosmos.Etl.Workflows.DataSource.DataSourceWorkflow.__DisplayClass26_0.__0.MoveNext() in /src/Cosmos/Cosmos.Etl/Workflows/DataSource/DataSourceWorkflow.cs:line 299

Id Web logs

No response

Relevant code snippets

public ConfidentialClientApplicationOptions ConfidentialClientApplicationOptions
{
get
{
if (_confidentialClientApplicationOptions == null)
{
_confidentialClientApplicationOptions = new ConfidentialClientApplicationOptions();
UpdateConfidentialClientApplicationOptionsFromMergedOptions(this, _confidentialClientApplicationOptions);
}
return _confidentialClientApplicationOptions;
}
}

Regression

No response

Expected behavior

No error occurs when accessing the GraphServiceClient from multiple threads.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions