Skip to content

Commit 0d34c89

Browse files
authored
Merge pull request #320 from microsoftgraph/AuthenticationHandler
Authentication handler
2 parents d7607bc + 1c1463d commit 0d34c89

File tree

11 files changed

+828
-63
lines changed

11 files changed

+828
-63
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// ------------------------------------------------------------------------------
2+
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
3+
// ------------------------------------------------------------------------------
4+
5+
namespace Microsoft.Graph
6+
{
7+
using System.Net.Http;
8+
/// <summary>
9+
/// Contains extension methods for <see cref="HttpRequestMessage"/>
10+
/// </summary>
11+
internal static class RequestExtensions
12+
{
13+
/// <summary>
14+
/// Checks the HTTP request's content to determine if it's buffered or streamed content.
15+
/// </summary>
16+
/// <param name="httpRequestMessage">The <see cref="HttpRequestMessage"/>needs to be sent.</param>
17+
/// <returns></returns>
18+
internal static bool IsBuffered(this HttpRequestMessage httpRequestMessage)
19+
{
20+
HttpContent requestContent = httpRequestMessage.Content;
21+
22+
if ((httpRequestMessage.Method == HttpMethod.Put || httpRequestMessage.Method == HttpMethod.Post || httpRequestMessage.Method.Method.Equals("PATCH"))
23+
&& requestContent != null && (requestContent.Headers.ContentLength == null || (int)requestContent.Headers.ContentLength == -1))
24+
{
25+
return false;
26+
}
27+
return true;
28+
}
29+
}
30+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// ------------------------------------------------------------------------------
2+
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
3+
// ------------------------------------------------------------------------------
4+
5+
namespace Microsoft.Graph
6+
{
7+
using System.Net.Http;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using System.Net;
11+
/// <summary>
12+
/// A <see cref="DelegatingHandler"/> implementation using standard .NET libraries.
13+
/// </summary>
14+
public class AuthenticationHandler: DelegatingHandler
15+
{
16+
/// <summary>
17+
/// MaxRetry property for 401's
18+
/// </summary>
19+
public int MaxRetry { get; set; } = 1;
20+
21+
/// <summary>
22+
/// AuthenticationProvider property
23+
/// </summary>
24+
public IAuthenticationProvider AuthenticationProvider { get; set; }
25+
26+
/// <summary>
27+
/// Construct a new <see cref="AuthenticationHandler"/>
28+
/// </summary>
29+
public AuthenticationHandler()
30+
{
31+
32+
}
33+
34+
/// <summary>
35+
/// Construct a new <see cref="AuthenticationHandler"/>
36+
/// <param name="authenticationProvider">An authentication provider to pass to <see cref="AuthenticationHandler"/> for authenticating requests.</param>
37+
/// </summary>
38+
public AuthenticationHandler(IAuthenticationProvider authenticationProvider)
39+
{
40+
AuthenticationProvider = authenticationProvider;
41+
}
42+
43+
/// <summary>
44+
/// Construct a new <see cref="AuthenticationHandler"/>
45+
/// </summary>
46+
/// <param name="authenticationProvider">An authentication provider to pass to <see cref="AuthenticationHandler"/> for authenticating requests.</param>
47+
/// <param name="innerHandler">A HTTP message handler to pass to the <see cref="AuthenticationHandler"/> for sending requests.</param>
48+
public AuthenticationHandler(IAuthenticationProvider authenticationProvider, HttpMessageHandler innerHandler)
49+
{
50+
InnerHandler = innerHandler;
51+
AuthenticationProvider = authenticationProvider;
52+
}
53+
54+
/// <summary>
55+
/// Checks HTTP response message status code if it's unauthorized (401) or not
56+
/// </summary>
57+
/// <param name="httpResponseMessage">The <see cref="HttpResponseMessage"/>to send.</param>
58+
/// <returns></returns>
59+
private bool IsUnauthorized(HttpResponseMessage httpResponseMessage)
60+
{
61+
return httpResponseMessage.StatusCode == HttpStatusCode.Unauthorized;
62+
}
63+
64+
/// <summary>
65+
/// Retry sending HTTP request
66+
/// </summary>
67+
/// <param name="httpResponseMessage">The <see cref="HttpResponseMessage"/>to send.</param>
68+
/// <param name="cancellationToken">The <see cref="CancellationToken"/>to send.</param>
69+
/// <returns></returns>
70+
private async Task<HttpResponseMessage> SendRetryAsync(HttpResponseMessage httpResponseMessage, CancellationToken cancellationToken)
71+
{
72+
int retryAttempt = 0;
73+
while (retryAttempt < MaxRetry)
74+
{
75+
var originalRequest = httpResponseMessage.RequestMessage;
76+
77+
// Authenticate request using AuthenticationProvider
78+
await AuthenticationProvider.AuthenticateRequestAsync(originalRequest);
79+
httpResponseMessage = await base.SendAsync(originalRequest, cancellationToken);
80+
81+
retryAttempt++;
82+
83+
if (!IsUnauthorized(httpResponseMessage) || !originalRequest.IsBuffered())
84+
{
85+
// Re-issue the request to get a new access token
86+
return httpResponseMessage;
87+
}
88+
}
89+
90+
return httpResponseMessage;
91+
}
92+
93+
/// <summary>
94+
/// Sends a HTTP request and retries the request when the response is unauthorized.
95+
/// This can happen when a token from the cache expires between graph getting the request and the backend receiving the request
96+
/// </summary>
97+
/// <param name="httpRequestMessage">The <see cref="HttpRequestMessage"/> to send.</param>
98+
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the request.</param>
99+
/// <returns></returns>
100+
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage httpRequestMessage, CancellationToken cancellationToken)
101+
{
102+
// Authenticate request using AuthenticationProvider
103+
if (AuthenticationProvider != null)
104+
{
105+
await AuthenticationProvider.AuthenticateRequestAsync(httpRequestMessage);
106+
}
107+
108+
HttpResponseMessage response = await base.SendAsync(httpRequestMessage, cancellationToken);
109+
110+
// Chcek if response is a 401 & is not a streamed body (is buffered)
111+
if (IsUnauthorized(response) && httpRequestMessage.IsBuffered() && (AuthenticationProvider != null))
112+
{
113+
// re-issue the request to get a new access token
114+
response = await SendRetryAsync(response, cancellationToken);
115+
}
116+
117+
return response;
118+
}
119+
}
120+
}

