microsoft-identity-web icon indicating copy to clipboard operation
microsoft-identity-web copied to clipboard

No way to use RequestOptionsExtension with Graph Bulk API (Microsoft.Identity.Web.GraphServiceClient)

Open sergey-tihon opened this issue 2 months ago • 0 comments

Microsoft.Identity.Web Library

Microsoft.Identity.Web.GraphServiceClient

Microsoft.Identity.Web version

2.19.0

Web app

Not Applicable

Web API

Protected web APIs call downstream web APIs

Token cache serialization

Not Applicable

Description

BatchRequestBuilder don't have a requestConfiguration parameter and there is no way to specify the auth scheme and app only auth for batch requests, like

 requestConfiguration.Options.WithAppOnly().WithAuthenticationScheme(JwtBearerDefaults.AuthenticationScheme);
        public async Task<BatchResponseContent> PostAsync(BatchRequestContent batchRequestContent, CancellationToken cancellationToken = default, Dictionary<string, ParsableFactory<IParsable>> errorMappings = null)
        {
            _ = batchRequestContent ?? throw new ArgumentNullException(nameof(batchRequestContent));
            var requestInfo = await ToPostRequestInformationAsync(batchRequestContent,cancellationToken);
            var nativeResponseHandler = new NativeResponseHandler();
            requestInfo.SetResponseHandler(nativeResponseHandler);
            await this.RequestAdapter.SendNoContentAsync(requestInfo, cancellationToken:cancellationToken);
            return new BatchResponseContent(nativeResponseHandler.Value as HttpResponseMessage, errorMappings);
        }

Reproduction steps

var batchRequestContent = new BatchRequestContent(graphServiceClient);
var groupsRequestId = await batchRequestContent.AddBatchRequestStepAsync(groupsRequest);
var usersRequestId = await batchRequestContent.AddBatchRequestStepAsync(usersRequest);

var response = await graphServiceClient.Batch
     .PostAsync(batchRequestContent, cancellationToken); // no way to specify options here

var groups = await response.GetResponseByIdAsync<GroupCollectionResponse>(groupsRequestId);

Error message

System.InvalidOperationException
IDW10503: Cannot determine the cloud Instance. The provided authentication scheme was ''. Microsoft.Identity.Web inferred 'JWT_OR_OIDC' as the authentication scheme. Available authentication schemes are 'JWT_OR_OIDC,Cookies,OpenIdConnect,Bearer'. See https://aka.ms/id-web/authSchemes. 
   at Microsoft.Identity.Web.TokenAcquisitionAspnetCoreHost.GetOptions(String authenticationScheme, String& effectiveAuthenticationScheme)
   at Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForUserAsync(IEnumerable`1 scopes, String authenticationScheme, String tenantId, String userFlow, ClaimsPrincipal user, TokenAcquisitionOptions tokenAcquisitionOptions)

Id Web logs

No response

Relevant code snippets

The workaround is to implement custom PostAsync that add GraphAuthenticationOptions before the call, but this approach does not work with BatchRequestContentCollection (ctor is internal), work only with BatchRequestContent and require reverse engineering of 2 libs behavior

public static class GraphBatchApiExtensions
{
    public static async Task<BatchResponseContent> SendBatchAsAppAsync(
        this GraphServiceClient graphServiceClient,
        BatchRequestContent batchRequestContent,
        CancellationToken cancellationToken = default, 
        Dictionary<string, ParsableFactory<IParsable>>? errorMappings = null)
    {
        ArgumentNullException.ThrowIfNull(graphServiceClient);
        ArgumentNullException.ThrowIfNull(batchRequestContent);
        
        var requestInfo = await graphServiceClient.Batch.ToPostRequestInformationAsync(batchRequestContent,cancellationToken); 
        WithAppOnly(requestInfo);
        
        var nativeResponseHandler = new NativeResponseHandler();
        requestInfo.SetResponseHandler(nativeResponseHandler);
        
        await graphServiceClient.RequestAdapter.SendNoContentAsync(requestInfo, cancellationToken:cancellationToken);
        return new BatchResponseContent(nativeResponseHandler.Value as HttpResponseMessage, errorMappings);
    }

    private static void WithAppOnly(RequestInformation requestInformation)
    {
        var authOptions = requestInformation.RequestOptions.OfType<GraphAuthenticationOptions>().FirstOrDefault();
        if (authOptions is null)
        {
            authOptions = new GraphAuthenticationOptions();
            requestInformation.AddRequestOptions([authOptions]);
        }
        
        authOptions.RequestAppToken = true;
        authOptions.AcquireTokenOptions ??= new ();
        authOptions.AcquireTokenOptions.AuthenticationOptionsName =
            JwtBearerDefaults.AuthenticationScheme;
    }
}

Regression

No response

Expected behavior

API unification

var batchRequestContent = new BatchRequestContent(graphServiceClient);
var groupsRequestId = await batchRequestContent.AddBatchRequestStepAsync(groupsRequest);
var usersRequestId = await batchRequestContent.AddBatchRequestStepAsync(usersRequest);

var response = await graphServiceClient.Batch
     .PostAsync(batchRequestContent, rc => {
           rc.Options.WithAppOnly().WithAuthenticationScheme(JwtBearerDefaults.AuthenticationScheme);
      }, cancellationToken); // new overload with requestConfiguration action

var groups = await response.GetResponseByIdAsync<GroupCollectionResponse>(groupsRequestId);

sergey-tihon avatar Jun 07 '24 10:06 sergey-tihon