msgraph-sdk-dotnet
msgraph-sdk-dotnet copied to clipboard
GraphServiceClient with custom HTTP Client causing Missing Authentication Provider Error
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)
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 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
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 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.
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 , 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..
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 ,
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 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.
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 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.
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 😅
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();
}
Thank you so much for the response @andrueastman, it works for me 😄
Closing older issues. Please reopen if found on version 5 or greater.