src/Microsoft.Graph.Core/Requests/RetryHandler.cs

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ namespace Microsoft.Graph
1111
using System.Net.Http;
1212
using System.Net;
1313
using System.Net.Http.Headers;
14+
1415
/// <summary>
1516
/// An <see cref="DelegatingHandler"/> implementation using standard .NET libraries.
1617
/// </summary>
@@ -54,7 +55,7 @@ protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage
5455

5556
var response = await base.SendAsync(httpRequest, cancellationToken);
5657

57-
if (IsRetry(response) && IsBuffered(httpRequest))
58+
if (IsRetry(response) && httpRequest.IsBuffered())
5859
{
5960
response = await SendRetryAsync(response, cancellationToken);
6061
}
@@ -94,7 +95,7 @@ public async Task<HttpResponseMessage> SendRetryAsync(HttpResponseMessage respon
9495
// Call base.SendAsync to send the request
9596
response = await base.SendAsync(request, cancellationToken);
9697

97-
if (!IsRetry(response) || !IsBuffered(request))
98+
if (!IsRetry(response) || !request.IsBuffered())
9899
{
99100
return response;
100101
}
@@ -126,23 +127,6 @@ public bool IsRetry(HttpResponseMessage response)
126127
return false;
127128
}
128129

129-
/// <summary>
130-
/// Check the HTTP request's content to determine whether it can be retried or not.
131-
/// </summary>
132-
/// <param name="request">The <see cref="HttpRequestMessage"/>needs to be sent.</param>
133-
/// <returns></returns>
134-
private bool IsBuffered(HttpRequestMessage request)
135-
{
136-
HttpContent content = request.Content;
137-
138-
if ((request.Method == HttpMethod.Put || request.Method == HttpMethod.Post || request.Method.Method.Equals("PATCH"))
139-
&& content != null && (content.Headers.ContentLength == null || (int)content.Headers.ContentLength == -1))
140-
{
141-
return false;
142-
}
143-
return true;
144-
145-
}
146130

