-
Notifications
You must be signed in to change notification settings - Fork 263
Authentication handler #320
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
911edd8
Added authentication handler
peombwa 0dba35c
Authentication handler basic tests
peombwa 8c79899
Added 401 retry
peombwa 4babc8c
Added MaxRetry to 401 handling
peombwa c1f329d
Added 401 retry tests
peombwa cba2c1b
Updated tests
peombwa 1c1463d
Added RequestExtensions
peombwa File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| // ------------------------------------------------------------------------------ | ||
| // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. | ||
| // ------------------------------------------------------------------------------ | ||
|
|
||
| namespace Microsoft.Graph | ||
| { | ||
| using System.Net.Http; | ||
| /// <summary> | ||
| /// Contains extension methods for <see cref="HttpRequestMessage"/> | ||
| /// </summary> | ||
| internal static class RequestExtensions | ||
| { | ||
| /// <summary> | ||
| /// Checks the HTTP request's content to determine if it's buffered or streamed content. | ||
| /// </summary> | ||
| /// <param name="httpRequestMessage">The <see cref="HttpRequestMessage"/>needs to be sent.</param> | ||
| /// <returns></returns> | ||
| internal static bool IsBuffered(this HttpRequestMessage httpRequestMessage) | ||
| { | ||
| HttpContent requestContent = httpRequestMessage.Content; | ||
|
|
||
| if ((httpRequestMessage.Method == HttpMethod.Put || httpRequestMessage.Method == HttpMethod.Post || httpRequestMessage.Method.Method.Equals("PATCH")) | ||
| && requestContent != null && (requestContent.Headers.ContentLength == null || (int)requestContent.Headers.ContentLength == -1)) | ||
| { | ||
| return false; | ||
| } | ||
| return true; | ||
| } | ||
| } | ||
| } |
120 changes: 120 additions & 0 deletions
120
src/Microsoft.Graph.Core/Requests/AuthenticationHandler.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| // ------------------------------------------------------------------------------ | ||
| // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. | ||
| // ------------------------------------------------------------------------------ | ||
|
|
||
| namespace Microsoft.Graph | ||
| { | ||
| using System.Net.Http; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using System.Net; | ||
| /// <summary> | ||
| /// A <see cref="DelegatingHandler"/> implementation using standard .NET libraries. | ||
| /// </summary> | ||
| public class AuthenticationHandler: DelegatingHandler | ||
| { | ||
| /// <summary> | ||
| /// MaxRetry property for 401's | ||
| /// </summary> | ||
| public int MaxRetry { get; set; } = 1; | ||
|
|
||
| /// <summary> | ||
| /// AuthenticationProvider property | ||
| /// </summary> | ||
| public IAuthenticationProvider AuthenticationProvider { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Construct a new <see cref="AuthenticationHandler"/> | ||
| /// </summary> | ||
| public AuthenticationHandler() | ||
| { | ||
|
|
||
| } | ||
|
|
||
| /// <summary> | ||
| /// Construct a new <see cref="AuthenticationHandler"/> | ||
| /// <param name="authenticationProvider">An authentication provider to pass to <see cref="AuthenticationHandler"/> for authenticating requests.</param> | ||
| /// </summary> | ||
| public AuthenticationHandler(IAuthenticationProvider authenticationProvider) | ||
| { | ||
| AuthenticationProvider = authenticationProvider; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Construct a new <see cref="AuthenticationHandler"/> | ||
| /// </summary> | ||
| /// <param name="authenticationProvider">An authentication provider to pass to <see cref="AuthenticationHandler"/> for authenticating requests.</param> | ||
| /// <param name="innerHandler">A HTTP message handler to pass to the <see cref="AuthenticationHandler"/> for sending requests.</param> | ||
| public AuthenticationHandler(IAuthenticationProvider authenticationProvider, HttpMessageHandler innerHandler) | ||
| { | ||
| InnerHandler = innerHandler; | ||
| AuthenticationProvider = authenticationProvider; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Checks HTTP response message status code if it's unauthorized (401) or not | ||
| /// </summary> | ||
| /// <param name="httpResponseMessage">The <see cref="HttpResponseMessage"/>to send.</param> | ||
| /// <returns></returns> | ||
| private bool IsUnauthorized(HttpResponseMessage httpResponseMessage) | ||
| { | ||
| return httpResponseMessage.StatusCode == HttpStatusCode.Unauthorized; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Retry sending HTTP request | ||
| /// </summary> | ||
| /// <param name="httpResponseMessage">The <see cref="HttpResponseMessage"/>to send.</param> | ||
| /// <param name="cancellationToken">The <see cref="CancellationToken"/>to send.</param> | ||
| /// <returns></returns> | ||
| private async Task<HttpResponseMessage> SendRetryAsync(HttpResponseMessage httpResponseMessage, CancellationToken cancellationToken) | ||
| { | ||
| int retryAttempt = 0; | ||
| while (retryAttempt < MaxRetry) | ||
| { | ||
| var originalRequest = httpResponseMessage.RequestMessage; | ||
|
|
||
| // Authenticate request using AuthenticationProvider | ||
| await AuthenticationProvider.AuthenticateRequestAsync(originalRequest); | ||
| httpResponseMessage = await base.SendAsync(originalRequest, cancellationToken); | ||
|
|
||
| retryAttempt++; | ||
|
|
||
| if (!IsUnauthorized(httpResponseMessage) || !originalRequest.IsBuffered()) | ||
| { | ||
| // Re-issue the request to get a new access token | ||
| return httpResponseMessage; | ||
| } | ||
| } | ||
|
|
||
| return httpResponseMessage; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Sends a HTTP request and retries the request when the response is unauthorized. | ||
| /// This can happen when a token from the cache expires between graph getting the request and the backend receiving the request | ||
| /// </summary> | ||
peombwa marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| /// <param name="httpRequestMessage">The <see cref="HttpRequestMessage"/> to send.</param> | ||
| /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the request.</param> | ||
| /// <returns></returns> | ||
| protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage httpRequestMessage, CancellationToken cancellationToken) | ||
| { | ||
| // Authenticate request using AuthenticationProvider | ||
| if (AuthenticationProvider != null) | ||
| { | ||
| await AuthenticationProvider.AuthenticateRequestAsync(httpRequestMessage); | ||
| } | ||
|
|
||
| HttpResponseMessage response = await base.SendAsync(httpRequestMessage, cancellationToken); | ||
|
|
||
| // Chcek if response is a 401 & is not a streamed body (is buffered) | ||
| if (IsUnauthorized(response) && httpRequestMessage.IsBuffered() && (AuthenticationProvider != null)) | ||
| { | ||
| // re-issue the request to get a new access token | ||
| response = await SendRetryAsync(response, cancellationToken); | ||
| } | ||
|
|
||
| return response; | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
68 changes: 68 additions & 0 deletions
68
tests/Microsoft.Graph.Core.Test/Extensions/RequestExtensionsTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| // ------------------------------------------------------------------------------ | ||
| // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. | ||
| // ------------------------------------------------------------------------------ | ||
|
|
||
| namespace Microsoft.Graph.Core.Test.Extensions | ||
| { | ||
| using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
| using System.Net.Http; | ||
|
|
||
| [TestClass] | ||
| public class RequestExtensionsTests | ||
| { | ||
| [TestMethod] | ||
| public void IsBuffered_Get() | ||
| { | ||
| HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Get, "http://example.com"); | ||
| var response = httpRequest.IsBuffered(); | ||
|
|
||
| Assert.IsTrue(response, "Unexpected content type"); | ||
| } | ||
| [TestMethod] | ||
| public void IsBuffered_PostWithNoContent() | ||
| { | ||
| HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Post, "http://example.com"); | ||
| var response = httpRequest.IsBuffered(); | ||
|
|
||
| Assert.IsTrue(response, "Unexpected content type"); | ||
| } | ||
| [TestMethod] | ||
| public void IsBuffered_PostWithBufferStringContent() | ||
| { | ||
| byte[] data = new byte[] { 1, 2, 3, 4, 5 }; | ||
| HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Post, "http://example.com"); | ||
| httpRequest.Content = new ByteArrayContent(data); | ||
| var response = httpRequest.IsBuffered(); | ||
|
|
||
| Assert.IsTrue(response, "Unexpected content type"); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void IsBuffered_PutWithStreamStringContent() | ||
| { | ||
| var stringContent = new StringContent("Hello World"); | ||
| var byteArrayContent = new ByteArrayContent(new byte[] { 1, 2, 3, 4, 5 }); | ||
| var mutliformDataContent = new MultipartFormDataContent(); | ||
| mutliformDataContent.Add(stringContent); | ||
| mutliformDataContent.Add(byteArrayContent); | ||
|
|
||
| HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Put, "http://example.com"); | ||
| httpRequest.Content = mutliformDataContent; | ||
| httpRequest.Content.Headers.ContentLength = -1; | ||
| var response = httpRequest.IsBuffered(); | ||
|
|
||
| Assert.IsFalse(response, "Unexpected content type"); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void IsBuffered_PatchWithStreamStringContent() | ||
| { | ||
| HttpRequestMessage httpRequest = new HttpRequestMessage(new HttpMethod("PATCH"), "http://example.com"); | ||
| httpRequest.Content = new StringContent("Hello World"); | ||
| httpRequest.Content.Headers.ContentLength = null; | ||
| var response = httpRequest.IsBuffered(); | ||
|
|
||
| Assert.IsFalse(response, "Unexpected content type"); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We will need to consider the following behavior where an Authorization header is not expected per a workload:
https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/driveitem_createuploadsession#remarks
SendRetryAsync will cause an error if a Authorization header is provided in this scenario. Other than that, this PR looks good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this won't be an issue as we stap the Authorization header only for graph.microsoft.com issues.