-
Notifications
You must be signed in to change notification settings - Fork 33
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #430 from microsoft/feat/auth-handler
Adds Authorization handler
- Loading branch information
Showing
9 changed files
with
384 additions
and
44 deletions.
There are no files selected for viewing
This file contains 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 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 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 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,73 @@ | ||
using System; | ||
using System.Net; | ||
using System.Net.Http; | ||
using System.Net.Http.Headers; | ||
using System.Text.RegularExpressions; | ||
|
||
namespace Microsoft.Kiota.Http.HttpClientLibrary | ||
{ | ||
/// <summary> | ||
/// Process continuous access evaluation | ||
/// </summary> | ||
static internal class ContinuousAccessEvaluation | ||
{ | ||
internal const string ClaimsKey = "claims"; | ||
internal const string BearerAuthenticationScheme = "Bearer"; | ||
private static readonly char[] ComaSplitSeparator = [',']; | ||
private static Func<AuthenticationHeaderValue, bool> filterAuthHeader = static x => x.Scheme.Equals(BearerAuthenticationScheme, StringComparison.OrdinalIgnoreCase); | ||
private static readonly Regex caeValueRegex = new("\"([^\"]*)\"", RegexOptions.Compiled, TimeSpan.FromMilliseconds(100)); | ||
|
||
/// <summary> | ||
/// Extracts claims header value from a response | ||
/// </summary> | ||
/// <param name="response"></param> | ||
/// <returns></returns> | ||
public static string GetClaims(HttpResponseMessage response) | ||
{ | ||
if(response == null) throw new ArgumentNullException(nameof(response)); | ||
if(response.StatusCode != HttpStatusCode.Unauthorized | ||
|| response.Headers.WwwAuthenticate.Count == 0) | ||
{ | ||
return string.Empty; | ||
} | ||
AuthenticationHeaderValue? authHeader = null; | ||
foreach(var header in response.Headers.WwwAuthenticate) | ||
{ | ||
if(filterAuthHeader(header)) | ||
{ | ||
authHeader = header; | ||
break; | ||
} | ||
} | ||
if(authHeader is not null) | ||
{ | ||
var authHeaderParameters = authHeader.Parameter?.Split(ComaSplitSeparator, StringSplitOptions.RemoveEmptyEntries); | ||
|
||
string? rawResponseClaims = null; | ||
if(authHeaderParameters != null) | ||
{ | ||
foreach(var parameter in authHeaderParameters) | ||
{ | ||
var trimmedParameter = parameter.Trim(); | ||
if(trimmedParameter.StartsWith(ClaimsKey, StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
rawResponseClaims = trimmedParameter; | ||
break; | ||
} | ||
} | ||
} | ||
|
||
if(rawResponseClaims != null && | ||
caeValueRegex.Match(rawResponseClaims) is Match claimsMatch && | ||
claimsMatch.Groups.Count > 1 && | ||
claimsMatch.Groups[1].Value is string responseClaims) | ||
{ | ||
return responseClaims; | ||
} | ||
|
||
} | ||
return string.Empty; | ||
} | ||
} | ||
} | ||
|
This file contains 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 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 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,104 @@ | ||
// ------------------------------------------------------------------------------ | ||
// 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.Diagnostics; | ||
using System.Net; | ||
using System.Net.Http; | ||
using System.Net.Http.Headers; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.Kiota.Abstractions.Authentication; | ||
using Microsoft.Kiota.Http.HttpClientLibrary.Extensions; | ||
|
||
namespace Microsoft.Kiota.Http.HttpClientLibrary.Middleware | ||
{ | ||
/// <summary> | ||
/// Adds an Authorization header to the request if the header is not already present. | ||
/// Also handles Continuous Access Evaluation (CAE) claims challenges if the initial | ||
/// token request was made using this handler | ||
/// </summary> | ||
public class AuthorizationHandler : DelegatingHandler | ||
{ | ||
|
||
private const string AuthorizationHeader = "Authorization"; | ||
private readonly BaseBearerTokenAuthenticationProvider authenticationProvider; | ||
|
||
/// <summary> | ||
/// Constructs an <see cref="AuthorizationHandler"/> | ||
/// </summary> | ||
/// <param name="authenticationProvider"></param> | ||
/// <exception cref="ArgumentNullException"></exception> | ||
public AuthorizationHandler(BaseBearerTokenAuthenticationProvider authenticationProvider) | ||
{ | ||
this.authenticationProvider = authenticationProvider ?? throw new ArgumentNullException(nameof(authenticationProvider)); | ||
} | ||
|
||
/// <summary> | ||
/// Adds an Authorization header if not already provided | ||
/// </summary> | ||
/// <param name="request"></param> | ||
/// <param name="cancellationToken"></param> | ||
/// <returns></returns> | ||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, | ||
CancellationToken cancellationToken) | ||
{ | ||
if(request == null) throw new ArgumentNullException(nameof(request)); | ||
Activity? activity = null; | ||
if(request.GetRequestOption<ObservabilityOptions>() is { } obsOptions) | ||
{ | ||
var activitySource = ActivitySourceRegistry.DefaultInstance.GetOrCreateActivitySource(obsOptions.TracerInstrumentationName); | ||
activity = activitySource?.StartActivity($"{nameof(AuthorizationHandler)}_{nameof(SendAsync)}"); | ||
activity?.SetTag("com.microsoft.kiota.handler.authorization.enable", true); | ||
} | ||
try | ||
{ | ||
if(request.Headers.Contains(AuthorizationHeader)) | ||
{ | ||
activity?.SetTag("com.microsoft.kiota.handler.authorization.token_present", true); | ||
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); | ||
} | ||
Dictionary<string, object> additionalAuthenticationContext = new Dictionary<string, object>(); | ||
await AuthenticateRequestAsync(request, additionalAuthenticationContext, activity, cancellationToken).ConfigureAwait(false); | ||
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); | ||
if(response.StatusCode != HttpStatusCode.Unauthorized || response.RequestMessage == null || !response.RequestMessage.IsBuffered()) | ||
return response; | ||
// Attempt CAE claims challenge | ||
var claims = ContinuousAccessEvaluation.GetClaims(response); | ||
if(string.IsNullOrEmpty(claims)) | ||
return response; | ||
activity?.AddEvent(new ActivityEvent("com.microsoft.kiota.handler.authorization.challenge_received")); | ||
additionalAuthenticationContext[ContinuousAccessEvaluation.ClaimsKey] = claims; | ||
var retryRequest = await response.RequestMessage.CloneAsync(cancellationToken); | ||
await AuthenticateRequestAsync(retryRequest, additionalAuthenticationContext, activity, cancellationToken).ConfigureAwait(false); | ||
activity?.SetTag("http.request.resend_count", 1); | ||
return await base.SendAsync(retryRequest, cancellationToken).ConfigureAwait(false); | ||
} | ||
finally | ||
{ | ||
activity?.Dispose(); | ||
} | ||
} | ||
|
||
private async Task AuthenticateRequestAsync(HttpRequestMessage request, | ||
Dictionary<string, object> additionalAuthenticationContext, | ||
Activity? activityForAttributes, | ||
CancellationToken cancellationToken) | ||
{ | ||
var accessTokenProvider = authenticationProvider.AccessTokenProvider; | ||
if(request.RequestUri == null || !accessTokenProvider.AllowedHostsValidator.IsUrlHostValid( | ||
request.RequestUri)) | ||
{ | ||
return; | ||
} | ||
var accessToken = await accessTokenProvider.GetAuthorizationTokenAsync( | ||
request.RequestUri, | ||
additionalAuthenticationContext, cancellationToken).ConfigureAwait(false); | ||
activityForAttributes?.SetTag("com.microsoft.kiota.handler.authorization.token_obtained", true); | ||
if(string.IsNullOrEmpty(accessToken)) return; | ||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); | ||
} | ||
} | ||
} |
This file contains 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.