Skip to content

Commit

Permalink
Merge pull request #924 from microsoftgraph/feat/auth-handler
Browse files Browse the repository at this point in the history
Add `create()` overloads to support adding Authorization handler to HTTP clients
  • Loading branch information
Ndiritu authored Nov 8, 2024
2 parents b3e4021 + b77114d commit 8b5e890
Show file tree
Hide file tree
Showing 21 changed files with 159 additions and 41 deletions.
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,29 @@ For an example of authenticating a UWP app using the V2 Authentication Endpoint,
You can create an instance of **HttpClient** that is pre-configured for making requests to Microsoft Graph APIs using `GraphClientFactory`.

```cs
HttpClient httpClient = GraphClientFactory.Create( version: "beta");
// The client credentials flow requires that you request the
// /.default scope, and pre-configure your permissions on the
// app registration in Azure. An administrator must grant consent
// to those permissions beforehand.
var scopes = new[] { "https://graph.microsoft.com/.default" };

// Values from app registration
var clientId = "YOUR_CLIENT_ID";
var tenantId = "YOUR_TENANT_ID";
var clientSecret = "YOUR_CLIENT_SECRET";

// using Azure.Identity;
var options = new ClientSecretCredentialOptions
{
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud,
};

// https://learn.microsoft.com/dotnet/api/azure.identity.clientsecretcredential
var clientSecretCredential = new ClientSecretCredential(
tenantId, clientId, clientSecret, options);

HttpClient httpClient = GraphClientFactory.create(tokenCredential: clientSecretCredential, version: "beta");

```