147131
/// <summary>
148132
/// Update Retry-Attempt header in the HTTP request
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// ------------------------------------------------------------------------------
2+
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
3+
// ------------------------------------------------------------------------------
4+
5+
namespace Microsoft.Graph.Core.Test.Extensions
6+
{
7+
using Microsoft.VisualStudio.TestTools.UnitTesting;
8+
using System.Net.Http;
9+
10+
[TestClass]
11+
public class RequestExtensionsTests
12+
{
13+
[TestMethod]
14+
public void IsBuffered_Get()
15+
{
16+
HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Get, "http://example.com");
17+
var response = httpRequest.IsBuffered();
18+
19+
Assert.IsTrue(response, "Unexpected content type");
20+
}
21+
[TestMethod]
22+
public void IsBuffered_PostWithNoContent()
23+
{
24+
HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Post, "http://example.com");
25+
var response = httpRequest.IsBuffered();
26+
27+
Assert.IsTrue(response, "Unexpected content type");
28+
}
29+
[TestMethod]
30+
public void IsBuffered_PostWithBufferStringContent()
31+
{
32+
byte[] data = new byte[] { 1, 2, 3, 4, 5 };
33+
HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Post, "http://example.com");
34+
httpRequest.Content = new ByteArrayContent(data);
35+
var response = httpRequest.IsBuffered();
36+
37+
Assert.IsTrue(response, "Unexpected content type");
38+
}
39+
40+
[TestMethod]
41+
public void IsBuffered_PutWithStreamStringContent()
42+
{
43+
var stringContent = new StringContent("Hello World");
44+
var byteArrayContent = new ByteArrayContent(new byte[] { 1, 2, 3, 4, 5 });
45+
var mutliformDataContent = new MultipartFormDataContent();
46+
mutliformDataContent.Add(stringContent);
47+
mutliformDataContent.Add(byteArrayContent);
48+
49+
HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Put, "http://example.com");
50+
httpRequest.Content = mutliformDataContent;
51+
httpRequest.Content.Headers.ContentLength = -1;
52+
var response = httpRequest.IsBuffered();
53+
54+
Assert.IsFalse(response, "Unexpected content type");
55+
}
56+
57+
[TestMethod]
58+
public void IsBuffered_PatchWithStreamStringContent()
59+
{
60+
HttpRequestMessage httpRequest = new HttpRequestMessage(new HttpMethod("PATCH"), "http://example.com");
61+
httpRequest.Content = new StringContent("Hello World");
62+
httpRequest.Content.Headers.ContentLength = null;
63+
var response = httpRequest.IsBuffered();
64+
65+
Assert.IsFalse(response, "Unexpected content type");
66+
}
67+
}
68+
}

tests/Microsoft.Graph.Core.Test/Microsoft.Graph.Core.Test.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,14 @@
7979
<Compile Include="Authentication\DelegateAuthenticationProviderTests.cs" />
8080
<Compile Include="Exceptions\ErrorTests.cs" />
8181
<Compile Include="Exceptions\ServiceExceptionTests.cs" />
82+
<Compile Include="Extensions\RequestExtensionsTests.cs" />
8283
<Compile Include="Helpers\ExtractSelectHelperTest.cs" />
8384
<Compile Include="Helpers\StringHelperTests.cs" />
8485
<Compile Include="Helpers\UrlHelperTests.cs" />
8586
<Compile Include="Mocks\MockProgress.cs" />
8687
<Compile Include="Mocks\MockRedirectHandler.cs" />
8788
<Compile Include="Requests\AsyncMonitorTests.cs" />
89+
<Compile Include="Requests\AuthenticationHandlerTests.cs" />
8890
<Compile Include="Requests\BaseRequestBuilderTests.cs" />
8991
<Compile Include="Requests\GraphClientFactoryTests.cs" />
9092
<Compile Include="Requests\RedirectHandlerTests.cs" />

tests/Microsoft.Graph.Core.Test/Mocks/MockAuthenticationProvider.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ public MockAuthenticationProvider()
1616
{
1717
this.SetupAllProperties();
1818

19-
this.Setup(provider => provider.AuthenticateRequestAsync(It.IsAny<HttpRequestMessage>())).Returns(Task.FromResult(0));
19+
this.Setup(
20+
provider => provider.AuthenticateRequestAsync(It.IsAny<HttpRequestMessage>()))
21+
.Returns(Task.FromResult(0));
2022
}
2123
}
2224
}

0 commit comments

Comments
 (0)