msgraph-sdk-dotnet icon indicating copy to clipboard operation
msgraph-sdk-dotnet copied to clipboard

GraphServiceClient with custom HTTP Client causing Missing Authentication Provider Error

Open Kanac opened this issue 3 years ago • 11 comments

Describe the bug I'm using Microsoft.Graph 3.27.0 and Microsoft.Graph.Core 1.2.4.0.

When instantiating GraphServiceClient with a custom HttpClient parameter, and making any MSGraph call, it fails with the error below:

Microsoft.Graph.ServiceException: Code: invalidRequest
Message: Authentication provider is required before sending a request.

To Reproduce

Sample Code:

in Startup.cs where we inject the httpClient to the TestClient

...
services.AddHttpClient<ITestClient, TestClient>()
...

In TestClient.cs which uses the GraphServiceClient

... 
this.GraphServiceClient = new GraphServiceClient(httpClient);

Make a sample Graph call in TestClient.cs (this is similar to https://github.com/microsoftgraph/msgraph-sdk-dotnet/issues/460#issuecomment-493136860)

...
List<HeaderOption> requestHeaders = new List<HeaderOption>() { new HeaderOption("Authorization", "Bearer " + accessToken) };
var users = await this.graphServiceClient.Me.Request(requestHeaders).GetAsync();

This results in the above error.

However, if we set this line on the httpClient, then the error goes away.

httpClient.DefaultRequestHeaders.Add("FeatureFlag", "00000004");

This corresponds to the AuthHandler feature flag defined here:

https://docs.microsoft.com/en-us/dotnet/api/microsoft.graph.featureflag?view=graph-core-dotnet

Why do we need to define this feature flag in our custom httpClient in order for this error to go away or am I missing something? This isn't documented anywhere, and the GraphServiceClient constructor should handle this itself. It took a couple hours of debugging to figure this out. We need to pass a custom httpClient because we use Polly to handle retry and timeout logic on http clients created by the AddHttpClient calls.

Expected behavior Figure our why we need to pass in this feature flag to the headers in order for the MS Graph SDK to work properly with custom HTTP client instances.

@peombwa mentions code similar to this should work fine, but it doesn't work without passing this feature flag (see comment here https://github.com/microsoftgraph/msgraph-sdk-dotnet/issues/591#issuecomment-575738238)

AB#8630

Kanac avatar Mar 17 '21 03:03 Kanac

Hey @Kanac, The sdk is designed to do authentication for you. However, if you wish to implement it for yourself, you would need to provide a signal to the sdk to not do it for you. This is typically done by passing a null AuthenticationProvider to the GraphServiceClient constructor(as shown in the code example you highlighted).

In the case of using custom http clients, it would be best to use the GraphClientFactory.Create() method to create the HttpClient and still pass in a null AuthenticationProvider so that the sdk will not try to authenticate the request for you by setting the Auth Header. This method exists to enable sdk users to get a native HttpClient instance that works best with the sdk and api and you therefore will not need to set the flag yourself.

Hopefully this info will help.

andrueastman avatar Mar 17 '21 09:03 andrueastman

@andrueastman Thanks for the reply.

Unfortunately, we need to use our own custom HTTP client created by the AddHttpClient call in startup, because we want to leverage Polly for retry, logging, and time outs. See the sample code below:

services.AddHttpClient<ITestClient, TestClient>()
                .AddPolicyHandler(retryPolicy)
                .AddPolicyHandler(request => request.Method == HttpMethod.Get ? readTimeoutPolicy : writeTimeoutPolicy)
                .AddHttpMessageHandler(serviceProvider =>
                {
                    IHttpLogHandlerFactory httpLogHandlerFactory = serviceProvider.GetRequiredService<IHttpLogHandlerFactory>();
                    return httpLogHandlerFactory.CreateHttpLogHandler(name);
                });

AddPolicyHandler and AddHttpMessageHandler only works on IHttpClientBuilder, which is why we need to create the HTTP Client ourselves like this instead of using the one created by GraphClientFactory in order for these Polly policies to work.

I noticed the HTTP Client created by GraphClientFactory in the above code actually has the default feature flag set to "00000047", but the only flag we need is "00000004" to get past the authentication issue. Do you recommend we use 47 or 4 in this case? Do the extra flags in 47 provide anything that we should probably use? And is it fine to hard code the feature flag like this in my sample code if we're creating our own HTTP Client? Ideally, we'd like to apply any useful default values in the HttpClient created by GraphClientFactory.

Kanac avatar Mar 17 '21 18:03 Kanac

@Kanac

And is it fine to hard code the feature flag like this in my sample code if we're creating our own HTTP Client?

Something like this should work based on how the sdk does it.

httpClient.DefaultRequestHeaders.Add(CoreConstants.Headers.FeatureFlag, Enum.Format(typeof(FeatureFlag), FeatureFlag.AuthHandler, "x"));

Do you recommend we use 47 or 4 in this case?

Using "00000004" would sufficient for your case. You will have signaled to the SDK that it shouldn't try to Authenticate for you. The other flags do not need to be set.

andrueastman avatar Mar 18 '21 10:03 andrueastman

@andrueastman Thanks, that works.

This might be worth adding into the documentation? If you provide your own HttpClient to the GraphServiceClient, then that means you aren't passing an authenticationProvider as well, because that's how the constructor works. So anyone who passes their own HttpClient has to set this authentication feature flag if I'm understanding this correctly.

Kanac avatar Mar 19 '21 21:03 Kanac

Yes, you have it right. I agree that is it worth adding some documentation about this. Once we add this to the docs, we will then close this issue.

andrueastman avatar Mar 22 '21 06:03 andrueastman

@andrueastman , we also have the need to call graph api for multiple tenants. Caching a graph client instance for every single tenant sounds a bit sketchy... Is using the requestbuilder to do our own auth the official way for the scenario? That feels lighter but not as clean because of the need to toggle the switch using the 'flag' mentioned in this issue. Also, would all the APIs support the requestbuilder? I am not seeing any common interface for the request api across all sort of request builders..

dxynnez avatar Mar 01 '22 16:03 dxynnez

I have been using this implementation below:

I created a custom IAuthenticationProvider:

public class StoredMSTokenAuthenticationProvider : IAuthenticationProvider
{
        private string _accessToken;
      
        public StoredMSTokenAuthenticationProvider(string accessToken)
        {
            _accessToken=accessToken;
        }
        public void SetAccessToken(string accessToken)
        {
            _accessToken = accessToken;
        }

        public Task AuthenticateRequestAsync(HttpRequestMessage request)
        {
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken);
            return Task.CompletedTask;
        }
}