For more information on initializing a client instance, see the [library overview](https://docs.microsoft.com/en-us/graph/sdks/sdks-overview)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,8 @@ namespace Microsoft.Graph
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Kiota.Http.HttpClientLibrary.Extensions;

/// <summary>
/// Contains extension methods for <see cref="HttpRequestMessage"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@ namespace Microsoft.Graph
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Kiota.Abstractions.Serialization;
using Microsoft.Kiota.Serialization.Json;

/// <summary>
/// Contains extension methods for <see cref="IDecryptableContentExtensions"/>
Expand Down Expand Up @@ -43,7 +41,7 @@ public static class IDecryptableContentExtensions

/// <summary>
/// Validates the signature and decrypted content attached with the notification.
/// https://docs.microsoft.com/en-us/graph/webhooks-with-resource-data#decrypting-resource-data-from-change-notifications
/// https://docs.microsoft.com/en-us/graph/webhooks-with-resource-data#decrypting-resource-data-from-change-notifications
/// </summary>
/// <param name="encryptedContent">The encrypted content of type <see cref="IDecryptableContent"/></param>
/// <param name="certificateProvider">Certificate provider to decrypt the content.
Expand Down
6 changes: 3 additions & 3 deletions src/Microsoft.Graph.Core/Microsoft.Graph.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@
<PackageReference Include="Microsoft.Kiota.Abstractions" Version="1.14.0" />
<PackageReference Include="Microsoft.Kiota.Authentication.Azure" Version="1.14.0" />
<PackageReference Include="Microsoft.Kiota.Serialization.Json" Version="1.14.0" />
<PackageReference Include="Microsoft.Kiota.Serialization.Text" Version="1.12.3" />
<PackageReference Include="Microsoft.Kiota.Serialization.Form" Version="1.12.3" />
<PackageReference Include="Microsoft.Kiota.Serialization.Text" Version="1.14.0" />
<PackageReference Include="Microsoft.Kiota.Serialization.Form" Version="1.14.0" />
<PackageReference Include="Microsoft.Kiota.Http.HttpClientLibrary" Version="1.14.0" />
<PackageReference Include="Microsoft.Kiota.Serialization.Multipart" Version="1.12.3" />
<PackageReference Include="Microsoft.Kiota.Serialization.Multipart" Version="1.14.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.11.20">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
Expand Down
4 changes: 1 addition & 3 deletions src/Microsoft.Graph.Core/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.CompilerServices;
#if DEBUG
[assembly: InternalsVisibleTo("Microsoft.Graph.DotnetCore.Core.Test")]
#endif
1 change: 0 additions & 1 deletion src/Microsoft.Graph.Core/Requests/AsyncMonitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ namespace Microsoft.Graph
using System;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Kiota.Abstractions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
namespace Microsoft.Graph
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Kiota.Abstractions;
using Microsoft.Kiota.Abstractions.Serialization;
Expand Down
9 changes: 4 additions & 5 deletions src/Microsoft.Graph.Core/Requests/DeltaResponseHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace Microsoft.Graph
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand All @@ -21,7 +20,7 @@ namespace Microsoft.Graph
#endif

/// <summary>
/// PREVIEW
/// PREVIEW
/// A response handler that exposes the list of changes returned in a response.
/// This supports scenarios where the service expresses changes to 'null'. The
/// deserializer can't express changes to null so you can now discover if a property
Expand Down Expand Up @@ -56,7 +55,7 @@ public async Task<ModelType> HandleResponseAsync<NativeResponseType, ModelType>(
// set on the response body object.
var responseString = await GetResponseStringAsync(responseMessage).ConfigureAwait(false);

// Get the response body object with the change list
// Get the response body object with the change list
// set on each response item.
var responseWithChangeList = await GetResponseBodyWithChangelistAsync(responseString).ConfigureAwait(false);
using var responseWithChangeListStream = new MemoryStream(Encoding.UTF8.GetBytes(responseWithChangeList));
Expand Down Expand Up @@ -112,7 +111,7 @@ private async Task<string> GetResponseBodyWithChangelistAsync(string deltaRespon
// return a string instead.
using (var responseJsonDocument = JsonDocument.Parse(deltaResponseBody))
{
// An array of delta objects. We will need to process
// An array of delta objects. We will need to process
// each one independently of each other.
if (!responseJsonDocument.RootElement.TryGetProperty("value", out var pageOfDeltaObjects))
{
Expand Down Expand Up @@ -193,7 +192,7 @@ private async Task GetObjectPropertiesAsync(JsonElement changedObject, List<stri
var arrayEnumerator = property.Value.EnumerateArray();
if (!arrayEnumerator.Any())
{
// Handle the edge case when the change involves changing to an empty array
// Handle the edge case when the change involves changing to an empty array
// as we can't observe elements in an empty collection in the foreach loop below
changes.Add(parent);
break;
Expand Down
57 changes: 56 additions & 1 deletion src/Microsoft.Graph.Core/Requests/GraphClientFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ namespace Microsoft.Graph
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using Azure.Core;
using Microsoft.Graph.Authentication;
using Microsoft.Kiota.Abstractions.Authentication;
using Microsoft.Kiota.Http.HttpClientLibrary;
using Microsoft.Kiota.Http.HttpClientLibrary.Middleware;

Expand Down Expand Up @@ -107,6 +110,58 @@ public static HttpClient Create(
return client;
}

/// <summary>
/// Creates a new <see cref="HttpClient"/> instance configured to authenticate requests using the provided <see cref="BaseBearerTokenAuthenticationProvider"/>.
/// </summary>
/// <param name="authenticationProvider">The authentication provider to initialise the Authorization handler</param>
/// <param name="handlers">Custom middleware pipeline to which the Authorization handler is appended. If null, default handlers are initialised</param>
/// <param name="version">The Graph version to use in the base URL</param>
/// <param name="nationalCloud">The national cloud endpoint to use</param>
/// <param name="proxy">The proxy to be used with the created client</param>
/// <param name="finalHandler">The last HttpMessageHandler to HTTP calls.</param>
/// <param name="disposeHandler">true if the inner handler should be disposed of by Dispose(), false if you intend to reuse the inner handler..</param>
/// <returns>An <see cref="HttpClient"/> instance with the configured handlers</returns>
public static HttpClient Create(
BaseBearerTokenAuthenticationProvider authenticationProvider,
IEnumerable<DelegatingHandler> handlers = null,
string version = "v1.0",
string nationalCloud = Global_Cloud,
IWebProxy proxy = null,
HttpMessageHandler finalHandler = null,
bool disposeHandler = true)
{
if (handlers == null)
{
handlers = CreateDefaultHandlers();
}
var handlerList = handlers.ToList();
handlerList.Add(new AuthorizationHandler(authenticationProvider));
return Create(handlerList, version, nationalCloud, proxy, finalHandler, disposeHandler);
}

/// <summary>
/// Creates a new <see cref="HttpClient"/> instance configured to authenticate requests using the provided <see cref="TokenCredential"/>.
/// </summary>
/// <param name="tokenCredential">Token credential object use to initialise an <see cref="AzureIdentityAuthenticationProvider"/></param>
/// <param name="handlers">Custom middleware pipeline to which the Authorization handler is appended. If null, default handlers are initialised</param>
/// <param name="version">The Graph version to use in the base URL</param>
/// <param name="nationalCloud">The national cloud endpoint to use</param>
/// <param name="proxy">The proxy to be used with the created client</param>
/// <param name="finalHandler">The last HttpMessageHandler to HTTP calls</param>
/// <param name="disposeHandler">true if the inner handler should be disposed of by Dispose(), false if you intend to reuse the inner handler.</param>
/// <returns>An <see cref="HttpClient"/> instance with the configured handlers</returns>
public static HttpClient Create(
TokenCredential tokenCredential,
IEnumerable<DelegatingHandler> handlers = null,
string version = "v1.0",
string nationalCloud = Global_Cloud,
IWebProxy proxy = null,
HttpMessageHandler finalHandler = null,
bool disposeHandler = true)
{
return Create(new AzureIdentityAuthenticationProvider(tokenCredential, null, null, true), handlers, version, nationalCloud, proxy, finalHandler, disposeHandler);
}

/// <summary>
/// Create a default set of middleware for calling Microsoft Graph
/// </summary>
Expand Down Expand Up @@ -211,7 +266,7 @@ internal static HttpMessageHandler GetNativePlatformHttpHandler(IWebProxy proxy
return new WinHttpHandler { Proxy = proxy, AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate , WindowsProxyUsePolicy = proxyPolicy, SendTimeout = Timeout.InfiniteTimeSpan, ReceiveDataTimeout = Timeout.InfiniteTimeSpan, ReceiveHeadersTimeout = Timeout.InfiniteTimeSpan };
#elif NET6_0_OR_GREATER
//use resilient configs when we can https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-5.0#alternatives-to-ihttpclientfactory-1
return new SocketsHttpHandler { Proxy = proxy, AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.All, PooledConnectionLifetime = TimeSpan.FromMinutes(1)};
return new SocketsHttpHandler { Proxy = proxy, AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.All, PooledConnectionLifetime = TimeSpan.FromMinutes(1)};
#else
return new HttpClientHandler { Proxy = proxy, AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate };
#endif
Expand Down
2 changes: 0 additions & 2 deletions src/Microsoft.Graph.Core/Requests/GraphRequestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@

namespace Microsoft.Graph
{
using System.Collections.Generic;
using System.Threading;
using Microsoft.Kiota.Abstractions;

/// <summary>
/// The graph request context class
Expand Down
3 changes: 1 addition & 2 deletions src/Microsoft.Graph.Core/Requests/GraphResponse{T}.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace Microsoft.Graph
{
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
Expand All @@ -27,7 +26,7 @@ public GraphResponse(RequestInformation requestInformation, HttpResponseMessage
}

/// <summary>
/// Gets the deserialized object
/// Gets the deserialized object
/// </summary>
/// <param name="responseHandler">The response handler to use for the reponse</param>
/// <param name="errorMappings">The errorMappings to use in the event of a non sucess request</param>
Expand Down
2 changes: 0 additions & 2 deletions src/Microsoft.Graph.Core/Requests/ResponseHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ namespace Microsoft.Graph
{
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Kiota.Abstractions;
using Microsoft.Kiota.Abstractions.Serialization;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@

using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Kiota.Http.HttpClientLibrary.Extensions;
using Xunit;

namespace Microsoft.Graph.DotnetCore.Core.Test.Helpers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace Microsoft.Graph.DotnetCore.Core.Test.Mocks
{
using System.Net.Http;
using Microsoft.Graph.Core.Requests;
using Microsoft.Kiota.Abstractions;
using Microsoft.Kiota.Abstractions.Authentication;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// ------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
// ------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Kiota.Abstractions.Authentication;
using Moq;

namespace Microsoft.Graph.DotnetCore.Core.Test.Mocks
{
public class MockAccessTokenProvider : Mock<IAccessTokenProvider>
{
public MockAccessTokenProvider(string accessToken = null) : base(MockBehavior.Strict)
{
this.Setup(x => x.GetAuthorizationTokenAsync(
It.IsAny<Uri>(),
It.IsAny<Dictionary<string, object>>(),
It.IsAny<CancellationToken>()
)).Returns(Task.FromResult(accessToken));

this.Setup(x => x.AllowedHostsValidator).Returns(
new AllowedHostsValidator(new List<string> { "graph.microsoft.com" })
);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

// General Information about an assembly is controlled through the following
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ namespace Microsoft.Graph.DotnetCore.Core.Test.Requests
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text.Json;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ namespace Microsoft.Graph.DotnetCore.Core.Test.Requests
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Azure.Core;
using Microsoft.Kiota.Abstractions.Authentication;
using Microsoft.Kiota.Http.HttpClientLibrary.Middleware;
using Microsoft.Kiota.Http.HttpClientLibrary.Middleware.Options;
using Mocks;
using Moq;
using Xunit;

public class GraphClientFactoryTests : IDisposable
Expand Down Expand Up @@ -315,6 +317,40 @@ public void CreateClientWithFinalHandlerDisposesTheFinalHandler(bool shouldDispo
Assert.Equal(shouldDisposeHandler, finalHandler.Disposed);
}

[Fact]
public async Task CreateClientWithAuthenticationProviderAuthenticatesRequestAsync()
{
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, "https://graph.microsoft.com/me");
var responseMessage = new HttpResponseMessage(HttpStatusCode.OK);
this.testHttpMessageHandler.SetHttpResponse(responseMessage);

var authProvider = new Mock<BaseBearerTokenAuthenticationProvider>(new MockAccessTokenProvider("token").Object);

using (HttpClient client = GraphClientFactory.Create(authenticationProvider: authProvider.Object, finalHandler: this.testHttpMessageHandler))
{
var response = await client.SendAsync(httpRequestMessage, new CancellationToken());
Assert.Equal("Bearer token", response.RequestMessage.Headers.Authorization.ToString());
}
}

[Fact]
public async Task CreateClientWithTokenCredentialAuthenticatesRequestAsync()
{
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, "https://graph.microsoft.com/me");
var responseMessage = new HttpResponseMessage(HttpStatusCode.OK);
this.testHttpMessageHandler.SetHttpResponse(responseMessage);

var tokenCredential = new Mock<TokenCredential>();
tokenCredential.Setup(x => x.GetTokenAsync(It.IsAny<TokenRequestContext>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new AccessToken("mockToken", DateTimeOffset.UtcNow.AddMinutes(10)));

using (HttpClient client = GraphClientFactory.Create(tokenCredential: tokenCredential.Object, finalHandler: this.testHttpMessageHandler))
{
var response = await client.SendAsync(httpRequestMessage, new CancellationToken());
Assert.Equal("Bearer mockToken", response.RequestMessage.Headers.Authorization.ToString());
}
}

private class MockHttpHandler : HttpMessageHandler
{
public bool Disposed;
Expand Down
Loading

0 comments on commit 8b5e890

Please sign in to comment.