Then I created an extension method for GraphServiceClient:

 public static GraphServiceClient SetAccessToken(this GraphServiceClient client, string accessToken)
 {
            if (client.AuthenticationProvider is not StoredMSTokenAuthenticationProvider)
            {
                client.AuthenticationProvider = new StoredMSTokenAuthenticationProvider(accessToken);
            }

            else
            {
                ((StoredMSTokenAuthenticationProvider)client.AuthenticationProvider).SetAccessToken(accessToken);
            }

            return client;
 }

Now in my service codes, whenever I use GraphServiceClient, I always add SetAccessToken to it first, like this:

var me = await _graphClient
        .SetAccessToken(await _tokenService.GetTokenAsync(...))
        .Me.Request()
        .GetAsync();

Here the _tokenService.GetTokenAsync method returns the stored access token from our own store/cache and we handle the refresh logic ourselves.

Note that I use DI from top to bottom, so each service layer class instance (which has a GraphServiceClient injected) is created/injected for the operations related to a single user account at a time. Even though we may operate on multiple accounts/tenants concurrently, they have separate transient instances of classes/graph clients, but the graph clients are configured to use HttpClient created/managed by .NET's IHttpClientFactory for proper reuse and resilience.

I'm not sure how this pattern would work in a singleton setting where the same graph client is used concurrently in multiple threads on different users.

mikequ-taggysoft avatar Mar 02 '22 20:03 mikequ-taggysoft

@mikequ-taggysoft ,

I would recommend following the per-request options pattern.

Values are set in an extension method, for example WithScopes(), and are retrieved in the auth provider.

Storing information about the request (tenant id for the call as @dxynnez needs) rather than a token might be a bit safer. And acquiring a token in the auth provider (instead of some other code) might be a bit clearer to the next developer (or if you are like me, for yourself in a few months. 😕)

Just my thoughts...

pschaeflein avatar Mar 02 '22 22:03 pschaeflein

@pschaeflein Our app does all MS Graph interactions (delegated permissions) via background services, such as calendar syncing etc, without user involvement (offline_access). For this type of usage scenario we must save the users' refresh tokens ourselves in our database (in the most secure manner possible of course). I don't see a way how the built-in MS auth providers can help us, without us providing our own refresh token retrieval/saving logic at a very minimum. I'd imagine some users in this discussion and #460 have the same use case.

mikequ-taggysoft avatar Mar 02 '22 23:03 mikequ-taggysoft

Sorry, I didn't mean use OOB auth provider. I meant:

var me = await _graphClient
        .Me.Request()
        .WithTokenOptions(options)   // options is TenantId, or whatever you need
        .GetAsync();

Then, in your auth provider pull the options from the request and make your call to acquire the token (from cache or AAD). This puts your token acquisition call in one place instead of everywhere you make graph calls.

(And, consider setting default options in the auth provider ctor, and only use WithTokenOptions for exceptions.)

pschaeflein avatar Mar 02 '22 23:03 pschaeflein

@pschaeflein Ahh I see, you meant placing the token retrieving logic inside the custom IAuthenticatorProvider itself, which then accesses the options (user IDs etc) from the request builder. This means we'd also make the auth provider part of the DI system (because we'll now need to inject the token data store services into it). I like this idea and will play with it.

mikequ-taggysoft avatar Mar 02 '22 23:03 mikequ-taggysoft

Quick question @andrueastman, I used the Microsoft.Graph.Core version 2.0.8 to make custom queries as a generic way using something like:

var request = new HttpRequestMessage(method, url)
request.Headers.Add("Prefer", "HonorNonIndexedQueriesWarningMayFailRandomly");
.
.
.
GraphServiceClient.AuthenticationProvider.AuthenticateRequestAsync(request).Wait();
var response = GraphServiceClient.HttpProvider.SendAsync(request).Result;
if (response.IsSuccessStatusCode)
{
       return response.Content.ReadAsStringAsync().Result;
}

But the version of Microsoft.Graph.Core was updated to 3.0.2 and I had to install the Microsoft.Graph NuGet package, but now those properties of GraphServiceClient (GraphServiceClient.AuthenticationProvider and GraphServiceClient.HttpProvider) are not available anymore. Are there any way to accomplish that? Do you know any workaround? Or just I need to use an httpRequest.

Thank you in advance 😅

JulioMunozc avatar Mar 24 '23 19:03 JulioMunozc

Hey @JulioMunozc

You could probably do something like this to achieve the same result.

            // create the request
            var requestInformation = new RequestInformation()
            {
                HttpMethod = Method.GET,
                URI = new Uri("https://graph.microsoft.com/v1.0/users")
            };
            requestInformation.Headers.Add("Prefer", "HonorNonIndexedQueriesWarningMayFailRandomly");

            //send the request and setup to get back a HttpResponseMessage
            var nativeResponseHadnler = new NativeResponseHandler();
            requestInformation.SetResponseHandler(nativeResponseHadnler);
            await graphClient.RequestAdapter.SendNoContentAsync(requestInformation); // this will be authenticated with the authenticationprovider

            var response = nativeResponseHadnler.Value as HttpResponseMessage;
            if (response.IsSuccessStatusCode)
            {
                string result = await response.Content.ReadAsStringAsync();
            }

andrueastman avatar Mar 28 '23 05:03 andrueastman

Thank you so much for the response @andrueastman, it works for me 😄

JulioMunozc avatar Mar 30 '23 14:03 JulioMunozc

Closing older issues. Please reopen if found on version 5 or greater.

ddyett avatar Jun 28 '23 01:06 ddyett