From 397943bd43b37ddc0a19c364e0ee2519fb0fe2cc Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Wed, 20 Jan 2021 17:10:13 -0600 Subject: [PATCH 01/22] BearerTokenAuthenticationPolicy in Experimental from scott's branch --- .../src/Azure.Core.Experimental.csproj | 3 +- .../src/TokenCredential.cs | 31 + .../src/TokenRequestContext.cs | 51 ++ .../AzureCoreExperimentalEventSource.cs | 25 + .../BearerTokenAuthenticationPolicy.cs | 404 ++++++++++ .../BearerTokenAuthenticationPolicyTests.cs | 755 ++++++++++++++++++ sdk/core/Azure.Core/src/Shared/base64.cs | 55 ++ 7 files changed, 1323 insertions(+), 1 deletion(-) create mode 100644 sdk/core/Azure.Core.Experimental/src/TokenCredential.cs create mode 100644 sdk/core/Azure.Core.Experimental/src/TokenRequestContext.cs create mode 100644 sdk/core/Azure.Core.Experimental/src/diagnostics/AzureCoreExperimentalEventSource.cs create mode 100644 sdk/core/Azure.Core.Experimental/src/pipeline/BearerTokenAuthenticationPolicy.cs create mode 100644 sdk/core/Azure.Core.Experimental/tests/BearerTokenAuthenticationPolicyTests.cs create mode 100644 sdk/core/Azure.Core/src/Shared/base64.cs diff --git a/sdk/core/Azure.Core.Experimental/src/Azure.Core.Experimental.csproj b/sdk/core/Azure.Core.Experimental/src/Azure.Core.Experimental.csproj index 00470e50bac06..b46b466fde5c6 100644 --- a/sdk/core/Azure.Core.Experimental/src/Azure.Core.Experimental.csproj +++ b/sdk/core/Azure.Core.Experimental/src/Azure.Core.Experimental.csproj @@ -15,8 +15,9 @@ + - + diff --git a/sdk/core/Azure.Core.Experimental/src/TokenCredential.cs b/sdk/core/Azure.Core.Experimental/src/TokenCredential.cs new file mode 100644 index 0000000000000..b0f0c91e16f27 --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/src/TokenCredential.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Azure.Core.Experimental +{ + /// + /// Represents a credential capable of providing an OAuth token. + /// + public abstract class TokenCredential + { + /// + /// Gets an for the specified set of scopes. + /// + /// The with authentication information. + /// The to use. + /// A valid . + public abstract ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken); + + /// + /// Gets an for the specified set of scopes. + /// + /// The with authentication information. + /// The to use. + /// A valid . + public abstract AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken); + } +} diff --git a/sdk/core/Azure.Core.Experimental/src/TokenRequestContext.cs b/sdk/core/Azure.Core.Experimental/src/TokenRequestContext.cs new file mode 100644 index 0000000000000..bd9229378ff31 --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/src/TokenRequestContext.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Azure.Core.Experimental +{ + /// + /// Contains the details of an authentication token request. + /// + public readonly struct TokenRequestContext + { + /// + /// Creates a new TokenRequest with the specified scopes. + /// + /// The scopes required for the token. + /// The of the request requiring a token for authentication, if applicable. + public TokenRequestContext(string[] scopes, string? parentRequestId) + { + Scopes = scopes; + ParentRequestId = parentRequestId; + ClaimsChallenge = default; + } + + /// + /// Creates a new TokenRequest with the specified scopes. + /// + /// The scopes required for the token. + /// The of the request requiring a token for authentication, if applicable. + /// A claims challenge returned from a failed authentication or authorization request. + public TokenRequestContext(string[] scopes, string? parentRequestId = default, string? claimsChallenge = default) + { + Scopes = scopes; + ClaimsChallenge = claimsChallenge; + ParentRequestId = parentRequestId; + } + + /// + /// The scopes required for the token. + /// + public string[] Scopes { get; } + + /// + /// A claims challenge returned from a failed authentication or authorization request. + /// + public string? ClaimsChallenge { get; } + + /// + /// The of the request requiring a token for authentication, if applicable. + /// + public string? ParentRequestId { get; } + } +} diff --git a/sdk/core/Azure.Core.Experimental/src/diagnostics/AzureCoreExperimentalEventSource.cs b/sdk/core/Azure.Core.Experimental/src/diagnostics/AzureCoreExperimentalEventSource.cs new file mode 100644 index 0000000000000..2235c0cf3774d --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/src/diagnostics/AzureCoreExperimentalEventSource.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Diagnostics.Tracing; + +namespace Azure.Core.Diagnostics +{ + [EventSource(Name = EventSourceName)] + internal sealed class AzureCoreExperimentalEventSource : EventSource + { + private const string EventSourceName = "Azure-Core-Experimental"; + + private const int BackgroundRefreshFailedEvent = 19; + + private AzureCoreExperimentalEventSource() : base(EventSourceName, EventSourceSettings.Default, AzureEventSourceListener.TraitName, AzureEventSourceListener.TraitValue) { } + + public static AzureCoreExperimentalEventSource Singleton { get; } = new AzureCoreExperimentalEventSource(); + + [Event(BackgroundRefreshFailedEvent, Level = EventLevel.Informational, Message = "Background token refresh [{0}] failed with exception {1}")] + public void BackgroundRefreshFailed(string requestId, string exception) + { + WriteEvent(BackgroundRefreshFailedEvent, requestId, exception); + } + } +} diff --git a/sdk/core/Azure.Core.Experimental/src/pipeline/BearerTokenAuthenticationPolicy.cs b/sdk/core/Azure.Core.Experimental/src/pipeline/BearerTokenAuthenticationPolicy.cs new file mode 100644 index 0000000000000..333adb285fbfa --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/src/pipeline/BearerTokenAuthenticationPolicy.cs @@ -0,0 +1,404 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Azure.Core.Diagnostics; +using Azure.Core.Pipeline; + +namespace Azure.Core.Experimental.Pipeline +{ + /// + /// A policy that sends an provided by a as an Authentication header. + /// + public class BearerTokenAuthenticationPolicy : Core.Pipeline.HttpPipelinePolicy + { + private const string AuthenticationChallengePattern = @"(\w+) ((?:\w+="".*?""(?:, )?)+)(?:, )?"; + private const string ChallengeParameterPattern = @"(?:(\w+)=""([^""]*)"")+"; + + private static readonly Regex s_AuthenticationChallengeRegex = new Regex(AuthenticationChallengePattern, RegexOptions.Compiled); + private static readonly Regex s_ChallengeParameterRegex = new Regex(ChallengeParameterPattern, RegexOptions.Compiled); + + private readonly AccessTokenCache _accessTokenCache; + private readonly string[] _scopes; + + /// + /// Creates a new instance of using provided token credential and scope to authenticate for. + /// + /// The token credential to use for authentication. + /// The scope to authenticate for. + public BearerTokenAuthenticationPolicy(Experimental.TokenCredential credential, string scope) : this(credential, new[] { scope }) { } + + /// + /// Creates a new instance of using provided token credential and scopes to authenticate for. + /// + /// The token credential to use for authentication. + /// Scopes to authenticate for. + public BearerTokenAuthenticationPolicy(Experimental.TokenCredential credential, IEnumerable scopes) + : this(credential, scopes, TimeSpan.FromMinutes(5), TimeSpan.FromSeconds(30)) { } + + internal BearerTokenAuthenticationPolicy(Experimental.TokenCredential credential, IEnumerable scopes, TimeSpan tokenRefreshOffset, TimeSpan tokenRefreshRetryDelay) + { + Argument.AssertNotNull(credential, nameof(credential)); + Argument.AssertNotNull(scopes, nameof(scopes)); + + _scopes = scopes.ToArray(); + _accessTokenCache = new AccessTokenCache(credential, tokenRefreshOffset, tokenRefreshRetryDelay, scopes.ToArray()); + } + + /// + public override ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline) + { + return ProcessAsync(message, pipeline, true); + } + + /// + public override void Process(HttpMessage message, ReadOnlyMemory pipeline) + { + ProcessAsync(message, pipeline, false).EnsureCompleted(); + } + + private async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline, bool async) + { + if (message.Request.Uri.Scheme != Uri.UriSchemeHttps) + { + throw new InvalidOperationException("Bearer token authentication is not permitted for non TLS protected (https) endpoints."); + } + + await AuthenticateRequestAsync(message, new Experimental.TokenRequestContext(_scopes, message.Request.ClientRequestId), async).ConfigureAwait(false); + + if (async) + { + await ProcessNextAsync(message, pipeline).ConfigureAwait(false); + } + else + { + ProcessNext(message, pipeline); + } + + if (message.Response.Status == 401 && message.Response.Headers.Contains("WWW-Authenticate")) + { + if (await OnChallengeAsync(message, async).ConfigureAwait(false)) + { + if (async) + { + await ProcessNextAsync(message, pipeline).ConfigureAwait(false); + } + else + { + ProcessNext(message, pipeline); + } + } + } + } + + internal static IEnumerable<(string, string)> ParseChallenges(string headerValue) + { + var challengeMatches = s_AuthenticationChallengeRegex.Matches(headerValue); + + for (int i = 0; i < challengeMatches.Count; i++) + { + yield return (challengeMatches[i].Groups[1].Value, challengeMatches[i].Groups[2].Value); + } + } + + internal static IEnumerable<(string, string)> ParseChallengeParameters(string challengeValue) + { + var paramMatches = s_ChallengeParameterRegex.Matches(challengeValue); + + for (int i = 0; i < paramMatches.Count; i++) + { + yield return (paramMatches[i].Groups[1].Value, paramMatches[i].Groups[2].Value); + } + } + + /// + /// Executed before initially sending the request to authenticate the request. + /// + /// The HttpMessage to be authenticated. + /// Specifies if the method is being called in an asynchronous context + protected virtual async Task OnBeforeRequestAsync(HttpMessage message, bool async) + { + await AuthenticateRequestAsync(message, new Experimental.TokenRequestContext(_scopes, message.Request.ClientRequestId), async).ConfigureAwait(false); + } + + /// + /// Executed in the event a 401 response with a WWW-Authenticate authentication challenge header is received after the initial request. + /// + /// This implementation handles common authentication challenges such as claims challenges. Service client libraries may derive from this and extend to handle service specific authentication challenges. + /// The HttpMessage to be authenticated. + /// Specifies if the method is being called in an asynchronous context + /// A boolean indicated whether the request should be retried + protected virtual async Task OnChallengeAsync(HttpMessage message, bool async) + { + var claimsChallenge = GetClaimsChallenge(message.Response); + + if (claimsChallenge != null) + { + await AuthenticateRequestAsync(message, new Experimental.TokenRequestContext(_scopes, message.Request.ClientRequestId, claimsChallenge), async).ConfigureAwait(false); + + return true; + } + + return false; + } + + private static string? GetClaimsChallenge(Response response) + { + if (response.Status == 401 && response.Headers.TryGetValue("WWW-Authenticate", out string? headerValue)) + { + foreach (var challenge in ParseChallenges(headerValue)) + { + if (string.Equals(challenge.Item1, "Bearer", StringComparison.OrdinalIgnoreCase)) + { + foreach (var parameter in ParseChallengeParameters(challenge.Item2)) + { + if (string.Equals(parameter.Item1, "claims", StringComparison.OrdinalIgnoreCase)) + { + // currently we are only handling ARM claims challenges which are always b64url encoded, and must be decoded. + // some handling will have to be added if we intend to handle claims challenges from Graph as well since they + // are not encoded. + return Base64Url.DecodeString(parameter.Item2); + } + } + } + } + } + + return null; + } + + private async Task AuthenticateRequestAsync(HttpMessage message, Experimental.TokenRequestContext context, bool async) + { + string headerValue; + if (async) + { + headerValue = await _accessTokenCache.GetHeaderValueAsync(message, context, async).ConfigureAwait(false); + } + else + { + headerValue = _accessTokenCache.GetHeaderValueAsync(message, context, async).EnsureCompleted(); + } + + //TODO: revert to Request.SetHeader if this migrates back to Azure.Core + message.Request.Headers.SetValue(HttpHeader.Names.Authorization, headerValue); + } + + private class AccessTokenCache + { + private readonly object _syncObj = new object(); + private readonly Experimental.TokenCredential _credential; + private readonly TimeSpan _tokenRefreshOffset; + private readonly TimeSpan _tokenRefreshRetryDelay; + + private Experimental.TokenRequestContext? _currentContext; + private TaskCompletionSource? _infoTcs; + private TaskCompletionSource? _backgroundUpdateTcs; + public AccessTokenCache(Experimental.TokenCredential credential, TimeSpan tokenRefreshOffset, TimeSpan tokenRefreshRetryDelay, string[] initialScopes) + { + _credential = credential; + _tokenRefreshOffset = tokenRefreshOffset; + _tokenRefreshRetryDelay = tokenRefreshRetryDelay; + _currentContext = new Experimental.TokenRequestContext(initialScopes); + } + + public async ValueTask GetHeaderValueAsync(HttpMessage message, Experimental.TokenRequestContext context, bool async) + { + bool getTokenFromCredential; + TaskCompletionSource headerValueTcs; + TaskCompletionSource? backgroundUpdateTcs; + (headerValueTcs, backgroundUpdateTcs, getTokenFromCredential) = GetTaskCompletionSources(context); + HeaderValueInfo info; + + if (getTokenFromCredential) + { + if (backgroundUpdateTcs != null) + { + if (async) + { + info = await headerValueTcs.Task.ConfigureAwait(false); + } + else + { +#pragma warning disable AZC0104 // Use EnsureCompleted() directly on asynchronous method return value. + info = headerValueTcs.Task.EnsureCompleted(); +#pragma warning restore AZC0104 // Use EnsureCompleted() directly on asynchronous method return value. + } + _ = Task.Run(() => GetHeaderValueFromCredentialInBackgroundAsync(backgroundUpdateTcs, info, context, async)); + return info.HeaderValue; + } + + try + { + info = await GetHeaderValueFromCredentialAsync(context, async, message.CancellationToken).ConfigureAwait(false); + headerValueTcs.SetResult(info); + } + catch (OperationCanceledException) + { + headerValueTcs.SetCanceled(); + throw; + } + catch (Exception exception) + { + headerValueTcs.SetException(exception); + throw; + } + } + + var headerValueTask = headerValueTcs.Task; + if (!headerValueTask.IsCompleted) + { + if (async) + { + await headerValueTask.AwaitWithCancellation(message.CancellationToken); + } + else + { + try + { + headerValueTask.Wait(message.CancellationToken); + } + catch (AggregateException) { } // ignore exception here to rethrow it with EnsureCompleted + } + } + if (async) + { + info = await headerValueTcs.Task.ConfigureAwait(false); + } + else + { +#pragma warning disable AZC0104 // Use EnsureCompleted() directly on asynchronous method return value. + info = headerValueTcs.Task.EnsureCompleted(); +#pragma warning restore AZC0104 // Use EnsureCompleted() directly on asynchronous method return value. + } + + return info.HeaderValue; + } + + private (TaskCompletionSource tcs, TaskCompletionSource? backgroundUpdateTcs, bool getTokenFromCredential) GetTaskCompletionSources(Experimental.TokenRequestContext context) + { + lock (_syncObj) + { + // Initial state. GetTaskCompletionSources has been called for the first time + if (_infoTcs == null || RequestRequiresNewToken(context)) + { + _currentContext = context; + _infoTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + _backgroundUpdateTcs = default; + return (_infoTcs, _backgroundUpdateTcs, true); + } + + // Getting new access token is in progress, wait for it + if (!_infoTcs.Task.IsCompleted) + { + _backgroundUpdateTcs = default; + return (_infoTcs, _backgroundUpdateTcs, false); + } + + DateTimeOffset now = DateTimeOffset.UtcNow; + // Access token has been successfully acquired in background and it is not expired yet, use it instead of current one + if (_backgroundUpdateTcs != null && _backgroundUpdateTcs.Task.Status == TaskStatus.RanToCompletion && _backgroundUpdateTcs.Task.Result.ExpiresOn > now) + { + _infoTcs = _backgroundUpdateTcs; + _backgroundUpdateTcs = default; + } + + // Attempt to get access token has failed or it has already expired. Need to get a new one + if (_infoTcs.Task.Status != TaskStatus.RanToCompletion || now >= _infoTcs.Task.Result.ExpiresOn) + { + _infoTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + return (_infoTcs, default, true); + } + + // Access token is still valid but is about to expire, try to get it in background + if (now >= _infoTcs.Task.Result.RefreshOn && _backgroundUpdateTcs == null) + { + _backgroundUpdateTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + return (_infoTcs, _backgroundUpdateTcs, true); + } + + // Access token is valid, use it + return (_infoTcs, default, false); + } + } + + // must be called under lock (_syncObj) + private bool RequestRequiresNewToken(Experimental.TokenRequestContext context) + { + if (_currentContext == null) + { + return true; + } + + if (context.Scopes != null) + { + for (int i = 0; i < context.Scopes.Length; i++) + { + if (context.Scopes[i] != _currentContext.Value.Scopes?[i]) + { + return true; + } + } + } + + if ((context.ClaimsChallenge != null) && (context.ClaimsChallenge != _currentContext.Value.ClaimsChallenge)) + { + return true; + } + + return false; + } + + private async ValueTask GetHeaderValueFromCredentialInBackgroundAsync(TaskCompletionSource backgroundUpdateTcs, HeaderValueInfo info, Experimental.TokenRequestContext context, bool async) + { + var cts = new CancellationTokenSource(_tokenRefreshRetryDelay); + try + { + HeaderValueInfo newInfo = await GetHeaderValueFromCredentialAsync(context, async, cts.Token).ConfigureAwait(false); + backgroundUpdateTcs.SetResult(newInfo); + } + catch (OperationCanceledException oce) when (cts.IsCancellationRequested) + { + backgroundUpdateTcs.SetResult(new HeaderValueInfo(info.HeaderValue, info.ExpiresOn, DateTimeOffset.UtcNow)); + AzureCoreExperimentalEventSource.Singleton.BackgroundRefreshFailed(context.ParentRequestId ?? string.Empty, oce.ToString()); + } + catch (Exception e) + { + backgroundUpdateTcs.SetResult(new HeaderValueInfo(info.HeaderValue, info.ExpiresOn, DateTimeOffset.UtcNow + _tokenRefreshRetryDelay)); + AzureCoreExperimentalEventSource.Singleton.BackgroundRefreshFailed(context.ParentRequestId ?? string.Empty, e.ToString()); + } + finally + { + cts.Dispose(); + } + } + + private async ValueTask GetHeaderValueFromCredentialAsync(Experimental.TokenRequestContext context, bool async, CancellationToken cancellationToken) + { + AccessToken token = async + ? await _credential.GetTokenAsync(context, cancellationToken).ConfigureAwait(false) + : _credential.GetToken(context, cancellationToken); + + return new HeaderValueInfo("Bearer " + token.Token, token.ExpiresOn, token.ExpiresOn - _tokenRefreshOffset); + } + + private readonly struct HeaderValueInfo + { + public string HeaderValue { get; } + public DateTimeOffset ExpiresOn { get; } + public DateTimeOffset RefreshOn { get; } + + public HeaderValueInfo(string headerValue, DateTimeOffset expiresOn, DateTimeOffset refreshOn) + { + HeaderValue = headerValue; + ExpiresOn = expiresOn; + RefreshOn = refreshOn; + } + } + } + } +} diff --git a/sdk/core/Azure.Core.Experimental/tests/BearerTokenAuthenticationPolicyTests.cs b/sdk/core/Azure.Core.Experimental/tests/BearerTokenAuthenticationPolicyTests.cs new file mode 100644 index 0000000000000..eeff9c56dd565 --- /dev/null +++ b/sdk/core/Azure.Core.Experimental/tests/BearerTokenAuthenticationPolicyTests.cs @@ -0,0 +1,755 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Azure.Core.TestFramework; +using Moq; +using NUnit.Framework; +using Azure.Core.Experimental.Pipeline; + +namespace Azure.Core.Tests +{ + public class BearerTokenAuthenticationPolicyTests : SyncAsyncPolicyTestBase + { + public BearerTokenAuthenticationPolicyTests(bool isAsync) : base(isAsync) { } + + [Test] + public async Task BearerTokenAuthenticationPolicy_UsesTokenProvidedByCredentials() + { + var credential = new TokenCredentialStub( + (r, c) => r.Scopes.SequenceEqual(new[] { "scope1", "scope2" }) ? new AccessToken("token", DateTimeOffset.MaxValue) : default, IsAsync); + var policy = new BearerTokenAuthenticationPolicy(credential, new[] { "scope1", "scope2" }); + + MockTransport transport = CreateMockTransport(new MockResponse(200)); + await SendGetRequest(transport, policy, uri: new Uri("https://example.com")); + + Assert.True(transport.SingleRequest.Headers.TryGetValue("Authorization", out string authValue)); + Assert.AreEqual("Bearer token", authValue); + } + + [Test] + public async Task BearerTokenAuthenticationPolicy_RequestsTokenEveryRequest() + { + var accessTokens = new Queue(); + accessTokens.Enqueue(new AccessToken("token1", DateTimeOffset.UtcNow)); + accessTokens.Enqueue(new AccessToken("token2", DateTimeOffset.UtcNow)); + + var credential = new TokenCredentialStub( + (r, c) => r.Scopes.SequenceEqual(new[] { "scope1", "scope2" }) ? accessTokens.Dequeue() : default, IsAsync); + + var policy = new BearerTokenAuthenticationPolicy(credential, new[] { "scope1", "scope2" }); + MockTransport transport = CreateMockTransport(new MockResponse(200), new MockResponse(200)); + + await SendGetRequest(transport, policy, uri: new Uri("https://example.com")); + await SendGetRequest(transport, policy, uri: new Uri("https://example.com")); + + Assert.True(transport.Requests[0].Headers.TryGetValue("Authorization", out string auth1Value)); + Assert.True(transport.Requests[1].Headers.TryGetValue("Authorization", out string auth2Value)); + + Assert.AreEqual("Bearer token1", auth1Value); + Assert.AreEqual("Bearer token2", auth2Value); + } + + [Test] + public async Task BearerTokenAuthenticationPolicy_CachesHeaderValue() + { + var credential = new TokenCredentialStub( + (r, c) => r.Scopes.SequenceEqual(new[] { "scope" }) ? new AccessToken("token", DateTimeOffset.MaxValue) : default, IsAsync); + + var policy = new BearerTokenAuthenticationPolicy(credential, "scope"); + MockTransport transport = CreateMockTransport(new MockResponse(200), new MockResponse(200)); + + await SendGetRequest(transport, policy, uri: new Uri("https://example.com")); + await SendGetRequest(transport, policy, uri: new Uri("https://example.com")); + + Assert.True(transport.Requests[0].Headers.TryGetValue("Authorization", out string auth1Value)); + Assert.True(transport.Requests[1].Headers.TryGetValue("Authorization", out string auth2Value)); + + Assert.AreSame(auth1Value, auth1Value); + Assert.AreEqual("Bearer token", auth2Value); + } + + [Test] + public void BearerTokenAuthenticationPolicy_ThrowsForNonTlsEndpoint() + { + var credential = new TokenCredentialStub( + (r, c) => r.Scopes.SequenceEqual(new[] { "scope" }) ? new AccessToken("token", DateTimeOffset.MaxValue) : default, IsAsync); + + var policy = new BearerTokenAuthenticationPolicy(credential, "scope"); + MockTransport transport = CreateMockTransport(); + + Assert.ThrowsAsync(async () => await SendGetRequest(transport, policy, uri: new Uri("http://example.com"))); + } + + [Test] + public void BearerTokenAuthenticationPolicy_ThrowsForEmptyToken() + { + var credential = new TokenCredentialStub((r, c) => new AccessToken(string.Empty, DateTimeOffset.MaxValue), IsAsync); + + var policy = new BearerTokenAuthenticationPolicy(credential, "scope"); + MockTransport transport = CreateMockTransport(); + + Assert.ThrowsAsync(async () => await SendGetRequest(transport, policy, uri: new Uri("http://example.com"))); + } + + [Test] + public async Task BearerTokenAuthenticationPolicy_OneHundredConcurrentCalls() + { + var credential = new TokenCredentialStub((r, c) => + { + Thread.Sleep(100); + return new AccessToken(Guid.NewGuid().ToString(), DateTimeOffset.UtcNow.AddMinutes(30)); + }, IsAsync); + + var policy = new BearerTokenAuthenticationPolicy(credential, "scope"); + MockTransport transport = CreateMockTransport(r => new MockResponse(200)); + var requestTasks = new Task[100]; + + for (int i = 0; i < requestTasks.Length; i++) + { + requestTasks[i] = SendGetRequest(transport, policy, uri: new Uri("https://example.com")); + } + + await Task.WhenAll(requestTasks); + Assert.True(transport.Requests[0].Headers.TryGetValue("Authorization", out string auth1Value)); + + for (int i = 1; i < requestTasks.Length; i++) + { + Assert.True(transport.Requests[i].Headers.TryGetValue("Authorization", out string authValue)); + Assert.AreEqual(auth1Value, authValue); + } + } + + [Test] + public async Task BearerTokenAuthenticationPolicy_GatedConcurrentCalls() + { + var requestMre = new ManualResetEventSlim(false); + var responseMre = new ManualResetEventSlim(false); + var credential = new TokenCredentialStub((r, c) => + { + requestMre.Set(); + responseMre.Wait(c); + return new AccessToken(Guid.NewGuid().ToString(), DateTimeOffset.UtcNow.AddMinutes(30)); + }, IsAsync); + + var policy = new BearerTokenAuthenticationPolicy(credential, "scope"); + MockTransport transport = CreateMockTransport(new MockResponse(200), new MockResponse(200)); + + var firstRequestTask = SendGetRequest(transport, policy, uri: new Uri("https://example.com")); + requestMre.Wait(); + + var secondRequestTask = SendGetRequest(transport, policy, uri: new Uri("https://example.com")); + responseMre.Set(); + + await Task.WhenAll(firstRequestTask, secondRequestTask); + + Assert.True(transport.Requests[0].Headers.TryGetValue("Authorization", out string auth1Value)); + Assert.True(transport.Requests[1].Headers.TryGetValue("Authorization", out string auth2Value)); + + Assert.AreEqual(auth1Value, auth2Value); + } + + [Test] + public async Task BearerTokenAuthenticationPolicy_SucceededFailedSucceeded() + { + var requestMre = new ManualResetEventSlim(false); + var callCount = 0; + var credential = new TokenCredentialStub((r, c) => + { + Interlocked.Increment(ref callCount); + var offsetTime = DateTimeOffset.UtcNow; + requestMre.Set(); + + return callCount == 2 + ? throw new InvalidOperationException("Call Failed") + : new AccessToken(Guid.NewGuid().ToString(), offsetTime.AddMilliseconds(1000)); + }, IsAsync); + + var policy = new BearerTokenAuthenticationPolicy(credential, new[] { "scope" }, TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(30)); + MockTransport transport = CreateMockTransport(r => new MockResponse(200)); + + var firstRequestTask = SendGetRequest(transport, policy, uri: new Uri("https://example.com/1")); + var secondRequestTask = SendGetRequest(transport, policy, uri: new Uri("https://example.com/2")); + + requestMre.Wait(); + await Task.Delay(200); + + await Task.WhenAll(firstRequestTask, secondRequestTask); + await Task.Delay(1000); + + Assert.AreEqual(1, callCount); + requestMre.Reset(); + + var failedTask = SendGetRequest(transport, policy, uri: new Uri("https://example.com/3/failed")); + requestMre.Wait(); + + Assert.AreEqual(2, callCount); + Assert.ThrowsAsync(async () => await failedTask); + + requestMre.Reset(); + + firstRequestTask = SendGetRequest(transport, policy, uri: new Uri("https://example.com/4")); + secondRequestTask = SendGetRequest(transport, policy, uri: new Uri("https://example.com/5")); + + requestMre.Wait(); + + await Task.WhenAll(firstRequestTask, secondRequestTask); + + Assert.True(transport.Requests[0].Headers.TryGetValue("Authorization", out string auth1Value)); + Assert.True(transport.Requests[1].Headers.TryGetValue("Authorization", out string auth2Value)); + Assert.True(transport.Requests[2].Headers.TryGetValue("Authorization", out string auth3Value)); + Assert.True(transport.Requests[3].Headers.TryGetValue("Authorization", out string auth4Value)); + + Assert.AreEqual(3, callCount); + Assert.AreEqual(auth1Value, auth2Value); + Assert.AreNotEqual(auth2Value, auth3Value); + Assert.AreEqual(auth3Value, auth4Value); + } + + [Test] + public async Task BearerTokenAuthenticationPolicy_TokenAlmostExpired() + { + var requestMre = new ManualResetEventSlim(true); + var responseMre = new ManualResetEventSlim(true); + var currentTime = DateTimeOffset.UtcNow; + var expires = new Queue(new[] { currentTime.AddMinutes(2), currentTime.AddMinutes(30) }); + var callCount = 0; + var credential = new TokenCredentialStub((r, c) => + { + requestMre.Set(); + responseMre.Wait(c); + requestMre.Reset(); + callCount++; + + return new AccessToken(Guid.NewGuid().ToString(), expires.Dequeue()); + }, IsAsync); + + var policy = new BearerTokenAuthenticationPolicy(credential, "scope"); + MockTransport transport = CreateMockTransport(new MockResponse(200), new MockResponse(200), new MockResponse(200), new MockResponse(200)); + + await SendGetRequest(transport, policy, uri: new Uri("https://example.com/1/Original")); + responseMre.Reset(); + + Task requestTask = SendGetRequest(transport, policy, uri: new Uri("https://example.com/3/Refresh")); + requestMre.Wait(); + + await SendGetRequest(transport, policy, uri: new Uri("https://example.com/2/AlmostExpired")); + await requestTask; + responseMre.Set(); + await Task.Delay(1_000); + + await SendGetRequest(transport, policy, uri: new Uri("https://example.com/4/AfterRefresh")); + + Assert.AreEqual(2, callCount); + + Assert.True(transport.Requests[0].Headers.TryGetValue("Authorization", out string auth1Value)); + Assert.True(transport.Requests[1].Headers.TryGetValue("Authorization", out string auth2Value)); + Assert.True(transport.Requests[2].Headers.TryGetValue("Authorization", out string auth3Value)); + Assert.True(transport.Requests[3].Headers.TryGetValue("Authorization", out string auth4Value)); + + Assert.AreEqual(auth1Value, auth2Value); + Assert.AreEqual(auth2Value, auth3Value); + Assert.AreNotEqual(auth3Value, auth4Value); + } + + [Test] + public async Task BearerTokenAuthenticationPolicy_TokenAlmostExpired_NoRefresh() + { + var requestMre = new ManualResetEventSlim(true); + var responseMre = new ManualResetEventSlim(true); + var currentTime = DateTimeOffset.UtcNow; + var callCount = 0; + + var credential = new TokenCredentialStub((r, c) => + { + callCount++; + responseMre.Wait(c); + requestMre.Set(); + + return new AccessToken(Guid.NewGuid().ToString(), currentTime.AddMinutes(2)); + }, IsAsync); + + var policy = new BearerTokenAuthenticationPolicy(credential, "scope"); + MockTransport transport = CreateMockTransport(new MockResponse(200), new MockResponse(200), new MockResponse(200), new MockResponse(200)); + + await SendGetRequest(transport, policy, uri: new Uri("https://example.com/1/Original")); + requestMre.Wait(); + responseMre.Reset(); + + await SendGetRequest(transport, policy, uri: new Uri("https://example.com/2/AlmostExpired")); + await SendGetRequest(transport, policy, uri: new Uri("https://example.com/3/AlmostExpired")); + await SendGetRequest(transport, policy, uri: new Uri("https://example.com/4/AlmostExpired")); + + requestMre.Reset(); + responseMre.Set(); + requestMre.Wait(); + + Assert.AreEqual(2, callCount); + + Assert.True(transport.Requests[0].Headers.TryGetValue("Authorization", out string auth1Value)); + Assert.True(transport.Requests[1].Headers.TryGetValue("Authorization", out string auth2Value)); + Assert.True(transport.Requests[2].Headers.TryGetValue("Authorization", out string auth3Value)); + Assert.True(transport.Requests[3].Headers.TryGetValue("Authorization", out string auth4Value)); + + Assert.AreEqual(auth1Value, auth2Value); + Assert.AreEqual(auth2Value, auth3Value); + Assert.AreEqual(auth3Value, auth4Value); + } + + [Test] + public async Task BearerTokenAuthenticationPolicy_TokenExpired() + { + var requestMre = new ManualResetEventSlim(true); + var responseMre = new ManualResetEventSlim(true); + var currentTime = DateTimeOffset.UtcNow; + var expires = new Queue(new[] { currentTime.AddSeconds(2), currentTime.AddMinutes(30) }); + var credential = new TokenCredentialStub((r, c) => + { + requestMre.Set(); + responseMre.Wait(c); + return new AccessToken(Guid.NewGuid().ToString(), expires.Dequeue()); + }, IsAsync); + + var policy = new BearerTokenAuthenticationPolicy(credential, new[] { "scope" }, TimeSpan.FromSeconds(2), TimeSpan.FromMilliseconds(50)); + MockTransport transport = CreateMockTransport(new MockResponse(200), new MockResponse(200), new MockResponse(200)); + + await SendGetRequest(transport, policy, uri: new Uri("https://example.com/0")); + Assert.True(transport.Requests[0].Headers.TryGetValue("Authorization", out string authValue)); + + await Task.Delay(3_000); + + requestMre.Reset(); + responseMre.Reset(); + + var firstRequestTask = SendGetRequest(transport, policy, uri: new Uri("https://example.com/1")); + var secondRequestTask = SendGetRequest(transport, policy, uri: new Uri("https://example.com/2")); + requestMre.Wait(); + await Task.Delay(1_000); + responseMre.Set(); + + await Task.WhenAll(firstRequestTask, secondRequestTask); + + Assert.True(transport.Requests[1].Headers.TryGetValue("Authorization", out string auth1Value)); + Assert.True(transport.Requests[2].Headers.TryGetValue("Authorization", out string auth2Value)); + + Assert.AreNotEqual(authValue, auth1Value); + Assert.AreEqual(auth1Value, auth2Value); + } + + [Test] + public void BearerTokenAuthenticationPolicy_OneHundredConcurrentCallsFailed() + { + var credential = new TokenCredentialStub((r, c) => + { + Thread.Sleep(100); + throw new InvalidOperationException("Error"); + }, IsAsync); + + var policy = new BearerTokenAuthenticationPolicy(credential, "scope"); + MockTransport transport = CreateMockTransport(r => new MockResponse(200)); + var requestTasks = new Task[100]; + + for (int i = 0; i < requestTasks.Length; i++) + { + requestTasks[i] = SendGetRequest(transport, policy, uri: new Uri("https://example.com")); + } + + Assert.CatchAsync(async () => await Task.WhenAll(requestTasks)); + + foreach (Task task in requestTasks) + { + Assert.IsTrue(task.IsFaulted); + } + } + + [Test] + public async Task BearerTokenAuthenticationPolicy_GatedConcurrentCallsFailed() + { + var requestMre = new ManualResetEventSlim(false); + var responseMre = new ManualResetEventSlim(false); + var credential = new TokenCredentialStub((r, c) => + { + requestMre.Set(); + responseMre.Wait(c); + throw new InvalidOperationException("Error"); + }, IsAsync); + + var policy = new BearerTokenAuthenticationPolicy(credential, "scope"); + MockTransport transport = CreateMockTransport(new MockResponse(200), new MockResponse(200)); + + var firstRequestTask = SendGetRequest(transport, policy, uri: new Uri("https://example.com")); + var secondRequestTask = SendGetRequest(transport, policy, uri: new Uri("https://example.com")); + + requestMre.Wait(); + await Task.Delay(1_000); + responseMre.Set(); + + Assert.CatchAsync(async () => await Task.WhenAll(firstRequestTask, secondRequestTask)); + + Assert.IsTrue(firstRequestTask.IsFaulted); + Assert.IsTrue(secondRequestTask.IsFaulted); + Assert.AreEqual(firstRequestTask.Exception.InnerException, secondRequestTask.Exception.InnerException); + } + + [Test] + public async Task BearerTokenAuthenticationPolicy_TokenExpiredThenFailed() + { + var requestMre = new ManualResetEventSlim(true); + var responseMre = new ManualResetEventSlim(true); + var fail = false; + var credential = new TokenCredentialStub((r, c) => + { + requestMre.Set(); + responseMre.Wait(c); + if (fail) + { + throw new InvalidOperationException("Error"); + } + + fail = true; + return new AccessToken(Guid.NewGuid().ToString(), DateTimeOffset.UtcNow.AddSeconds(2)); + }, IsAsync); + + var policy = new BearerTokenAuthenticationPolicy(credential, new[] { "scope" }, TimeSpan.FromSeconds(2), TimeSpan.FromMilliseconds(50)); + MockTransport transport = CreateMockTransport(new MockResponse(200), new MockResponse(200), new MockResponse(200)); + + await SendGetRequest(transport, policy, uri: new Uri("https://example.com/0")); + Assert.True(transport.Requests[0].Headers.TryGetValue("Authorization", out string _)); + + await Task.Delay(3_000); + + requestMre.Reset(); + responseMre.Reset(); + + var firstRequestTask = SendGetRequest(transport, policy, uri: new Uri("https://example.com")); + var secondRequestTask = SendGetRequest(transport, policy, uri: new Uri("https://example.com")); + + requestMre.Wait(); + await Task.Delay(1_000); + responseMre.Set(); + + Assert.CatchAsync(async () => await Task.WhenAll(firstRequestTask, secondRequestTask)); + + Assert.IsTrue(firstRequestTask.IsFaulted); + Assert.IsTrue(secondRequestTask.IsFaulted); + Assert.AreEqual(firstRequestTask.Exception.InnerException, secondRequestTask.Exception.InnerException); + } + + [Test] + [Ignore("https://github.com/Azure/azure-sdk-for-net/issues/14612")] + public async Task BearerTokenAuthenticationPolicy_TokenAlmostExpiredThenFailed() + { + var requestMre = new ManualResetEventSlim(true); + var responseMre = new ManualResetEventSlim(true); + var credentialMre = new ManualResetEventSlim(false); + + var getTokenRequestTimes = new List(); + var transportCallCount = 0; + var credential = new TokenCredentialStub((r, c) => + { + if (transportCallCount > 0) + { + credentialMre.Set(); + getTokenRequestTimes.Add(DateTimeOffset.UtcNow); + throw new InvalidOperationException("Error"); + } + + return new AccessToken(Guid.NewGuid().ToString(), DateTimeOffset.UtcNow.AddMinutes(1.5)); + }, IsAsync); + + var tokenRefreshRetryDelay = TimeSpan.FromSeconds(2); + var policy = new BearerTokenAuthenticationPolicy(credential, new[] { "scope" }, TimeSpan.FromMinutes(2), tokenRefreshRetryDelay); + MockTransport transport = CreateMockTransport(r => + { + requestMre.Set(); + responseMre.Wait(); + if (Interlocked.Increment(ref transportCallCount) == 4) + { + credentialMre.Wait(); + } + return new MockResponse(200); + }); + + await SendGetRequest(transport, policy, uri: new Uri("https://example.com/1")); + Assert.True(transport.Requests[0].Headers.TryGetValue("Authorization", out string auth1Value)); + + requestMre.Reset(); + responseMre.Reset(); + + Task requestTask1 = SendGetRequest(transport, policy, uri: new Uri("https://example.com/2/TokenFromCache/RefreshInBackground")); + Task requestTask2 = SendGetRequest(transport, policy, uri: new Uri("https://example.com/3/TokenFromCache/")); + + requestMre.Wait(); + responseMre.Set(); + + await Task.WhenAll(requestTask1, requestTask2); + + await SendGetRequest(transport, policy, uri: new Uri("https://example.com/4/TokenFromCache")); + + await Task.Delay((int)tokenRefreshRetryDelay.TotalMilliseconds + 1_000); + credentialMre.Reset(); + + await SendGetRequest(transport, policy, uri: new Uri("https://example.com/5/TokenFromCache/GetTokenFailed")); + credentialMre.Wait(); + + Assert.True(transport.Requests[1].Headers.TryGetValue("Authorization", out string auth2Value)); + Assert.True(transport.Requests[2].Headers.TryGetValue("Authorization", out string auth3Value)); + Assert.True(transport.Requests[3].Headers.TryGetValue("Authorization", out string auth4Value)); + Assert.True(transport.Requests[4].Headers.TryGetValue("Authorization", out string auth5Value)); + + Assert.AreEqual(auth1Value, auth2Value); + Assert.AreEqual(auth2Value, auth3Value); + Assert.AreEqual(auth3Value, auth4Value); + Assert.AreEqual(auth4Value, auth5Value); + + Assert.AreEqual(2, getTokenRequestTimes.Count); + Assert.True(getTokenRequestTimes[1] - getTokenRequestTimes[0] > tokenRefreshRetryDelay); + } + + [Test] + public void BearerTokenAuthenticationPolicy_GatedConcurrentCallsCancelled() + { + var requestMre = new ManualResetEventSlim(false); + var responseMre = new ManualResetEventSlim(false); + var cts = new CancellationTokenSource(); + var credential = new TokenCredentialStub((r, c) => + { + requestMre.Set(); + responseMre.Wait(c); + throw new InvalidOperationException("Error"); + }, IsAsync); + + var policy = new BearerTokenAuthenticationPolicy(credential, "scope"); + MockTransport transport = CreateMockTransport(new MockResponse(200), new MockResponse(200)); + + var firstRequestTask = SendGetRequest(transport, policy, uri: new Uri("https://example.com"), cancellationToken: default); + requestMre.Wait(); + + var secondRequestTask = SendGetRequest(transport, policy, uri: new Uri("https://example.com"), cancellationToken: cts.Token); + cts.Cancel(); + + Assert.CatchAsync(async () => await secondRequestTask); + responseMre.Set(); + + Assert.CatchAsync(async () => await firstRequestTask); + } + + private const string CaeInsufficientClaimsChallenge = "Bearer realm=\"\", authorization_uri=\"https://login.microsoftonline.com/common/oauth2/authorize\", client_id=\"00000003-0000-0000-c000-000000000000\", error=\"insufficient_claims\", claims=\"eyJhY2Nlc3NfdG9rZW4iOiB7ImZvbyI6ICJiYXIifX0=\""; + private const string CaeInsufficientClaimsChallengeValue = "eyJhY2Nlc3NfdG9rZW4iOiB7ImZvbyI6ICJiYXIifX0="; + private static readonly Challenge ParsedCaeInsufficientClaimsChallenge = new Challenge + { + Scheme = "Bearer", + Parameters = + { + ("realm", ""), + ("authorization_uri", "https://login.microsoftonline.com/common/oauth2/authorize"), + ("client_id", "00000003-0000-0000-c000-000000000000"), + ("error", "insufficient_claims"), + ("claims", "eyJhY2Nlc3NfdG9rZW4iOiB7ImZvbyI6ICJiYXIifX0="), + } + }; + + private const string CaeSessionsRevokedClaimsChallenge = "Bearer authorization_uri=\"https://login.windows-ppe.net/\", error=\"invalid_token\", error_description=\"User session has been revoked\", claims=\"eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwgInZhbHVlIjoiMTYwMzc0MjgwMCJ9fX0=\""; + private const string CaeSessionsRevokedClaimsChallengeValue = "eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwgInZhbHVlIjoiMTYwMzc0MjgwMCJ9fX0="; + private static readonly Challenge ParsedCaeSessionsRevokedClaimsChallenge = new Challenge + { + Scheme = "Bearer", + Parameters = + { + ("authorization_uri", "https://login.windows-ppe.net/"), + ("error", "invalid_token"), + ("error_description", "User session has been revoked"), + ("claims", "eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwgInZhbHVlIjoiMTYwMzc0MjgwMCJ9fX0="), + } + }; + + private const string KeyVaultChallenge = "Bearer authorization=\"https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47\", resource=\"https://vault.azure.net\""; + private static readonly Challenge ParsedKeyVaultChallenge = new Challenge + { + Scheme = "Bearer", + Parameters = + { + ("authorization", "https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47"), + ("resource", "https://vault.azure.net"), + } + }; + + private const string ArmChallenge = "Bearer authorization_uri=\"https://login.windows.net/\", error=\"invalid_token\", error_description=\"The authentication failed because of missing 'Authorization' header.\""; + private static readonly Challenge ParsedArmChallenge = new Challenge() + { + Scheme = "Bearer", + Parameters = + { + ("authorization_uri", "https://login.windows.net/"), + ("error", "invalid_token"), + ("error_description", "The authentication failed because of missing 'Authorization' header."), + } + }; + + private static readonly Dictionary ChallengeStrings = new Dictionary() + { + { "CaeInsufficientClaims", CaeInsufficientClaimsChallenge }, + { "CaeSessionsRevoked", CaeSessionsRevokedClaimsChallenge }, + { "KeyVault", KeyVaultChallenge }, + { "Arm", ArmChallenge } + }; + + private static readonly Dictionary ParsedChallenges = new Dictionary() + { + { "CaeInsufficientClaims", ParsedCaeInsufficientClaimsChallenge }, + { "CaeSessionsRevoked", ParsedCaeSessionsRevokedClaimsChallenge }, + { "KeyVault", ParsedKeyVaultChallenge }, + { "Arm", ParsedArmChallenge } + }; + + private class Challenge + { + public string Scheme { get; set; } + + public List<(string, string)> Parameters { get; } = new List<(string, string)>(); + } + + [Test] + public void BearerTokenAuthenticationPolicy_ValidateChallengeParsing([Values("CaeInsufficientClaims", "CaeSessionsRevoked", "KeyVault", "Arm")]string challengeKey) + { + var challenge = ChallengeStrings[challengeKey]; + + List parsedChallenges = new List(); + + foreach (var challengeTuple in BearerTokenAuthenticationPolicy.ParseChallenges(challenge)) + { + Challenge parsedChallenge = new Challenge(); + + parsedChallenge.Scheme = challengeTuple.Item1; + + foreach (var paramTuple in BearerTokenAuthenticationPolicy.ParseChallengeParameters(challengeTuple.Item2)) + { + parsedChallenge.Parameters.Add(paramTuple); + } + + parsedChallenges.Add(parsedChallenge); + } + + Assert.AreEqual(1, parsedChallenges.Count); + + ValidateParsedChallenge(ParsedChallenges[challengeKey], parsedChallenges[0]); + } + + [Test] + public async Task BearerTokenAuthenticationPolicy_ValidateClaimsChallengeTokenRequest() + { + string currentClaimChallenge = null; + + int tokensRequested = 0; + + var credential = new TokenCredentialStub((r, c) => + { + tokensRequested++; + + Assert.AreEqual(currentClaimChallenge, r.ClaimsChallenge); + + return new AccessToken(Guid.NewGuid().ToString(), DateTimeOffset.UtcNow + TimeSpan.FromDays(1)); + }, IsAsync); + + var policy = new BearerTokenAuthenticationPolicy(credential, "scope"); + + var insufficientClaimsChallengeResponse = new MockResponse(401); + + insufficientClaimsChallengeResponse.AddHeader(new HttpHeader("WWW-Authenticate", CaeInsufficientClaimsChallenge)); + + var sessionRevokedChallengeResponse = new MockResponse(401); + + sessionRevokedChallengeResponse.AddHeader(new HttpHeader("WWW-Authenticate", CaeSessionsRevokedClaimsChallenge)); + + var armChallengeResponse = new MockResponse(401); + + armChallengeResponse.AddHeader(new HttpHeader("WWW-Authenticate", ArmChallenge)); + + var keyvaultChallengeResponse = new MockResponse(401); + + keyvaultChallengeResponse.AddHeader(new HttpHeader("WWW-Authenticate", KeyVaultChallenge)); + + MockTransport transport = CreateMockTransport(new MockResponse(200), + insufficientClaimsChallengeResponse, + new MockResponse(200), + sessionRevokedChallengeResponse, + new MockResponse(200), + armChallengeResponse, + keyvaultChallengeResponse); + + var response = await SendGetRequest(transport, policy, uri: new Uri("https://example.com"), cancellationToken: default); + + Assert.AreEqual(tokensRequested, 1); + + Assert.AreEqual(response.Status, 200); + + currentClaimChallenge = Base64Url.DecodeString(CaeInsufficientClaimsChallengeValue); + + response = await SendGetRequest(transport, policy, uri: new Uri("https://example.com"), cancellationToken: default); + + Assert.AreEqual(tokensRequested, 2); + + Assert.AreEqual(response.Status, 200); + + currentClaimChallenge = Base64Url.DecodeString(CaeSessionsRevokedClaimsChallengeValue); + + response = await SendGetRequest(transport, policy, uri: new Uri("https://example.com"), cancellationToken: default); + + Assert.AreEqual(tokensRequested, 3); + + Assert.AreEqual(response.Status, 200); + + currentClaimChallenge = null; + + response = await SendGetRequest(transport, policy, uri: new Uri("https://example.com"), cancellationToken: default); + + Assert.AreEqual(tokensRequested, 3); + + Assert.AreEqual(response.Status, 401); + + response = await SendGetRequest(transport, policy, uri: new Uri("https://example.com"), cancellationToken: default); + + Assert.AreEqual(tokensRequested, 3); + + Assert.AreEqual(response.Status, 401); + } + + private void ValidateParsedChallenge(Challenge expected, Challenge actual) + { + Assert.AreEqual(expected.Scheme, actual.Scheme); + + CollectionAssert.AreEquivalent(expected.Parameters, actual.Parameters); + } + + private class TokenCredentialStub : Experimental.TokenCredential + { + public TokenCredentialStub(Func handler, bool isAsync) + { + if (isAsync) + { +#pragma warning disable 1998 + _getTokenAsyncHandler = async (r, c) => handler(r, c); +#pragma warning restore 1998 + } + else + { + _getTokenHandler = handler; + } + } + + private readonly Func> _getTokenAsyncHandler; + private readonly Func _getTokenHandler; + + public override ValueTask GetTokenAsync(Experimental.TokenRequestContext requestContext, CancellationToken cancellationToken) + => _getTokenAsyncHandler(requestContext, cancellationToken); + + public override AccessToken GetToken(Experimental.TokenRequestContext requestContext, CancellationToken cancellationToken) + => _getTokenHandler(requestContext, cancellationToken); + } + } +} diff --git a/sdk/core/Azure.Core/src/Shared/base64.cs b/sdk/core/Azure.Core/src/Shared/base64.cs new file mode 100644 index 0000000000000..fe2db64a645d9 --- /dev/null +++ b/sdk/core/Azure.Core/src/Shared/base64.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Text; + +namespace Azure.Core +{ + internal static class Base64Url + { + /// Converts a Base64URL encoded string to a string. + /// The Base64Url encoded string containing UTF8 bytes for a string. + /// The string represented by the Base64URL encoded string. + public static byte[] Decode(string encoded) + { + encoded = new StringBuilder(encoded).Replace('-', '+').Replace('_', '/').Append('=', (encoded.Length % 4 == 0) ? 0 : 4 - (encoded.Length % 4)).ToString(); + + return Convert.FromBase64String(encoded); + } + + /// Encode a byte array as a Base64URL encoded string. + /// Raw byte input buffer. + /// The bytes, encoded as a Base64URL string. + public static string Encode(byte[] bytes) + { + return new StringBuilder(Convert.ToBase64String(bytes)).Replace('+', '-').Replace('/', '_').Replace("=", "").ToString(); + } + + /// Converts a Base64URL encoded string to a string. + /// The Base64Url encoded string containing UTF8 bytes for a string. + /// The string represented by the Base64URL encoded string. + internal static string DecodeString(string encoded) + { + return UTF8Encoding.UTF8.GetString(Decode(encoded)); + } + + /// Encode a string as a Base64URL encoded string. + /// String input buffer. + /// The UTF8 bytes for the string, encoded as a Base64URL string. + internal static string EncodeString(string value) + { + return Encode(UTF8Encoding.UTF8.GetBytes(value)); + } + + public static string HexToBase64Url(string hex) + { + byte[] bytes = new byte[hex.Length / 2]; + + for (int i = 0; i < hex.Length; i += 2) + bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16); + + return Base64Url.Encode(bytes); + } + } +} From 21dd82d187d722e4182f7a134027e18d3e43960f Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Thu, 21 Jan 2021 10:19:24 -0600 Subject: [PATCH 02/22] wip --- .../BearerTokenAuthenticationPolicy.cs | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/sdk/core/Azure.Core.Experimental/src/pipeline/BearerTokenAuthenticationPolicy.cs b/sdk/core/Azure.Core.Experimental/src/pipeline/BearerTokenAuthenticationPolicy.cs index 333adb285fbfa..a5da953884e31 100644 --- a/sdk/core/Azure.Core.Experimental/src/pipeline/BearerTokenAuthenticationPolicy.cs +++ b/sdk/core/Azure.Core.Experimental/src/pipeline/BearerTokenAuthenticationPolicy.cs @@ -15,7 +15,7 @@ namespace Azure.Core.Experimental.Pipeline /// /// A policy that sends an provided by a as an Authentication header. /// - public class BearerTokenAuthenticationPolicy : Core.Pipeline.HttpPipelinePolicy + public class BearerTokenAuthenticationPolicy : HttpPipelinePolicy { private const string AuthenticationChallengePattern = @"(\w+) ((?:\w+="".*?""(?:, )?)+)(?:, )?"; private const string ChallengeParameterPattern = @"(?:(\w+)=""([^""]*)"")+"; @@ -31,17 +31,17 @@ public class BearerTokenAuthenticationPolicy : Core.Pipeline.HttpPipelinePolicy /// /// The token credential to use for authentication. /// The scope to authenticate for. - public BearerTokenAuthenticationPolicy(Experimental.TokenCredential credential, string scope) : this(credential, new[] { scope }) { } + public BearerTokenAuthenticationPolicy(TokenCredential credential, string scope) : this(credential, new[] { scope }) { } /// /// Creates a new instance of using provided token credential and scopes to authenticate for. /// /// The token credential to use for authentication. /// Scopes to authenticate for. - public BearerTokenAuthenticationPolicy(Experimental.TokenCredential credential, IEnumerable scopes) + public BearerTokenAuthenticationPolicy(TokenCredential credential, IEnumerable scopes) : this(credential, scopes, TimeSpan.FromMinutes(5), TimeSpan.FromSeconds(30)) { } - internal BearerTokenAuthenticationPolicy(Experimental.TokenCredential credential, IEnumerable scopes, TimeSpan tokenRefreshOffset, TimeSpan tokenRefreshRetryDelay) + internal BearerTokenAuthenticationPolicy(TokenCredential credential, IEnumerable scopes, TimeSpan tokenRefreshOffset, TimeSpan tokenRefreshRetryDelay) { Argument.AssertNotNull(credential, nameof(credential)); Argument.AssertNotNull(scopes, nameof(scopes)); @@ -69,7 +69,7 @@ private async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemorySpecifies if the method is being called in an asynchronous context protected virtual async Task OnBeforeRequestAsync(HttpMessage message, bool async) { - await AuthenticateRequestAsync(message, new Experimental.TokenRequestContext(_scopes, message.Request.ClientRequestId), async).ConfigureAwait(false); + await AuthenticateRequestAsync(message, new TokenRequestContext(_scopes, message.Request.ClientRequestId), async).ConfigureAwait(false); } /// @@ -131,15 +131,15 @@ protected virtual async Task OnBeforeRequestAsync(HttpMessage message, bool asyn /// /// This implementation handles common authentication challenges such as claims challenges. Service client libraries may derive from this and extend to handle service specific authentication challenges. /// The HttpMessage to be authenticated. - /// Specifies if the method is being called in an asynchronous context - /// A boolean indicated whether the request should be retried + /// Specifies if the method is being called in an asynchronous context. + /// A boolean indicated whether the request should be retried. protected virtual async Task OnChallengeAsync(HttpMessage message, bool async) { var claimsChallenge = GetClaimsChallenge(message.Response); if (claimsChallenge != null) { - await AuthenticateRequestAsync(message, new Experimental.TokenRequestContext(_scopes, message.Request.ClientRequestId, claimsChallenge), async).ConfigureAwait(false); + await AuthenticateRequestAsync(message, new TokenRequestContext(_scopes, message.Request.ClientRequestId, claimsChallenge), async).ConfigureAwait(false); return true; } @@ -172,7 +172,7 @@ protected virtual async Task OnChallengeAsync(HttpMessage message, bool as return null; } - private async Task AuthenticateRequestAsync(HttpMessage message, Experimental.TokenRequestContext context, bool async) + private async Task AuthenticateRequestAsync(HttpMessage message, TokenRequestContext context, bool async) { string headerValue; if (async) @@ -191,22 +191,22 @@ private async Task AuthenticateRequestAsync(HttpMessage message, Experimental.To private class AccessTokenCache { private readonly object _syncObj = new object(); - private readonly Experimental.TokenCredential _credential; + private readonly TokenCredential _credential; private readonly TimeSpan _tokenRefreshOffset; private readonly TimeSpan _tokenRefreshRetryDelay; - private Experimental.TokenRequestContext? _currentContext; + private TokenRequestContext? _currentContext; private TaskCompletionSource? _infoTcs; private TaskCompletionSource? _backgroundUpdateTcs; - public AccessTokenCache(Experimental.TokenCredential credential, TimeSpan tokenRefreshOffset, TimeSpan tokenRefreshRetryDelay, string[] initialScopes) + public AccessTokenCache(TokenCredential credential, TimeSpan tokenRefreshOffset, TimeSpan tokenRefreshRetryDelay, string[] initialScopes) { _credential = credential; _tokenRefreshOffset = tokenRefreshOffset; _tokenRefreshRetryDelay = tokenRefreshRetryDelay; - _currentContext = new Experimental.TokenRequestContext(initialScopes); + _currentContext = new TokenRequestContext(initialScopes); } - public async ValueTask GetHeaderValueAsync(HttpMessage message, Experimental.TokenRequestContext context, bool async) + public async ValueTask GetHeaderValueAsync(HttpMessage message, TokenRequestContext context, bool async) { bool getTokenFromCredential; TaskCompletionSource headerValueTcs; @@ -279,7 +279,7 @@ public async ValueTask GetHeaderValueAsync(HttpMessage message, Experime return info.HeaderValue; } - private (TaskCompletionSource tcs, TaskCompletionSource? backgroundUpdateTcs, bool getTokenFromCredential) GetTaskCompletionSources(Experimental.TokenRequestContext context) + private (TaskCompletionSource tcs, TaskCompletionSource? backgroundUpdateTcs, bool getTokenFromCredential) GetTaskCompletionSources(TokenRequestContext context) { lock (_syncObj) { @@ -327,7 +327,7 @@ public async ValueTask GetHeaderValueAsync(HttpMessage message, Experime } // must be called under lock (_syncObj) - private bool RequestRequiresNewToken(Experimental.TokenRequestContext context) + private bool RequestRequiresNewToken(TokenRequestContext context) { if (_currentContext == null) { @@ -353,7 +353,7 @@ private bool RequestRequiresNewToken(Experimental.TokenRequestContext context) return false; } - private async ValueTask GetHeaderValueFromCredentialInBackgroundAsync(TaskCompletionSource backgroundUpdateTcs, HeaderValueInfo info, Experimental.TokenRequestContext context, bool async) + private async ValueTask GetHeaderValueFromCredentialInBackgroundAsync(TaskCompletionSource backgroundUpdateTcs, HeaderValueInfo info, TokenRequestContext context, bool async) { var cts = new CancellationTokenSource(_tokenRefreshRetryDelay); try @@ -377,7 +377,7 @@ private async ValueTask GetHeaderValueFromCredentialInBackgroundAsync(TaskComple } } - private async ValueTask GetHeaderValueFromCredentialAsync(Experimental.TokenRequestContext context, bool async, CancellationToken cancellationToken) + private async ValueTask GetHeaderValueFromCredentialAsync(TokenRequestContext context, bool async, CancellationToken cancellationToken) { AccessToken token = async ? await _credential.GetTokenAsync(context, cancellationToken).ConfigureAwait(false) From 27aa4439197e54f589cdf91272acc12456df3e3e Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Mon, 25 Jan 2021 08:47:52 -0600 Subject: [PATCH 03/22] Factor BearerTokenAuthPolicy --- .../src/TokenCredential.cs | 31 ------ .../src/TokenRequestContext.cs | 51 ---------- sdk/core/Azure.Core/src/Azure.Core.csproj | 1 + .../src/Shared/AzureCoreSharedEventSource.cs} | 8 +- ...arerTokenChallengeAuthenticationPolicy.cs} | 44 +++++---- .../Azure.Core/src/TokenRequestContext.cs | 5 + .../Azure.Core/tests/Azure.Core.Tests.csproj | 1 + ...okenChallengeAuthenticationPolicyTests.cs} | 94 +++++++++---------- 8 files changed, 85 insertions(+), 150 deletions(-) delete mode 100644 sdk/core/Azure.Core.Experimental/src/TokenCredential.cs delete mode 100644 sdk/core/Azure.Core.Experimental/src/TokenRequestContext.cs rename sdk/core/{Azure.Core.Experimental/src/diagnostics/AzureCoreExperimentalEventSource.cs => Azure.Core/src/Shared/AzureCoreSharedEventSource.cs} (58%) rename sdk/core/{Azure.Core.Experimental/src/pipeline/BearerTokenAuthenticationPolicy.cs => Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs} (89%) rename sdk/core/{Azure.Core.Experimental/tests/BearerTokenAuthenticationPolicyTests.cs => Azure.Core/tests/BearerTokenChallengeAuthenticationPolicyTests.cs} (85%) diff --git a/sdk/core/Azure.Core.Experimental/src/TokenCredential.cs b/sdk/core/Azure.Core.Experimental/src/TokenCredential.cs deleted file mode 100644 index b0f0c91e16f27..0000000000000 --- a/sdk/core/Azure.Core.Experimental/src/TokenCredential.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Azure.Core.Experimental -{ - /// - /// Represents a credential capable of providing an OAuth token. - /// - public abstract class TokenCredential - { - /// - /// Gets an for the specified set of scopes. - /// - /// The with authentication information. - /// The to use. - /// A valid . - public abstract ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken); - - /// - /// Gets an for the specified set of scopes. - /// - /// The with authentication information. - /// The to use. - /// A valid . - public abstract AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken); - } -} diff --git a/sdk/core/Azure.Core.Experimental/src/TokenRequestContext.cs b/sdk/core/Azure.Core.Experimental/src/TokenRequestContext.cs deleted file mode 100644 index bd9229378ff31..0000000000000 --- a/sdk/core/Azure.Core.Experimental/src/TokenRequestContext.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Azure.Core.Experimental -{ - /// - /// Contains the details of an authentication token request. - /// - public readonly struct TokenRequestContext - { - /// - /// Creates a new TokenRequest with the specified scopes. - /// - /// The scopes required for the token. - /// The of the request requiring a token for authentication, if applicable. - public TokenRequestContext(string[] scopes, string? parentRequestId) - { - Scopes = scopes; - ParentRequestId = parentRequestId; - ClaimsChallenge = default; - } - - /// - /// Creates a new TokenRequest with the specified scopes. - /// - /// The scopes required for the token. - /// The of the request requiring a token for authentication, if applicable. - /// A claims challenge returned from a failed authentication or authorization request. - public TokenRequestContext(string[] scopes, string? parentRequestId = default, string? claimsChallenge = default) - { - Scopes = scopes; - ClaimsChallenge = claimsChallenge; - ParentRequestId = parentRequestId; - } - - /// - /// The scopes required for the token. - /// - public string[] Scopes { get; } - - /// - /// A claims challenge returned from a failed authentication or authorization request. - /// - public string? ClaimsChallenge { get; } - - /// - /// The of the request requiring a token for authentication, if applicable. - /// - public string? ParentRequestId { get; } - } -} diff --git a/sdk/core/Azure.Core/src/Azure.Core.csproj b/sdk/core/Azure.Core/src/Azure.Core.csproj index 6125e5d33ea21..25fa99c96dade 100644 --- a/sdk/core/Azure.Core/src/Azure.Core.csproj +++ b/sdk/core/Azure.Core/src/Azure.Core.csproj @@ -26,6 +26,7 @@ + diff --git a/sdk/core/Azure.Core.Experimental/src/diagnostics/AzureCoreExperimentalEventSource.cs b/sdk/core/Azure.Core/src/Shared/AzureCoreSharedEventSource.cs similarity index 58% rename from sdk/core/Azure.Core.Experimental/src/diagnostics/AzureCoreExperimentalEventSource.cs rename to sdk/core/Azure.Core/src/Shared/AzureCoreSharedEventSource.cs index 2235c0cf3774d..510d2948e70cd 100644 --- a/sdk/core/Azure.Core.Experimental/src/diagnostics/AzureCoreExperimentalEventSource.cs +++ b/sdk/core/Azure.Core/src/Shared/AzureCoreSharedEventSource.cs @@ -6,15 +6,15 @@ namespace Azure.Core.Diagnostics { [EventSource(Name = EventSourceName)] - internal sealed class AzureCoreExperimentalEventSource : EventSource + internal sealed class AzureCoreSharedEventSource : EventSource { - private const string EventSourceName = "Azure-Core-Experimental"; + private const string EventSourceName = "Azure-Core-Shared"; private const int BackgroundRefreshFailedEvent = 19; - private AzureCoreExperimentalEventSource() : base(EventSourceName, EventSourceSettings.Default, AzureEventSourceListener.TraitName, AzureEventSourceListener.TraitValue) { } + private AzureCoreSharedEventSource() : base(EventSourceName, EventSourceSettings.Default, AzureEventSourceListener.TraitName, AzureEventSourceListener.TraitValue) { } - public static AzureCoreExperimentalEventSource Singleton { get; } = new AzureCoreExperimentalEventSource(); + public static AzureCoreSharedEventSource Singleton { get; } = new AzureCoreSharedEventSource(); [Event(BackgroundRefreshFailedEvent, Level = EventLevel.Informational, Message = "Background token refresh [{0}] failed with exception {1}")] public void BackgroundRefreshFailed(string requestId, string exception) diff --git a/sdk/core/Azure.Core.Experimental/src/pipeline/BearerTokenAuthenticationPolicy.cs b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs similarity index 89% rename from sdk/core/Azure.Core.Experimental/src/pipeline/BearerTokenAuthenticationPolicy.cs rename to sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs index a5da953884e31..34e97a6668667 100644 --- a/sdk/core/Azure.Core.Experimental/src/pipeline/BearerTokenAuthenticationPolicy.cs +++ b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs @@ -8,14 +8,13 @@ using System.Threading; using System.Threading.Tasks; using Azure.Core.Diagnostics; -using Azure.Core.Pipeline; -namespace Azure.Core.Experimental.Pipeline +namespace Azure.Core.Pipeline { /// /// A policy that sends an provided by a as an Authentication header. /// - public class BearerTokenAuthenticationPolicy : HttpPipelinePolicy + internal class BearerTokenChallengeAuthenticationPolicy : HttpPipelinePolicy { private const string AuthenticationChallengePattern = @"(\w+) ((?:\w+="".*?""(?:, )?)+)(?:, )?"; private const string ChallengeParameterPattern = @"(?:(\w+)=""([^""]*)"")+"; @@ -24,24 +23,35 @@ public class BearerTokenAuthenticationPolicy : HttpPipelinePolicy private static readonly Regex s_ChallengeParameterRegex = new Regex(ChallengeParameterPattern, RegexOptions.Compiled); private readonly AccessTokenCache _accessTokenCache; - private readonly string[] _scopes; + private string[] _scopes; /// - /// Creates a new instance of using provided token credential and scope to authenticate for. + /// Sets the current scopes for the policy. + /// + /// The scopes. + protected void SetScopes(string[] scopes) + { + Argument.AssertNotNull(scopes, nameof(scopes)); + + _scopes = scopes; + } + + /// + /// Creates a new instance of using provided token credential and scope to authenticate for. /// /// The token credential to use for authentication. /// The scope to authenticate for. - public BearerTokenAuthenticationPolicy(TokenCredential credential, string scope) : this(credential, new[] { scope }) { } + public BearerTokenChallengeAuthenticationPolicy(TokenCredential credential, string scope) : this(credential, new[] { scope }) { } /// - /// Creates a new instance of using provided token credential and scopes to authenticate for. + /// Creates a new instance of using provided token credential and scopes to authenticate for. /// /// The token credential to use for authentication. /// Scopes to authenticate for. - public BearerTokenAuthenticationPolicy(TokenCredential credential, IEnumerable scopes) + public BearerTokenChallengeAuthenticationPolicy(TokenCredential credential, IEnumerable scopes) : this(credential, scopes, TimeSpan.FromMinutes(5), TimeSpan.FromSeconds(30)) { } - internal BearerTokenAuthenticationPolicy(TokenCredential credential, IEnumerable scopes, TimeSpan tokenRefreshOffset, TimeSpan tokenRefreshRetryDelay) + internal BearerTokenChallengeAuthenticationPolicy(TokenCredential credential, IEnumerable scopes, TimeSpan tokenRefreshOffset, TimeSpan tokenRefreshRetryDelay) { Argument.AssertNotNull(credential, nameof(credential)); Argument.AssertNotNull(scopes, nameof(scopes)); @@ -147,9 +157,9 @@ protected virtual async Task OnChallengeAsync(HttpMessage message, bool as return false; } - private static string? GetClaimsChallenge(Response response) + private static string GetClaimsChallenge(Response response) { - if (response.Status == 401 && response.Headers.TryGetValue("WWW-Authenticate", out string? headerValue)) + if (response.Status == 401 && response.Headers.TryGetValue("WWW-Authenticate", out string headerValue)) { foreach (var challenge in ParseChallenges(headerValue)) { @@ -196,8 +206,8 @@ private class AccessTokenCache private readonly TimeSpan _tokenRefreshRetryDelay; private TokenRequestContext? _currentContext; - private TaskCompletionSource? _infoTcs; - private TaskCompletionSource? _backgroundUpdateTcs; + private TaskCompletionSource _infoTcs; + private TaskCompletionSource _backgroundUpdateTcs; public AccessTokenCache(TokenCredential credential, TimeSpan tokenRefreshOffset, TimeSpan tokenRefreshRetryDelay, string[] initialScopes) { _credential = credential; @@ -210,7 +220,7 @@ public async ValueTask GetHeaderValueAsync(HttpMessage message, TokenReq { bool getTokenFromCredential; TaskCompletionSource headerValueTcs; - TaskCompletionSource? backgroundUpdateTcs; + TaskCompletionSource backgroundUpdateTcs; (headerValueTcs, backgroundUpdateTcs, getTokenFromCredential) = GetTaskCompletionSources(context); HeaderValueInfo info; @@ -279,7 +289,7 @@ public async ValueTask GetHeaderValueAsync(HttpMessage message, TokenReq return info.HeaderValue; } - private (TaskCompletionSource tcs, TaskCompletionSource? backgroundUpdateTcs, bool getTokenFromCredential) GetTaskCompletionSources(TokenRequestContext context) + private (TaskCompletionSource tcs, TaskCompletionSource backgroundUpdateTcs, bool getTokenFromCredential) GetTaskCompletionSources(TokenRequestContext context) { lock (_syncObj) { @@ -364,12 +374,12 @@ private async ValueTask GetHeaderValueFromCredentialInBackgroundAsync(TaskComple catch (OperationCanceledException oce) when (cts.IsCancellationRequested) { backgroundUpdateTcs.SetResult(new HeaderValueInfo(info.HeaderValue, info.ExpiresOn, DateTimeOffset.UtcNow)); - AzureCoreExperimentalEventSource.Singleton.BackgroundRefreshFailed(context.ParentRequestId ?? string.Empty, oce.ToString()); + AzureCoreSharedEventSource.Singleton.BackgroundRefreshFailed(context.ParentRequestId ?? string.Empty, oce.ToString()); } catch (Exception e) { backgroundUpdateTcs.SetResult(new HeaderValueInfo(info.HeaderValue, info.ExpiresOn, DateTimeOffset.UtcNow + _tokenRefreshRetryDelay)); - AzureCoreExperimentalEventSource.Singleton.BackgroundRefreshFailed(context.ParentRequestId ?? string.Empty, e.ToString()); + AzureCoreSharedEventSource.Singleton.BackgroundRefreshFailed(context.ParentRequestId ?? string.Empty, e.ToString()); } finally { diff --git a/sdk/core/Azure.Core/src/TokenRequestContext.cs b/sdk/core/Azure.Core/src/TokenRequestContext.cs index fe4237f61b34d..c9fbf4de477a1 100644 --- a/sdk/core/Azure.Core/src/TokenRequestContext.cs +++ b/sdk/core/Azure.Core/src/TokenRequestContext.cs @@ -38,6 +38,11 @@ public TokenRequestContext(string[] scopes, string? parentRequestId = default, s /// public string[] Scopes { get; } + /// + /// A claims challenge returned from a failed authentication or authorization request. + /// + public string ClaimsChallenge { get; } + /// /// The of the request requiring a token for authentication, if applicable. /// diff --git a/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj b/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj index 19c3058fc5cb8..518e7abdb64d6 100644 --- a/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj +++ b/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj @@ -33,6 +33,7 @@ + diff --git a/sdk/core/Azure.Core.Experimental/tests/BearerTokenAuthenticationPolicyTests.cs b/sdk/core/Azure.Core/tests/BearerTokenChallengeAuthenticationPolicyTests.cs similarity index 85% rename from sdk/core/Azure.Core.Experimental/tests/BearerTokenAuthenticationPolicyTests.cs rename to sdk/core/Azure.Core/tests/BearerTokenChallengeAuthenticationPolicyTests.cs index eeff9c56dd565..31875ef9e8e03 100644 --- a/sdk/core/Azure.Core.Experimental/tests/BearerTokenAuthenticationPolicyTests.cs +++ b/sdk/core/Azure.Core/tests/BearerTokenChallengeAuthenticationPolicyTests.cs @@ -7,23 +7,23 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Azure.Core.Pipeline; using Azure.Core.TestFramework; using Moq; using NUnit.Framework; -using Azure.Core.Experimental.Pipeline; namespace Azure.Core.Tests { - public class BearerTokenAuthenticationPolicyTests : SyncAsyncPolicyTestBase + public class BearerTokenChallengeAuthenticationPolicyTests : SyncAsyncPolicyTestBase { - public BearerTokenAuthenticationPolicyTests(bool isAsync) : base(isAsync) { } + public BearerTokenChallengeAuthenticationPolicyTests(bool isAsync) : base(isAsync) { } [Test] - public async Task BearerTokenAuthenticationPolicy_UsesTokenProvidedByCredentials() + public async Task BearerTokenChallengeAuthenticationPolicy_UsesTokenProvidedByCredentials() { var credential = new TokenCredentialStub( (r, c) => r.Scopes.SequenceEqual(new[] { "scope1", "scope2" }) ? new AccessToken("token", DateTimeOffset.MaxValue) : default, IsAsync); - var policy = new BearerTokenAuthenticationPolicy(credential, new[] { "scope1", "scope2" }); + var policy = new BearerTokenChallengeAuthenticationPolicy(credential, new[] { "scope1", "scope2" }); MockTransport transport = CreateMockTransport(new MockResponse(200)); await SendGetRequest(transport, policy, uri: new Uri("https://example.com")); @@ -33,7 +33,7 @@ public async Task BearerTokenAuthenticationPolicy_UsesTokenProvidedByCredentials } [Test] - public async Task BearerTokenAuthenticationPolicy_RequestsTokenEveryRequest() + public async Task BearerTokenChallengeAuthenticationPolicy_RequestsTokenEveryRequest() { var accessTokens = new Queue(); accessTokens.Enqueue(new AccessToken("token1", DateTimeOffset.UtcNow)); @@ -42,7 +42,7 @@ public async Task BearerTokenAuthenticationPolicy_RequestsTokenEveryRequest() var credential = new TokenCredentialStub( (r, c) => r.Scopes.SequenceEqual(new[] { "scope1", "scope2" }) ? accessTokens.Dequeue() : default, IsAsync); - var policy = new BearerTokenAuthenticationPolicy(credential, new[] { "scope1", "scope2" }); + var policy = new BearerTokenChallengeAuthenticationPolicy(credential, new[] { "scope1", "scope2" }); MockTransport transport = CreateMockTransport(new MockResponse(200), new MockResponse(200)); await SendGetRequest(transport, policy, uri: new Uri("https://example.com")); @@ -56,12 +56,12 @@ public async Task BearerTokenAuthenticationPolicy_RequestsTokenEveryRequest() } [Test] - public async Task BearerTokenAuthenticationPolicy_CachesHeaderValue() + public async Task BearerTokenChallengeAuthenticationPolicy_CachesHeaderValue() { var credential = new TokenCredentialStub( (r, c) => r.Scopes.SequenceEqual(new[] { "scope" }) ? new AccessToken("token", DateTimeOffset.MaxValue) : default, IsAsync); - var policy = new BearerTokenAuthenticationPolicy(credential, "scope"); + var policy = new BearerTokenChallengeAuthenticationPolicy(credential, "scope"); MockTransport transport = CreateMockTransport(new MockResponse(200), new MockResponse(200)); await SendGetRequest(transport, policy, uri: new Uri("https://example.com")); @@ -75,30 +75,30 @@ public async Task BearerTokenAuthenticationPolicy_CachesHeaderValue() } [Test] - public void BearerTokenAuthenticationPolicy_ThrowsForNonTlsEndpoint() + public void BearerTokenChallengeAuthenticationPolicy_ThrowsForNonTlsEndpoint() { var credential = new TokenCredentialStub( (r, c) => r.Scopes.SequenceEqual(new[] { "scope" }) ? new AccessToken("token", DateTimeOffset.MaxValue) : default, IsAsync); - var policy = new BearerTokenAuthenticationPolicy(credential, "scope"); + var policy = new BearerTokenChallengeAuthenticationPolicy(credential, "scope"); MockTransport transport = CreateMockTransport(); Assert.ThrowsAsync(async () => await SendGetRequest(transport, policy, uri: new Uri("http://example.com"))); } [Test] - public void BearerTokenAuthenticationPolicy_ThrowsForEmptyToken() + public void BearerTokenChallengeAuthenticationPolicy_ThrowsForEmptyToken() { var credential = new TokenCredentialStub((r, c) => new AccessToken(string.Empty, DateTimeOffset.MaxValue), IsAsync); - var policy = new BearerTokenAuthenticationPolicy(credential, "scope"); + var policy = new BearerTokenChallengeAuthenticationPolicy(credential, "scope"); MockTransport transport = CreateMockTransport(); Assert.ThrowsAsync(async () => await SendGetRequest(transport, policy, uri: new Uri("http://example.com"))); } [Test] - public async Task BearerTokenAuthenticationPolicy_OneHundredConcurrentCalls() + public async Task BearerTokenChallengeAuthenticationPolicy_OneHundredConcurrentCalls() { var credential = new TokenCredentialStub((r, c) => { @@ -106,7 +106,7 @@ public async Task BearerTokenAuthenticationPolicy_OneHundredConcurrentCalls() return new AccessToken(Guid.NewGuid().ToString(), DateTimeOffset.UtcNow.AddMinutes(30)); }, IsAsync); - var policy = new BearerTokenAuthenticationPolicy(credential, "scope"); + var policy = new BearerTokenChallengeAuthenticationPolicy(credential, "scope"); MockTransport transport = CreateMockTransport(r => new MockResponse(200)); var requestTasks = new Task[100]; @@ -126,7 +126,7 @@ public async Task BearerTokenAuthenticationPolicy_OneHundredConcurrentCalls() } [Test] - public async Task BearerTokenAuthenticationPolicy_GatedConcurrentCalls() + public async Task BearerTokenChallengeAuthenticationPolicy_GatedConcurrentCalls() { var requestMre = new ManualResetEventSlim(false); var responseMre = new ManualResetEventSlim(false); @@ -137,7 +137,7 @@ public async Task BearerTokenAuthenticationPolicy_GatedConcurrentCalls() return new AccessToken(Guid.NewGuid().ToString(), DateTimeOffset.UtcNow.AddMinutes(30)); }, IsAsync); - var policy = new BearerTokenAuthenticationPolicy(credential, "scope"); + var policy = new BearerTokenChallengeAuthenticationPolicy(credential, "scope"); MockTransport transport = CreateMockTransport(new MockResponse(200), new MockResponse(200)); var firstRequestTask = SendGetRequest(transport, policy, uri: new Uri("https://example.com")); @@ -155,7 +155,7 @@ public async Task BearerTokenAuthenticationPolicy_GatedConcurrentCalls() } [Test] - public async Task BearerTokenAuthenticationPolicy_SucceededFailedSucceeded() + public async Task BearerTokenChallengeAuthenticationPolicy_SucceededFailedSucceeded() { var requestMre = new ManualResetEventSlim(false); var callCount = 0; @@ -170,7 +170,7 @@ public async Task BearerTokenAuthenticationPolicy_SucceededFailedSucceeded() : new AccessToken(Guid.NewGuid().ToString(), offsetTime.AddMilliseconds(1000)); }, IsAsync); - var policy = new BearerTokenAuthenticationPolicy(credential, new[] { "scope" }, TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(30)); + var policy = new BearerTokenChallengeAuthenticationPolicy(credential, new[] { "scope" }, TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(30)); MockTransport transport = CreateMockTransport(r => new MockResponse(200)); var firstRequestTask = SendGetRequest(transport, policy, uri: new Uri("https://example.com/1")); @@ -212,7 +212,7 @@ public async Task BearerTokenAuthenticationPolicy_SucceededFailedSucceeded() } [Test] - public async Task BearerTokenAuthenticationPolicy_TokenAlmostExpired() + public async Task BearerTokenChallengeAuthenticationPolicy_TokenAlmostExpired() { var requestMre = new ManualResetEventSlim(true); var responseMre = new ManualResetEventSlim(true); @@ -229,7 +229,7 @@ public async Task BearerTokenAuthenticationPolicy_TokenAlmostExpired() return new AccessToken(Guid.NewGuid().ToString(), expires.Dequeue()); }, IsAsync); - var policy = new BearerTokenAuthenticationPolicy(credential, "scope"); + var policy = new BearerTokenChallengeAuthenticationPolicy(credential, "scope"); MockTransport transport = CreateMockTransport(new MockResponse(200), new MockResponse(200), new MockResponse(200), new MockResponse(200)); await SendGetRequest(transport, policy, uri: new Uri("https://example.com/1/Original")); @@ -258,7 +258,7 @@ public async Task BearerTokenAuthenticationPolicy_TokenAlmostExpired() } [Test] - public async Task BearerTokenAuthenticationPolicy_TokenAlmostExpired_NoRefresh() + public async Task BearerTokenChallengeAuthenticationPolicy_TokenAlmostExpired_NoRefresh() { var requestMre = new ManualResetEventSlim(true); var responseMre = new ManualResetEventSlim(true); @@ -274,7 +274,7 @@ public async Task BearerTokenAuthenticationPolicy_TokenAlmostExpired_NoRefresh() return new AccessToken(Guid.NewGuid().ToString(), currentTime.AddMinutes(2)); }, IsAsync); - var policy = new BearerTokenAuthenticationPolicy(credential, "scope"); + var policy = new BearerTokenChallengeAuthenticationPolicy(credential, "scope"); MockTransport transport = CreateMockTransport(new MockResponse(200), new MockResponse(200), new MockResponse(200), new MockResponse(200)); await SendGetRequest(transport, policy, uri: new Uri("https://example.com/1/Original")); @@ -302,7 +302,7 @@ public async Task BearerTokenAuthenticationPolicy_TokenAlmostExpired_NoRefresh() } [Test] - public async Task BearerTokenAuthenticationPolicy_TokenExpired() + public async Task BearerTokenChallengeAuthenticationPolicy_TokenExpired() { var requestMre = new ManualResetEventSlim(true); var responseMre = new ManualResetEventSlim(true); @@ -315,7 +315,7 @@ public async Task BearerTokenAuthenticationPolicy_TokenExpired() return new AccessToken(Guid.NewGuid().ToString(), expires.Dequeue()); }, IsAsync); - var policy = new BearerTokenAuthenticationPolicy(credential, new[] { "scope" }, TimeSpan.FromSeconds(2), TimeSpan.FromMilliseconds(50)); + var policy = new BearerTokenChallengeAuthenticationPolicy(credential, new[] { "scope" }, TimeSpan.FromSeconds(2), TimeSpan.FromMilliseconds(50)); MockTransport transport = CreateMockTransport(new MockResponse(200), new MockResponse(200), new MockResponse(200)); await SendGetRequest(transport, policy, uri: new Uri("https://example.com/0")); @@ -342,7 +342,7 @@ public async Task BearerTokenAuthenticationPolicy_TokenExpired() } [Test] - public void BearerTokenAuthenticationPolicy_OneHundredConcurrentCallsFailed() + public void BearerTokenChallengeAuthenticationPolicy_OneHundredConcurrentCallsFailed() { var credential = new TokenCredentialStub((r, c) => { @@ -350,7 +350,7 @@ public void BearerTokenAuthenticationPolicy_OneHundredConcurrentCallsFailed() throw new InvalidOperationException("Error"); }, IsAsync); - var policy = new BearerTokenAuthenticationPolicy(credential, "scope"); + var policy = new BearerTokenChallengeAuthenticationPolicy(credential, "scope"); MockTransport transport = CreateMockTransport(r => new MockResponse(200)); var requestTasks = new Task[100]; @@ -368,7 +368,7 @@ public void BearerTokenAuthenticationPolicy_OneHundredConcurrentCallsFailed() } [Test] - public async Task BearerTokenAuthenticationPolicy_GatedConcurrentCallsFailed() + public async Task BearerTokenChallengeAuthenticationPolicy_GatedConcurrentCallsFailed() { var requestMre = new ManualResetEventSlim(false); var responseMre = new ManualResetEventSlim(false); @@ -379,7 +379,7 @@ public async Task BearerTokenAuthenticationPolicy_GatedConcurrentCallsFailed() throw new InvalidOperationException("Error"); }, IsAsync); - var policy = new BearerTokenAuthenticationPolicy(credential, "scope"); + var policy = new BearerTokenChallengeAuthenticationPolicy(credential, "scope"); MockTransport transport = CreateMockTransport(new MockResponse(200), new MockResponse(200)); var firstRequestTask = SendGetRequest(transport, policy, uri: new Uri("https://example.com")); @@ -397,7 +397,7 @@ public async Task BearerTokenAuthenticationPolicy_GatedConcurrentCallsFailed() } [Test] - public async Task BearerTokenAuthenticationPolicy_TokenExpiredThenFailed() + public async Task BearerTokenChallengeAuthenticationPolicy_TokenExpiredThenFailed() { var requestMre = new ManualResetEventSlim(true); var responseMre = new ManualResetEventSlim(true); @@ -415,7 +415,7 @@ public async Task BearerTokenAuthenticationPolicy_TokenExpiredThenFailed() return new AccessToken(Guid.NewGuid().ToString(), DateTimeOffset.UtcNow.AddSeconds(2)); }, IsAsync); - var policy = new BearerTokenAuthenticationPolicy(credential, new[] { "scope" }, TimeSpan.FromSeconds(2), TimeSpan.FromMilliseconds(50)); + var policy = new BearerTokenChallengeAuthenticationPolicy(credential, new[] { "scope" }, TimeSpan.FromSeconds(2), TimeSpan.FromMilliseconds(50)); MockTransport transport = CreateMockTransport(new MockResponse(200), new MockResponse(200), new MockResponse(200)); await SendGetRequest(transport, policy, uri: new Uri("https://example.com/0")); @@ -442,7 +442,7 @@ public async Task BearerTokenAuthenticationPolicy_TokenExpiredThenFailed() [Test] [Ignore("https://github.com/Azure/azure-sdk-for-net/issues/14612")] - public async Task BearerTokenAuthenticationPolicy_TokenAlmostExpiredThenFailed() + public async Task BearerTokenChallengeAuthenticationPolicy_TokenAlmostExpiredThenFailed() { var requestMre = new ManualResetEventSlim(true); var responseMre = new ManualResetEventSlim(true); @@ -463,7 +463,7 @@ public async Task BearerTokenAuthenticationPolicy_TokenAlmostExpiredThenFailed() }, IsAsync); var tokenRefreshRetryDelay = TimeSpan.FromSeconds(2); - var policy = new BearerTokenAuthenticationPolicy(credential, new[] { "scope" }, TimeSpan.FromMinutes(2), tokenRefreshRetryDelay); + var policy = new BearerTokenChallengeAuthenticationPolicy(credential, new[] { "scope" }, TimeSpan.FromMinutes(2), tokenRefreshRetryDelay); MockTransport transport = CreateMockTransport(r => { requestMre.Set(); @@ -512,7 +512,7 @@ public async Task BearerTokenAuthenticationPolicy_TokenAlmostExpiredThenFailed() } [Test] - public void BearerTokenAuthenticationPolicy_GatedConcurrentCallsCancelled() + public void BearerTokenChallengeAuthenticationPolicy_GatedConcurrentCallsCancelled() { var requestMre = new ManualResetEventSlim(false); var responseMre = new ManualResetEventSlim(false); @@ -524,7 +524,7 @@ public void BearerTokenAuthenticationPolicy_GatedConcurrentCallsCancelled() throw new InvalidOperationException("Error"); }, IsAsync); - var policy = new BearerTokenAuthenticationPolicy(credential, "scope"); + var policy = new BearerTokenChallengeAuthenticationPolicy(credential, "scope"); MockTransport transport = CreateMockTransport(new MockResponse(200), new MockResponse(200)); var firstRequestTask = SendGetRequest(transport, policy, uri: new Uri("https://example.com"), cancellationToken: default); @@ -539,7 +539,7 @@ public void BearerTokenAuthenticationPolicy_GatedConcurrentCallsCancelled() Assert.CatchAsync(async () => await firstRequestTask); } - private const string CaeInsufficientClaimsChallenge = "Bearer realm=\"\", authorization_uri=\"https://login.microsoftonline.com/common/oauth2/authorize\", client_id=\"00000003-0000-0000-c000-000000000000\", error=\"insufficient_claims\", claims=\"eyJhY2Nlc3NfdG9rZW4iOiB7ImZvbyI6ICJiYXIifX0=\""; + private const string CaeInsufficientClaimsChallenge = "Bearer realm=\"\", authorization_uri=\"https://login.microsoftonline.com/common/oauth2/authorize\", client_id=\"00000003-0000-0000-c000-000000000000\", error=\"insufficient_claims\", claims=\"eyJhY2Nlc3NfdG9rZW4iOiB7ImZvbyI6ICJiYXIifX0=\""; private const string CaeInsufficientClaimsChallengeValue = "eyJhY2Nlc3NfdG9rZW4iOiB7ImZvbyI6ICJiYXIifX0="; private static readonly Challenge ParsedCaeInsufficientClaimsChallenge = new Challenge { @@ -615,19 +615,19 @@ private class Challenge } [Test] - public void BearerTokenAuthenticationPolicy_ValidateChallengeParsing([Values("CaeInsufficientClaims", "CaeSessionsRevoked", "KeyVault", "Arm")]string challengeKey) + public void BearerTokenChallengeAuthenticationPolicy_ValidateChallengeParsing([Values("CaeInsufficientClaims", "CaeSessionsRevoked", "KeyVault", "Arm")] string challengeKey) { var challenge = ChallengeStrings[challengeKey]; List parsedChallenges = new List(); - foreach (var challengeTuple in BearerTokenAuthenticationPolicy.ParseChallenges(challenge)) + foreach (var challengeTuple in BearerTokenChallengeAuthenticationPolicy.ParseChallenges(challenge)) { Challenge parsedChallenge = new Challenge(); parsedChallenge.Scheme = challengeTuple.Item1; - foreach (var paramTuple in BearerTokenAuthenticationPolicy.ParseChallengeParameters(challengeTuple.Item2)) + foreach (var paramTuple in BearerTokenChallengeAuthenticationPolicy.ParseChallengeParameters(challengeTuple.Item2)) { parsedChallenge.Parameters.Add(paramTuple); } @@ -641,7 +641,7 @@ public void BearerTokenAuthenticationPolicy_ValidateChallengeParsing([Values("Ca } [Test] - public async Task BearerTokenAuthenticationPolicy_ValidateClaimsChallengeTokenRequest() + public async Task BearerTokenChallengeAuthenticationPolicy_ValidateClaimsChallengeTokenRequest() { string currentClaimChallenge = null; @@ -656,7 +656,7 @@ public async Task BearerTokenAuthenticationPolicy_ValidateClaimsChallengeTokenRe return new AccessToken(Guid.NewGuid().ToString(), DateTimeOffset.UtcNow + TimeSpan.FromDays(1)); }, IsAsync); - var policy = new BearerTokenAuthenticationPolicy(credential, "scope"); + var policy = new BearerTokenChallengeAuthenticationPolicy(credential, "scope"); var insufficientClaimsChallengeResponse = new MockResponse(401); @@ -726,9 +726,9 @@ private void ValidateParsedChallenge(Challenge expected, Challenge actual) CollectionAssert.AreEquivalent(expected.Parameters, actual.Parameters); } - private class TokenCredentialStub : Experimental.TokenCredential + private class TokenCredentialStub : TokenCredential { - public TokenCredentialStub(Func handler, bool isAsync) + public TokenCredentialStub(Func handler, bool isAsync) { if (isAsync) { @@ -742,13 +742,13 @@ public TokenCredentialStub(Func> _getTokenAsyncHandler; - private readonly Func _getTokenHandler; + private readonly Func> _getTokenAsyncHandler; + private readonly Func _getTokenHandler; - public override ValueTask GetTokenAsync(Experimental.TokenRequestContext requestContext, CancellationToken cancellationToken) + public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) => _getTokenAsyncHandler(requestContext, cancellationToken); - public override AccessToken GetToken(Experimental.TokenRequestContext requestContext, CancellationToken cancellationToken) + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) => _getTokenHandler(requestContext, cancellationToken); } } From 408c75d30f811c6dbc99a3c264b6c17b7c1e9d73 Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Mon, 25 Jan 2021 08:52:35 -0600 Subject: [PATCH 04/22] remove unneeded types --- sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj b/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj index 518e7abdb64d6..593a7415302f5 100644 --- a/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj +++ b/sdk/core/Azure.Core/tests/Azure.Core.Tests.csproj @@ -24,6 +24,7 @@ + @@ -33,7 +34,6 @@ - From 3c2ed8cb1d9c5047f414fefd0e41894c4eaf7c4c Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Mon, 25 Jan 2021 11:41:31 -0600 Subject: [PATCH 05/22] TryGetTokenRequestContextFromChallenge --- ...earerTokenChallengeAuthenticationPolicy.cs | 154 +++++++++--------- .../Azure.Core/src/TokenRequestContext.cs | 2 +- 2 files changed, 78 insertions(+), 78 deletions(-) diff --git a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs index 34e97a6668667..d60fad492a9c3 100644 --- a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs +++ b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs @@ -9,6 +9,8 @@ using System.Threading.Tasks; using Azure.Core.Diagnostics; +#nullable enable + namespace Azure.Core.Pipeline { /// @@ -25,17 +27,6 @@ internal class BearerTokenChallengeAuthenticationPolicy : HttpPipelinePolicy private readonly AccessTokenCache _accessTokenCache; private string[] _scopes; - /// - /// Sets the current scopes for the policy. - /// - /// The scopes. - protected void SetScopes(string[] scopes) - { - Argument.AssertNotNull(scopes, nameof(scopes)); - - _scopes = scopes; - } - /// /// Creates a new instance of using provided token credential and scope to authenticate for. /// @@ -72,6 +63,38 @@ public override void Process(HttpMessage message, ReadOnlyMemory + /// Executed before initially sending the request to authenticate the request. + /// + /// The HttpMessage to be authenticated. + /// Specifies if the method is being called in an asynchronous context + protected virtual async Task OnBeforeRequestAsync(HttpMessage message, bool async) + { + await Task.CompletedTask; + } + + /// + /// Executed in the event a 401 response with a WWW-Authenticate authentication challenge header is received after the initial request. + /// + /// This implementation handles common authentication challenges such as claims challenges. Service client libraries may derive from this and extend to handle service specific authentication challenges. + /// The HttpMessage to be authenticated. + /// Specifies if the method is being called in an asynchronous context. + /// A boolean indicated whether the request contained a valid challenge and a was successfully initialized with it. + protected virtual bool TryGetTokenRequestContextFromChallenge(HttpMessage message, out TokenRequestContext context) + { + context = default; + + var claimsChallenge = GetClaimsChallenge(message.Response); + + if (claimsChallenge != null) + { + context = new TokenRequestContext(_scopes, message.Request.ClientRequestId, claimsChallenge); + return true; + } + + return false; + } + private async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline, bool async) { if (message.Request.Uri.Scheme != Uri.UriSchemeHttps) @@ -79,6 +102,8 @@ private async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory ParseChallenges(string headerValue) - { - var challengeMatches = s_AuthenticationChallengeRegex.Matches(headerValue); - - for (int i = 0; i < challengeMatches.Count; i++) - { - yield return (challengeMatches[i].Groups[1].Value, challengeMatches[i].Groups[2].Value); - } - } - - internal static IEnumerable<(string, string)> ParseChallengeParameters(string challengeValue) + private async Task AuthenticateRequestAsync(HttpMessage message, TokenRequestContext context, bool async) { - var paramMatches = s_ChallengeParameterRegex.Matches(challengeValue); - - for (int i = 0; i < paramMatches.Count; i++) + string headerValue; + if (async) { - yield return (paramMatches[i].Groups[1].Value, paramMatches[i].Groups[2].Value); + headerValue = await _accessTokenCache.GetHeaderValueAsync(message, context, async).ConfigureAwait(false); } - } - - /// - /// Executed before initially sending the request to authenticate the request. - /// - /// The HttpMessage to be authenticated. - /// Specifies if the method is being called in an asynchronous context - protected virtual async Task OnBeforeRequestAsync(HttpMessage message, bool async) - { - await AuthenticateRequestAsync(message, new TokenRequestContext(_scopes, message.Request.ClientRequestId), async).ConfigureAwait(false); - } - - /// - /// Executed in the event a 401 response with a WWW-Authenticate authentication challenge header is received after the initial request. - /// - /// This implementation handles common authentication challenges such as claims challenges. Service client libraries may derive from this and extend to handle service specific authentication challenges. - /// The HttpMessage to be authenticated. - /// Specifies if the method is being called in an asynchronous context. - /// A boolean indicated whether the request should be retried. - protected virtual async Task OnChallengeAsync(HttpMessage message, bool async) - { - var claimsChallenge = GetClaimsChallenge(message.Response); - - if (claimsChallenge != null) + else { - await AuthenticateRequestAsync(message, new TokenRequestContext(_scopes, message.Request.ClientRequestId, claimsChallenge), async).ConfigureAwait(false); - - return true; + headerValue = _accessTokenCache.GetHeaderValueAsync(message, context, async).EnsureCompleted(); } - return false; + //TODO: revert to Request.SetHeader if this migrates back to Azure.Core + message.Request.Headers.SetValue(HttpHeader.Names.Authorization, headerValue); } - private static string GetClaimsChallenge(Response response) + private static string? GetClaimsChallenge(Response response) { - if (response.Status == 401 && response.Headers.TryGetValue("WWW-Authenticate", out string headerValue)) + if (response.Status == 401 && response.Headers.TryGetValue("WWW-Authenticate", out string? headerValue)) { foreach (var challenge in ParseChallenges(headerValue)) { @@ -182,22 +178,6 @@ private static string GetClaimsChallenge(Response response) return null; } - private async Task AuthenticateRequestAsync(HttpMessage message, TokenRequestContext context, bool async) - { - string headerValue; - if (async) - { - headerValue = await _accessTokenCache.GetHeaderValueAsync(message, context, async).ConfigureAwait(false); - } - else - { - headerValue = _accessTokenCache.GetHeaderValueAsync(message, context, async).EnsureCompleted(); - } - - //TODO: revert to Request.SetHeader if this migrates back to Azure.Core - message.Request.Headers.SetValue(HttpHeader.Names.Authorization, headerValue); - } - private class AccessTokenCache { private readonly object _syncObj = new object(); @@ -206,8 +186,8 @@ private class AccessTokenCache private readonly TimeSpan _tokenRefreshRetryDelay; private TokenRequestContext? _currentContext; - private TaskCompletionSource _infoTcs; - private TaskCompletionSource _backgroundUpdateTcs; + private TaskCompletionSource? _infoTcs; + private TaskCompletionSource? _backgroundUpdateTcs; public AccessTokenCache(TokenCredential credential, TimeSpan tokenRefreshOffset, TimeSpan tokenRefreshRetryDelay, string[] initialScopes) { _credential = credential; @@ -220,7 +200,7 @@ public async ValueTask GetHeaderValueAsync(HttpMessage message, TokenReq { bool getTokenFromCredential; TaskCompletionSource headerValueTcs; - TaskCompletionSource backgroundUpdateTcs; + TaskCompletionSource? backgroundUpdateTcs; (headerValueTcs, backgroundUpdateTcs, getTokenFromCredential) = GetTaskCompletionSources(context); HeaderValueInfo info; @@ -289,7 +269,7 @@ public async ValueTask GetHeaderValueAsync(HttpMessage message, TokenReq return info.HeaderValue; } - private (TaskCompletionSource tcs, TaskCompletionSource backgroundUpdateTcs, bool getTokenFromCredential) GetTaskCompletionSources(TokenRequestContext context) + private (TaskCompletionSource tcs, TaskCompletionSource? backgroundUpdateTcs, bool getTokenFromCredential) GetTaskCompletionSources(TokenRequestContext context) { lock (_syncObj) { @@ -410,5 +390,25 @@ public HeaderValueInfo(string headerValue, DateTimeOffset expiresOn, DateTimeOff } } } + + internal static IEnumerable<(string, string)> ParseChallenges(string headerValue) + { + var challengeMatches = s_AuthenticationChallengeRegex.Matches(headerValue); + + for (int i = 0; i < challengeMatches.Count; i++) + { + yield return (challengeMatches[i].Groups[1].Value, challengeMatches[i].Groups[2].Value); + } + } + + internal static IEnumerable<(string, string)> ParseChallengeParameters(string challengeValue) + { + var paramMatches = s_ChallengeParameterRegex.Matches(challengeValue); + + for (int i = 0; i < paramMatches.Count; i++) + { + yield return (paramMatches[i].Groups[1].Value, paramMatches[i].Groups[2].Value); + } + } } } diff --git a/sdk/core/Azure.Core/src/TokenRequestContext.cs b/sdk/core/Azure.Core/src/TokenRequestContext.cs index c9fbf4de477a1..fd31ead5d5072 100644 --- a/sdk/core/Azure.Core/src/TokenRequestContext.cs +++ b/sdk/core/Azure.Core/src/TokenRequestContext.cs @@ -41,7 +41,7 @@ public TokenRequestContext(string[] scopes, string? parentRequestId = default, s /// /// A claims challenge returned from a failed authentication or authorization request. /// - public string ClaimsChallenge { get; } + public string? ClaimsChallenge { get; } /// /// The of the request requiring a token for authentication, if applicable. From f1973ad723d633fea624893408addc8139f7b44e Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Mon, 25 Jan 2021 11:50:38 -0600 Subject: [PATCH 06/22] wip --- .../src/Shared/BearerTokenChallengeAuthenticationPolicy.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs index d60fad492a9c3..7236b73363b02 100644 --- a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs +++ b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs @@ -70,7 +70,7 @@ public override void Process(HttpMessage message, ReadOnlyMemorySpecifies if the method is being called in an asynchronous context protected virtual async Task OnBeforeRequestAsync(HttpMessage message, bool async) { - await Task.CompletedTask; + await Task.CompletedTask.ConfigureAwait(false); } /// @@ -78,7 +78,7 @@ protected virtual async Task OnBeforeRequestAsync(HttpMessage message, bool asyn /// /// This implementation handles common authentication challenges such as claims challenges. Service client libraries may derive from this and extend to handle service specific authentication challenges. /// The HttpMessage to be authenticated. - /// Specifies if the method is being called in an asynchronous context. + /// If the return value is true, a . /// A boolean indicated whether the request contained a valid challenge and a was successfully initialized with it. protected virtual bool TryGetTokenRequestContextFromChallenge(HttpMessage message, out TokenRequestContext context) { From 689a6b139a9c2b544183049211fd9a8301f7757f Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Mon, 25 Jan 2021 11:54:03 -0600 Subject: [PATCH 07/22] wip --- .../src/Shared/BearerTokenChallengeAuthenticationPolicy.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs index 7236b73363b02..6b7e1fce7221e 100644 --- a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs +++ b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs @@ -70,7 +70,10 @@ public override void Process(HttpMessage message, ReadOnlyMemorySpecifies if the method is being called in an asynchronous context protected virtual async Task OnBeforeRequestAsync(HttpMessage message, bool async) { - await Task.CompletedTask.ConfigureAwait(false); + if (async) + { + await Task.CompletedTask.ConfigureAwait(false); + } } /// From bccd1905c4b21bd5c3ab803a16f78ed5528240f8 Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Mon, 25 Jan 2021 19:15:17 -0600 Subject: [PATCH 08/22] tweaks to extensibility --- ...earerTokenChallengeAuthenticationPolicy.cs | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs index 6b7e1fce7221e..c97ee84984fd2 100644 --- a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs +++ b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs @@ -67,8 +67,9 @@ public override void Process(HttpMessage message, ReadOnlyMemory /// The HttpMessage to be authenticated. + /// /// Specifies if the method is being called in an asynchronous context - protected virtual async Task OnBeforeRequestAsync(HttpMessage message, bool async) + protected virtual async Task OnBeforeRequestAsync(HttpMessage message, ReadOnlyMemory pipeline, bool async) { if (async) { @@ -105,27 +106,36 @@ private async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory 0) { - await ProcessNextAsync(message, pipeline).ConfigureAwait(false); - } - else - { - ProcessNext(message, pipeline); + await AuthenticateRequestAsync(message, new TokenRequestContext(_scopes, message.Request.ClientRequestId), async).ConfigureAwait(false); + + if (async) + { + await ProcessNextAsync(message, pipeline).ConfigureAwait(false); + } + else + { + ProcessNext(message, pipeline); + } } - // Check if we have received a challenge. - if (message.Response.Status == 401 && message.Response.Headers.Contains("WWW-Authenticate")) + // Check if we have received a challenge or we have not yet issued the first request. + if (!message.HasResponse || (message.Response.Status == 401 && message.Response.Headers.Contains("WWW-Authenticate"))) { // Attempt to get the TokenRequestContext based on the challenge. // If we fail to get the context, the challenge was not present or invalid. // If we succeed in getting the context, authenticate the request and pass it up the policy chain. if (TryGetTokenRequestContextFromChallenge(message, out TokenRequestContext context)) { + // Ensure the scopes are consistent with what was set by . + _scopes = context.Scopes; + await AuthenticateRequestAsync(message, context, async).ConfigureAwait(false); if (async) From 2860d9114b1f794e12db66c86f79eb390721e0b9 Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Tue, 26 Jan 2021 09:43:46 -0600 Subject: [PATCH 09/22] without OnBeforeRequest --- ...earerTokenChallengeAuthenticationPolicy.cs | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs index c97ee84984fd2..4f9078797bd5f 100644 --- a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs +++ b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs @@ -63,20 +63,6 @@ public override void Process(HttpMessage message, ReadOnlyMemory - /// Executed before initially sending the request to authenticate the request. - /// - /// The HttpMessage to be authenticated. - /// - /// Specifies if the method is being called in an asynchronous context - protected virtual async Task OnBeforeRequestAsync(HttpMessage message, ReadOnlyMemory pipeline, bool async) - { - if (async) - { - await Task.CompletedTask.ConfigureAwait(false); - } - } - /// /// Executed in the event a 401 response with a WWW-Authenticate authentication challenge header is received after the initial request. /// @@ -106,11 +92,9 @@ private async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory 0) { await AuthenticateRequestAsync(message, new TokenRequestContext(_scopes, message.Request.ClientRequestId), async).ConfigureAwait(false); From 1c8131a900ae85bde4b36acaf2432637b975e44d Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Wed, 27 Jan 2021 14:49:31 -0600 Subject: [PATCH 10/22] name tuples --- .../BearerTokenChallengeAuthenticationPolicy.cs | 16 ++++++++-------- ...rerTokenChallengeAuthenticationPolicyTests.cs | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs index 4f9078797bd5f..66bec8159a9b2 100644 --- a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs +++ b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs @@ -154,18 +154,18 @@ private async Task AuthenticateRequestAsync(HttpMessage message, TokenRequestCon { if (response.Status == 401 && response.Headers.TryGetValue("WWW-Authenticate", out string? headerValue)) { - foreach (var challenge in ParseChallenges(headerValue)) + foreach (var (ChallengeKey, ChallengeParameters) in ParseChallenges(headerValue)) { - if (string.Equals(challenge.Item1, "Bearer", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(ChallengeKey, "Bearer", StringComparison.OrdinalIgnoreCase)) { - foreach (var parameter in ParseChallengeParameters(challenge.Item2)) + foreach (var (ChallengeParameterKey, ChallengeParameterValue) in ParseChallengeParameters(ChallengeParameters)) { - if (string.Equals(parameter.Item1, "claims", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(ChallengeParameterKey, "claims", StringComparison.OrdinalIgnoreCase)) { // currently we are only handling ARM claims challenges which are always b64url encoded, and must be decoded. // some handling will have to be added if we intend to handle claims challenges from Graph as well since they // are not encoded. - return Base64Url.DecodeString(parameter.Item2); + return Base64Url.DecodeString(ChallengeParameterValue); } } } @@ -388,7 +388,7 @@ public HeaderValueInfo(string headerValue, DateTimeOffset expiresOn, DateTimeOff } } - internal static IEnumerable<(string, string)> ParseChallenges(string headerValue) + internal static IEnumerable<(string ChallengeKey, string ChallengeParameters)> ParseChallenges(string headerValue) { var challengeMatches = s_AuthenticationChallengeRegex.Matches(headerValue); @@ -398,9 +398,9 @@ public HeaderValueInfo(string headerValue, DateTimeOffset expiresOn, DateTimeOff } } - internal static IEnumerable<(string, string)> ParseChallengeParameters(string challengeValue) + internal static IEnumerable<(string ChallengeParameterKey, string ChallengeParameterValue)> ParseChallengeParameters(string ChallengeParameters) { - var paramMatches = s_ChallengeParameterRegex.Matches(challengeValue); + var paramMatches = s_ChallengeParameterRegex.Matches(ChallengeParameters); for (int i = 0; i < paramMatches.Count; i++) { diff --git a/sdk/core/Azure.Core/tests/BearerTokenChallengeAuthenticationPolicyTests.cs b/sdk/core/Azure.Core/tests/BearerTokenChallengeAuthenticationPolicyTests.cs index 31875ef9e8e03..20a15c40dd61f 100644 --- a/sdk/core/Azure.Core/tests/BearerTokenChallengeAuthenticationPolicyTests.cs +++ b/sdk/core/Azure.Core/tests/BearerTokenChallengeAuthenticationPolicyTests.cs @@ -621,13 +621,13 @@ public void BearerTokenChallengeAuthenticationPolicy_ValidateChallengeParsing([V List parsedChallenges = new List(); - foreach (var challengeTuple in BearerTokenChallengeAuthenticationPolicy.ParseChallenges(challenge)) + foreach (var (ChallengeKey, ChallengeParameters) in BearerTokenChallengeAuthenticationPolicy.ParseChallenges(challenge)) { Challenge parsedChallenge = new Challenge(); - parsedChallenge.Scheme = challengeTuple.Item1; + parsedChallenge.Scheme = ChallengeKey; - foreach (var paramTuple in BearerTokenChallengeAuthenticationPolicy.ParseChallengeParameters(challengeTuple.Item2)) + foreach (var paramTuple in BearerTokenChallengeAuthenticationPolicy.ParseChallengeParameters(ChallengeParameters)) { parsedChallenge.Parameters.Add(paramTuple); } From 6a0862822258a754190649ab382889a8704087d7 Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Fri, 29 Jan 2021 12:10:08 -0600 Subject: [PATCH 11/22] eliminate the logic for checking empty scopes --- ...earerTokenChallengeAuthenticationPolicy.cs | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs index 66bec8159a9b2..b5239b48b4220 100644 --- a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs +++ b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs @@ -92,30 +92,40 @@ private async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory 0) - { - await AuthenticateRequestAsync(message, new TokenRequestContext(_scopes, message.Request.ClientRequestId), async).ConfigureAwait(false); + TokenRequestContext context; - if (async) - { - await ProcessNextAsync(message, pipeline).ConfigureAwait(false); - } - else + // If the message already has a challenge response due to a sub-class pre-processing the request, get the context from the challenge. + if (message.HasResponse && message.Response.Status == 401 && message.Response.Headers.Contains("WWW-Authenticate")) + { + if (!TryGetTokenRequestContextFromChallenge(message, out context)) { - ProcessNext(message, pipeline); + // We were unsuccessful in handling the challenge, so bail out now. + return; } } + else + { + context = new TokenRequestContext(_scopes, message.Request.ClientRequestId); + } + + await AuthenticateRequestAsync(message, context, async).ConfigureAwait(false); + + if (async) + { + await ProcessNextAsync(message, pipeline).ConfigureAwait(false); + } + else + { + ProcessNext(message, pipeline); + } // Check if we have received a challenge or we have not yet issued the first request. - if (!message.HasResponse || (message.Response.Status == 401 && message.Response.Headers.Contains("WWW-Authenticate"))) + if (message.Response.Status == 401 && message.Response.Headers.Contains("WWW-Authenticate")) { // Attempt to get the TokenRequestContext based on the challenge. // If we fail to get the context, the challenge was not present or invalid. // If we succeed in getting the context, authenticate the request and pass it up the policy chain. - if (TryGetTokenRequestContextFromChallenge(message, out TokenRequestContext context)) + if (TryGetTokenRequestContextFromChallenge(message, out context)) { // Ensure the scopes are consistent with what was set by . _scopes = context.Scopes; From b5557a5dc8f76e27e139902d2f3570edba5a6e7d Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Fri, 29 Jan 2021 12:37:52 -0600 Subject: [PATCH 12/22] fix issue with comparing scopes --- .../src/Shared/BearerTokenChallengeAuthenticationPolicy.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs index b5239b48b4220..268112e4fed18 100644 --- a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs +++ b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs @@ -333,6 +333,11 @@ private bool RequestRequiresNewToken(TokenRequestContext context) if (context.Scopes != null) { + if (context.Scopes.Length != _currentContext.Value.Scopes?.Length) + { + return true; + } + for (int i = 0; i < context.Scopes.Length; i++) { if (context.Scopes[i] != _currentContext.Value.Scopes?[i]) From 87846a71e117aac552f01ac7d8f12437fa41deb2 Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Mon, 1 Feb 2021 14:23:46 -0600 Subject: [PATCH 13/22] small tweaks --- .../BearerTokenChallengeAuthenticationPolicy.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs index 268112e4fed18..4ff5a47b15ec3 100644 --- a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs +++ b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -20,7 +21,7 @@ internal class BearerTokenChallengeAuthenticationPolicy : HttpPipelinePolicy { private const string AuthenticationChallengePattern = @"(\w+) ((?:\w+="".*?""(?:, )?)+)(?:, )?"; private const string ChallengeParameterPattern = @"(?:(\w+)=""([^""]*)"")+"; - + private const string ChallengeHeader = "WWW-Authenticate"; private static readonly Regex s_AuthenticationChallengeRegex = new Regex(AuthenticationChallengePattern, RegexOptions.Compiled); private static readonly Regex s_ChallengeParameterRegex = new Regex(ChallengeParameterPattern, RegexOptions.Compiled); @@ -95,7 +96,7 @@ private async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory Date: Tue, 2 Feb 2021 16:27:04 -0600 Subject: [PATCH 14/22] merge with master TokenRequestContext --- .../src/Shared/BearerTokenChallengeAuthenticationPolicy.cs | 2 +- sdk/core/Azure.Core/src/TokenRequestContext.cs | 5 ----- .../tests/BearerTokenChallengeAuthenticationPolicyTests.cs | 2 +- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs index 4ff5a47b15ec3..35c568a27356b 100644 --- a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs +++ b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs @@ -347,7 +347,7 @@ private bool RequestRequiresNewToken(TokenRequestContext context) } } - if ((context.ClaimsChallenge != null) && (context.ClaimsChallenge != _currentContext.Value.ClaimsChallenge)) + if ((context.Claims != null) && (context.Claims != _currentContext.Value.Claims)) { return true; } diff --git a/sdk/core/Azure.Core/src/TokenRequestContext.cs b/sdk/core/Azure.Core/src/TokenRequestContext.cs index fd31ead5d5072..fe4237f61b34d 100644 --- a/sdk/core/Azure.Core/src/TokenRequestContext.cs +++ b/sdk/core/Azure.Core/src/TokenRequestContext.cs @@ -38,11 +38,6 @@ public TokenRequestContext(string[] scopes, string? parentRequestId = default, s /// public string[] Scopes { get; } - /// - /// A claims challenge returned from a failed authentication or authorization request. - /// - public string? ClaimsChallenge { get; } - /// /// The of the request requiring a token for authentication, if applicable. /// diff --git a/sdk/core/Azure.Core/tests/BearerTokenChallengeAuthenticationPolicyTests.cs b/sdk/core/Azure.Core/tests/BearerTokenChallengeAuthenticationPolicyTests.cs index 20a15c40dd61f..3f409dc9fc3da 100644 --- a/sdk/core/Azure.Core/tests/BearerTokenChallengeAuthenticationPolicyTests.cs +++ b/sdk/core/Azure.Core/tests/BearerTokenChallengeAuthenticationPolicyTests.cs @@ -651,7 +651,7 @@ public async Task BearerTokenChallengeAuthenticationPolicy_ValidateClaimsChallen { tokensRequested++; - Assert.AreEqual(currentClaimChallenge, r.ClaimsChallenge); + Assert.AreEqual(currentClaimChallenge, r.Claims); return new AccessToken(Guid.NewGuid().ToString(), DateTimeOffset.UtcNow + TimeSpan.FromDays(1)); }, IsAsync); From af4a40fea1679cebf03196eafa0c0367cc8a701b Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Tue, 2 Feb 2021 16:44:26 -0600 Subject: [PATCH 15/22] set _scopes --- .../src/Shared/BearerTokenChallengeAuthenticationPolicy.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs index 35c568a27356b..756eb7c42fa44 100644 --- a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs +++ b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs @@ -103,6 +103,7 @@ private async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory Date: Tue, 2 Feb 2021 16:50:53 -0600 Subject: [PATCH 16/22] revert changes to experimental --- .../Azure.Core.Experimental/src/Azure.Core.Experimental.csproj | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sdk/core/Azure.Core.Experimental/src/Azure.Core.Experimental.csproj b/sdk/core/Azure.Core.Experimental/src/Azure.Core.Experimental.csproj index b46b466fde5c6..00470e50bac06 100644 --- a/sdk/core/Azure.Core.Experimental/src/Azure.Core.Experimental.csproj +++ b/sdk/core/Azure.Core.Experimental/src/Azure.Core.Experimental.csproj @@ -15,9 +15,8 @@ - - + From 609eb745a7f751fcdba6144f2d3867cc5d3861fd Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Wed, 3 Feb 2021 09:20:34 -0600 Subject: [PATCH 17/22] pr comments --- ...earerTokenChallengeAuthenticationPolicy.cs | 2 +- sdk/core/Azure.Core/src/Shared/base64.cs | 55 ------------------- 2 files changed, 1 insertion(+), 56 deletions(-) delete mode 100644 sdk/core/Azure.Core/src/Shared/base64.cs diff --git a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs index 756eb7c42fa44..6ede41c32838c 100644 --- a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs +++ b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs @@ -68,7 +68,7 @@ public override void Process(HttpMessage message, ReadOnlyMemory /// This implementation handles common authentication challenges such as claims challenges. Service client libraries may derive from this and extend to handle service specific authentication challenges. - /// The HttpMessage to be authenticated. + /// The to be authenticated. /// If the return value is true, a . /// A boolean indicated whether the request contained a valid challenge and a was successfully initialized with it. protected virtual bool TryGetTokenRequestContextFromChallenge(HttpMessage message, out TokenRequestContext context) diff --git a/sdk/core/Azure.Core/src/Shared/base64.cs b/sdk/core/Azure.Core/src/Shared/base64.cs deleted file mode 100644 index fe2db64a645d9..0000000000000 --- a/sdk/core/Azure.Core/src/Shared/base64.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Text; - -namespace Azure.Core -{ - internal static class Base64Url - { - /// Converts a Base64URL encoded string to a string. - /// The Base64Url encoded string containing UTF8 bytes for a string. - /// The string represented by the Base64URL encoded string. - public static byte[] Decode(string encoded) - { - encoded = new StringBuilder(encoded).Replace('-', '+').Replace('_', '/').Append('=', (encoded.Length % 4 == 0) ? 0 : 4 - (encoded.Length % 4)).ToString(); - - return Convert.FromBase64String(encoded); - } - - /// Encode a byte array as a Base64URL encoded string. - /// Raw byte input buffer. - /// The bytes, encoded as a Base64URL string. - public static string Encode(byte[] bytes) - { - return new StringBuilder(Convert.ToBase64String(bytes)).Replace('+', '-').Replace('/', '_').Replace("=", "").ToString(); - } - - /// Converts a Base64URL encoded string to a string. - /// The Base64Url encoded string containing UTF8 bytes for a string. - /// The string represented by the Base64URL encoded string. - internal static string DecodeString(string encoded) - { - return UTF8Encoding.UTF8.GetString(Decode(encoded)); - } - - /// Encode a string as a Base64URL encoded string. - /// String input buffer. - /// The UTF8 bytes for the string, encoded as a Base64URL string. - internal static string EncodeString(string value) - { - return Encode(UTF8Encoding.UTF8.GetBytes(value)); - } - - public static string HexToBase64Url(string hex) - { - byte[] bytes = new byte[hex.Length / 2]; - - for (int i = 0; i < hex.Length; i += 2) - bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16); - - return Base64Url.Encode(bytes); - } - } -} From d999c68bf6286648a19787a578e103179497e01d Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Wed, 3 Feb 2021 18:10:22 -0600 Subject: [PATCH 18/22] replace RegEx with Span parsing --- ...earerTokenChallengeAuthenticationPolicy.cs | 133 +++++++++++++----- ...TokenChallengeAuthenticationPolicyTests.cs | 76 ++++++++-- 2 files changed, 168 insertions(+), 41 deletions(-) diff --git a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs index 6ede41c32838c..ce0f8ae27758b 100644 --- a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs +++ b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs @@ -163,22 +163,24 @@ private async Task AuthenticateRequestAsync(HttpMessage message, TokenRequestCon private static string? GetClaimsChallenge(Response response) { - if (response.Status == (int)HttpStatusCode.Unauthorized && response.Headers.TryGetValue(ChallengeHeader, out string? headerValue)) + if (response.Status != (int)HttpStatusCode.Unauthorized || !response.Headers.TryGetValue(ChallengeHeader, out string? headerValue)) { - foreach (var (ChallengeKey, ChallengeParameters) in ParseChallenges(headerValue)) + return null; + } + + ReadOnlySpan bearer = "Bearer".AsSpan(); + ReadOnlySpan claims = "claims".AsSpan(); + ReadOnlySpan headerSpan = headerValue.AsSpan(); + + // Iterate through each challenge value. + while (TryGetNextChallenge(ref headerSpan, out var challengeKey, out var challengeParameters)) + { + // Enumerate each key=value parameter until we find the 'claims' key on the 'Bearer' challenge. + while (TryGetNextParameter(ref headerSpan, out var key, out var value)) { - if (string.Equals(ChallengeKey, "Bearer", StringComparison.OrdinalIgnoreCase)) + if (challengeKey.Equals(bearer, StringComparison.OrdinalIgnoreCase) && key.Equals(claims, StringComparison.OrdinalIgnoreCase)) { - foreach (var (ChallengeParameterKey, ChallengeParameterValue) in ParseChallengeParameters(ChallengeParameters)) - { - if (string.Equals(ChallengeParameterKey, "claims", StringComparison.OrdinalIgnoreCase)) - { - // currently we are only handling ARM claims challenges which are always b64url encoded, and must be decoded. - // some handling will have to be added if we intend to handle claims challenges from Graph as well since they - // are not encoded. - return Base64Url.DecodeString(ChallengeParameterValue); - } - } + return Base64Url.DecodeString(value.ToString()); } } } @@ -186,6 +188,91 @@ private async Task AuthenticateRequestAsync(HttpMessage message, TokenRequestCon return null; } + internal static bool TryGetNextChallenge(ref ReadOnlySpan headerValue, out ReadOnlySpan challengeKey, out ReadOnlySpan challengeValue) + { + challengeKey = default; + challengeValue = default; + + headerValue = headerValue.TrimStart(' '); + int endOfChallengeKey = headerValue.IndexOf(' '); + + if (endOfChallengeKey < 0) + { + return false; + } + + challengeKey = headerValue.Slice(0, endOfChallengeKey); + + // Get the parameters string. + challengeValue = headerValue.Slice(endOfChallengeKey + 1); + + // Slice the challenge key from the headerValue + headerValue = headerValue.Slice(endOfChallengeKey + 1); + + return true; + } + + public static bool TryGetNextParameter(ref ReadOnlySpan parameters, out ReadOnlySpan paramKey, out ReadOnlySpan paramValue, char separator = '=') + { + paramKey = default; + paramValue = default; + var spaceOrComma = " ,".AsSpan(); + + // Trim any separater prefixes. + parameters = parameters.TrimStart(spaceOrComma); + + int nextSpace = parameters.IndexOf(' '); + int nextEquals = parameters.IndexOf('='); + + if (nextSpace < nextEquals && nextSpace != -1) + { + // we encountered another challenge value. + return false; + } + + // Get the index of the first separator + var separatorIndex = parameters.IndexOf(separator); + if (separatorIndex < 0) + return false; + + // Get the paramKey. + paramKey = parameters.Slice(0, separatorIndex).Trim(); + + // Slice to remove the 'paramKey=' from the parameters. + parameters = parameters.Slice(separatorIndex + 1); + + // The start of paramValue will usually be a quoted string. Find the first quote. + int quoteIndex = parameters.IndexOf('\"'); + + // Get the paramValue, which is delimited by the trailing quote. + parameters = parameters.Slice(quoteIndex + 1); + if (quoteIndex >= 0) + { + // The values are quote wrapped + paramValue = parameters.Slice(0, parameters.IndexOf('\"')); + } + else + { + //the values are not quote wrapped (storage is one example of this) + // either find the next space indicating the delimiter to the next value, or go to the end since this is the last value. + int trailingDelimiterIndex = parameters.IndexOfAny(spaceOrComma); + if (trailingDelimiterIndex >= 0) + { + paramValue = parameters.Slice(0, trailingDelimiterIndex); + } + else + { + paramValue = parameters; + } + } + + // Slice to remove the '"paramValue"' from the parameters. + if (parameters != paramValue) + parameters = parameters.Slice(paramValue.Length + 1); + + return true; + } + private class AccessTokenCache { private readonly object _syncObj = new object(); @@ -403,25 +490,5 @@ public HeaderValueInfo(string headerValue, DateTimeOffset expiresOn, DateTimeOff } } } - - internal static IEnumerable<(string ChallengeKey, string ChallengeParameters)> ParseChallenges(string headerValue) - { - var challengeMatches = s_AuthenticationChallengeRegex.Matches(headerValue); - - for (int i = 0; i < challengeMatches.Count; i++) - { - yield return (challengeMatches[i].Groups[1].Value, challengeMatches[i].Groups[2].Value); - } - } - - internal static IEnumerable<(string ChallengeParameterKey, string ChallengeParameterValue)> ParseChallengeParameters(string ChallengeParameters) - { - var paramMatches = s_ChallengeParameterRegex.Matches(ChallengeParameters); - - for (int i = 0; i < paramMatches.Count; i++) - { - yield return (paramMatches[i].Groups[1].Value, paramMatches[i].Groups[2].Value); - } - } } } diff --git a/sdk/core/Azure.Core/tests/BearerTokenChallengeAuthenticationPolicyTests.cs b/sdk/core/Azure.Core/tests/BearerTokenChallengeAuthenticationPolicyTests.cs index 3f409dc9fc3da..1eb65b7c08374 100644 --- a/sdk/core/Azure.Core/tests/BearerTokenChallengeAuthenticationPolicyTests.cs +++ b/sdk/core/Azure.Core/tests/BearerTokenChallengeAuthenticationPolicyTests.cs @@ -591,12 +591,34 @@ public void BearerTokenChallengeAuthenticationPolicy_GatedConcurrentCallsCancell } }; + private const string StorageChallenge = "Bearer authorization_uri=https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47/oauth2/authorize resource_id=https://storage.azure.com"; + private static readonly Challenge ParsedStorageChallenge = new Challenge() + { + Scheme = "Bearer", + Parameters = + { + ("authorization_uri", "https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47/oauth2/authorize"), + ("resource_id", "https://storage.azure.com"), + } + }; + private static readonly Challenge ParsedMultipleChallenges = new Challenge + { + Scheme = "Bearer", + Parameters = + { + ("authorization_uri", "https://login.windows-ppe.net/"), + ("error", "invalid_token"), + ("error_description", "User session has been revoked"), + ("claims", "eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwgInZhbHVlIjoiMTYwMzc0MjgwMCJ9fX0="), + } + }; private static readonly Dictionary ChallengeStrings = new Dictionary() { { "CaeInsufficientClaims", CaeInsufficientClaimsChallenge }, { "CaeSessionsRevoked", CaeSessionsRevokedClaimsChallenge }, { "KeyVault", KeyVaultChallenge }, - { "Arm", ArmChallenge } + { "Arm", ArmChallenge }, + { "Storage", StorageChallenge }, }; private static readonly Dictionary ParsedChallenges = new Dictionary() @@ -604,7 +626,16 @@ public void BearerTokenChallengeAuthenticationPolicy_GatedConcurrentCallsCancell { "CaeInsufficientClaims", ParsedCaeInsufficientClaimsChallenge }, { "CaeSessionsRevoked", ParsedCaeSessionsRevokedClaimsChallenge }, { "KeyVault", ParsedKeyVaultChallenge }, - { "Arm", ParsedArmChallenge } + { "Arm", ParsedArmChallenge }, + { "Storage", ParsedStorageChallenge } + }; + + private static readonly List MultipleParsedChallenges = new List() + { + { ParsedCaeInsufficientClaimsChallenge }, + { ParsedCaeSessionsRevokedClaimsChallenge }, + { ParsedKeyVaultChallenge }, + { ParsedArmChallenge }, }; private class Challenge @@ -615,21 +646,21 @@ private class Challenge } [Test] - public void BearerTokenChallengeAuthenticationPolicy_ValidateChallengeParsing([Values("CaeInsufficientClaims", "CaeSessionsRevoked", "KeyVault", "Arm")] string challengeKey) + public void BearerTokenChallengeAuthenticationPolicy_ValidateChallengeParsing([Values("CaeInsufficientClaims", "CaeSessionsRevoked", "KeyVault", "Arm", "Storage")] string challengeKey) { - var challenge = ChallengeStrings[challengeKey]; + var challenge = ChallengeStrings[challengeKey].AsSpan(); List parsedChallenges = new List(); - foreach (var (ChallengeKey, ChallengeParameters) in BearerTokenChallengeAuthenticationPolicy.ParseChallenges(challenge)) + while (BearerTokenChallengeAuthenticationPolicy.TryGetNextChallenge(ref challenge, out var scheme, out var parameters)) { Challenge parsedChallenge = new Challenge(); - parsedChallenge.Scheme = ChallengeKey; + parsedChallenge.Scheme = scheme.ToString(); - foreach (var paramTuple in BearerTokenChallengeAuthenticationPolicy.ParseChallengeParameters(ChallengeParameters)) + while (BearerTokenChallengeAuthenticationPolicy.TryGetNextParameter(ref challenge, out var key, out var value)) { - parsedChallenge.Parameters.Add(paramTuple); + parsedChallenge.Parameters.Add((key.ToString(), value.ToString())); } parsedChallenges.Add(parsedChallenge); @@ -640,6 +671,35 @@ public void BearerTokenChallengeAuthenticationPolicy_ValidateChallengeParsing([V ValidateParsedChallenge(ParsedChallenges[challengeKey], parsedChallenges[0]); } + [Test] + public void BearerTokenChallengeAuthenticationPolicy_ValidateChallengeParsingWithMultipleChallenges() + { + var challenge = string.Join(", ", new[] { CaeInsufficientClaimsChallenge, CaeSessionsRevokedClaimsChallenge, KeyVaultChallenge, ArmChallenge }).AsSpan(); + + List parsedChallenges = new List(); + + while (BearerTokenChallengeAuthenticationPolicy.TryGetNextChallenge(ref challenge, out var scheme, out var parameters)) + { + Challenge parsedChallenge = new Challenge(); + + parsedChallenge.Scheme = scheme.ToString(); + + while (BearerTokenChallengeAuthenticationPolicy.TryGetNextParameter(ref challenge, out var key, out var value)) + { + parsedChallenge.Parameters.Add((key.ToString(), value.ToString())); + } + + parsedChallenges.Add(parsedChallenge); + } + + Assert.AreEqual(MultipleParsedChallenges.Count, parsedChallenges.Count); + + for (int i = 0; i < parsedChallenges.Count; i++) + { + ValidateParsedChallenge(MultipleParsedChallenges[i], parsedChallenges[i]); + } + } + [Test] public async Task BearerTokenChallengeAuthenticationPolicy_ValidateClaimsChallengeTokenRequest() { From fd14befe1dbad8ea7f7c6d1482e85dcb243604a4 Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Wed, 3 Feb 2021 18:26:19 -0600 Subject: [PATCH 19/22] remove RegEx fields --- .../src/Shared/BearerTokenChallengeAuthenticationPolicy.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs index ce0f8ae27758b..2488359126c57 100644 --- a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs +++ b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs @@ -19,12 +19,7 @@ namespace Azure.Core.Pipeline /// internal class BearerTokenChallengeAuthenticationPolicy : HttpPipelinePolicy { - private const string AuthenticationChallengePattern = @"(\w+) ((?:\w+="".*?""(?:, )?)+)(?:, )?"; - private const string ChallengeParameterPattern = @"(?:(\w+)=""([^""]*)"")+"; private const string ChallengeHeader = "WWW-Authenticate"; - private static readonly Regex s_AuthenticationChallengeRegex = new Regex(AuthenticationChallengePattern, RegexOptions.Compiled); - private static readonly Regex s_ChallengeParameterRegex = new Regex(ChallengeParameterPattern, RegexOptions.Compiled); - private readonly AccessTokenCache _accessTokenCache; private string[] _scopes; From 432b11d50452c212a7dd12eb25426aa1076d5f55 Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Thu, 4 Feb 2021 08:31:27 -0600 Subject: [PATCH 20/22] small tweaks --- .../BearerTokenChallengeAuthenticationPolicy.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs index 2488359126c57..b527090d14d77 100644 --- a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs +++ b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs @@ -217,24 +217,22 @@ public static bool TryGetNextParameter(ref ReadOnlySpan parameters, out Re parameters = parameters.TrimStart(spaceOrComma); int nextSpace = parameters.IndexOf(' '); - int nextEquals = parameters.IndexOf('='); + int nextSeparator = parameters.IndexOf(separator); - if (nextSpace < nextEquals && nextSpace != -1) + if (nextSpace < nextSeparator && nextSpace != -1) { // we encountered another challenge value. return false; } - // Get the index of the first separator - var separatorIndex = parameters.IndexOf(separator); - if (separatorIndex < 0) + if (nextSeparator < 0) return false; // Get the paramKey. - paramKey = parameters.Slice(0, separatorIndex).Trim(); + paramKey = parameters.Slice(0, nextSeparator).Trim(); // Slice to remove the 'paramKey=' from the parameters. - parameters = parameters.Slice(separatorIndex + 1); + parameters = parameters.Slice(nextSeparator + 1); // The start of paramValue will usually be a quoted string. Find the first quote. int quoteIndex = parameters.IndexOf('\"'); From 944dcb6bf73888bbe9c104c63f434fe337f149a9 Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Thu, 4 Feb 2021 13:18:00 -0600 Subject: [PATCH 21/22] pr comments --- ...earerTokenChallengeAuthenticationPolicy.cs | 97 +++++++++---------- ...TokenChallengeAuthenticationPolicyTests.cs | 4 +- 2 files changed, 47 insertions(+), 54 deletions(-) diff --git a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs index b527090d14d77..1a1e5d44d1fa6 100644 --- a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs +++ b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Linq; using System.Net; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Azure.Core.Diagnostics; @@ -16,6 +15,7 @@ namespace Azure.Core.Pipeline { /// /// A policy that sends an provided by a as an Authentication header. + /// Note: This class is currently in preview and is therefore subject to possible future breaking changes. /// internal class BearerTokenChallengeAuthenticationPolicy : HttpPipelinePolicy { @@ -168,7 +168,7 @@ private async Task AuthenticateRequestAsync(HttpMessage message, TokenRequestCon ReadOnlySpan headerSpan = headerValue.AsSpan(); // Iterate through each challenge value. - while (TryGetNextChallenge(ref headerSpan, out var challengeKey, out var challengeParameters)) + while (TryGetNextChallenge(ref headerSpan, out var challengeKey)) { // Enumerate each key=value parameter until we find the 'claims' key on the 'Bearer' challenge. while (TryGetNextParameter(ref headerSpan, out var key, out var value)) @@ -183,10 +183,20 @@ private async Task AuthenticateRequestAsync(HttpMessage message, TokenRequestCon return null; } - internal static bool TryGetNextChallenge(ref ReadOnlySpan headerValue, out ReadOnlySpan challengeKey, out ReadOnlySpan challengeValue) + /// + /// Iterates through the challenge schemes present in a challenge header. + /// + /// + /// The header value which will be sliced to remove the first parsed . + /// + /// The parsed challenge scheme. + /// + /// true if a challenge scheme was successfully parsed. + /// The value of should be passed to to parse the challenge parameters if true. + /// + internal static bool TryGetNextChallenge(ref ReadOnlySpan headerValue, out ReadOnlySpan challengeKey) { challengeKey = default; - challengeValue = default; headerValue = headerValue.TrimStart(' '); int endOfChallengeKey = headerValue.IndexOf(' '); @@ -198,26 +208,35 @@ internal static bool TryGetNextChallenge(ref ReadOnlySpan headerValue, out challengeKey = headerValue.Slice(0, endOfChallengeKey); - // Get the parameters string. - challengeValue = headerValue.Slice(endOfChallengeKey + 1); - // Slice the challenge key from the headerValue headerValue = headerValue.Slice(endOfChallengeKey + 1); return true; } - public static bool TryGetNextParameter(ref ReadOnlySpan parameters, out ReadOnlySpan paramKey, out ReadOnlySpan paramValue, char separator = '=') + /// + /// Iterates through a challenge header value after being parsed by . + /// + /// The header value after being parsed by . + /// The parsed challenge parameter key. + /// The parsed challenge parameter value. + /// The challenge parameter key / value pair separator. The default is '='. + /// + /// true if the next available challenge parameter was successfully parsed. + /// false if there are no more parameters for the current challenge scheme or an additional challenge scheme was encountered in the . + /// The value of should be passed again to to attempt to parse any additional challenge schemes if false. + /// + internal static bool TryGetNextParameter(ref ReadOnlySpan headerValue, out ReadOnlySpan paramKey, out ReadOnlySpan paramValue, char separator = '=') { paramKey = default; paramValue = default; var spaceOrComma = " ,".AsSpan(); // Trim any separater prefixes. - parameters = parameters.TrimStart(spaceOrComma); + headerValue = headerValue.TrimStart(spaceOrComma); - int nextSpace = parameters.IndexOf(' '); - int nextSeparator = parameters.IndexOf(separator); + int nextSpace = headerValue.IndexOf(' '); + int nextSeparator = headerValue.IndexOf(separator); if (nextSpace < nextSeparator && nextSpace != -1) { @@ -229,39 +248,39 @@ public static bool TryGetNextParameter(ref ReadOnlySpan parameters, out Re return false; // Get the paramKey. - paramKey = parameters.Slice(0, nextSeparator).Trim(); + paramKey = headerValue.Slice(0, nextSeparator).Trim(); // Slice to remove the 'paramKey=' from the parameters. - parameters = parameters.Slice(nextSeparator + 1); + headerValue = headerValue.Slice(nextSeparator + 1); // The start of paramValue will usually be a quoted string. Find the first quote. - int quoteIndex = parameters.IndexOf('\"'); + int quoteIndex = headerValue.IndexOf('\"'); // Get the paramValue, which is delimited by the trailing quote. - parameters = parameters.Slice(quoteIndex + 1); + headerValue = headerValue.Slice(quoteIndex + 1); if (quoteIndex >= 0) { // The values are quote wrapped - paramValue = parameters.Slice(0, parameters.IndexOf('\"')); + paramValue = headerValue.Slice(0, headerValue.IndexOf('\"')); } else { //the values are not quote wrapped (storage is one example of this) // either find the next space indicating the delimiter to the next value, or go to the end since this is the last value. - int trailingDelimiterIndex = parameters.IndexOfAny(spaceOrComma); + int trailingDelimiterIndex = headerValue.IndexOfAny(spaceOrComma); if (trailingDelimiterIndex >= 0) { - paramValue = parameters.Slice(0, trailingDelimiterIndex); + paramValue = headerValue.Slice(0, trailingDelimiterIndex); } else { - paramValue = parameters; + paramValue = headerValue; } } // Slice to remove the '"paramValue"' from the parameters. - if (parameters != paramValue) - parameters = parameters.Slice(paramValue.Length + 1); + if (headerValue != paramValue) + headerValue = headerValue.Slice(paramValue.Length + 1); return true; } @@ -357,7 +376,7 @@ public async ValueTask GetHeaderValueAsync(HttpMessage message, TokenReq return info.HeaderValue; } - private (TaskCompletionSource tcs, TaskCompletionSource? backgroundUpdateTcs, bool getTokenFromCredential) GetTaskCompletionSources(TokenRequestContext context) + private (TaskCompletionSource, TaskCompletionSource?, bool) GetTaskCompletionSources(TokenRequestContext context) { lock (_syncObj) { @@ -405,36 +424,10 @@ public async ValueTask GetHeaderValueAsync(HttpMessage message, TokenReq } // must be called under lock (_syncObj) - private bool RequestRequiresNewToken(TokenRequestContext context) - { - if (_currentContext == null) - { - return true; - } - - if (context.Scopes != null) - { - if (context.Scopes.Length != _currentContext.Value.Scopes?.Length) - { - return true; - } - - for (int i = 0; i < context.Scopes.Length; i++) - { - if (context.Scopes[i] != _currentContext.Value.Scopes?[i]) - { - return true; - } - } - } - - if ((context.Claims != null) && (context.Claims != _currentContext.Value.Claims)) - { - return true; - } - - return false; - } + private bool RequestRequiresNewToken(TokenRequestContext context) => + _currentContext == null || + (context.Scopes != null && !context.Scopes.SequenceEqual(_currentContext.Value.Scopes)) || + (context.Claims != null && !string.Equals(context.Claims, _currentContext.Value.Claims)); private async ValueTask GetHeaderValueFromCredentialInBackgroundAsync(TaskCompletionSource backgroundUpdateTcs, HeaderValueInfo info, TokenRequestContext context, bool async) { diff --git a/sdk/core/Azure.Core/tests/BearerTokenChallengeAuthenticationPolicyTests.cs b/sdk/core/Azure.Core/tests/BearerTokenChallengeAuthenticationPolicyTests.cs index 1eb65b7c08374..a9bbf035b6885 100644 --- a/sdk/core/Azure.Core/tests/BearerTokenChallengeAuthenticationPolicyTests.cs +++ b/sdk/core/Azure.Core/tests/BearerTokenChallengeAuthenticationPolicyTests.cs @@ -652,7 +652,7 @@ public void BearerTokenChallengeAuthenticationPolicy_ValidateChallengeParsing([V List parsedChallenges = new List(); - while (BearerTokenChallengeAuthenticationPolicy.TryGetNextChallenge(ref challenge, out var scheme, out var parameters)) + while (BearerTokenChallengeAuthenticationPolicy.TryGetNextChallenge(ref challenge, out var scheme)) { Challenge parsedChallenge = new Challenge(); @@ -678,7 +678,7 @@ public void BearerTokenChallengeAuthenticationPolicy_ValidateChallengeParsingWit List parsedChallenges = new List(); - while (BearerTokenChallengeAuthenticationPolicy.TryGetNextChallenge(ref challenge, out var scheme, out var parameters)) + while (BearerTokenChallengeAuthenticationPolicy.TryGetNextChallenge(ref challenge, out var scheme)) { Challenge parsedChallenge = new Challenge(); From e63491125e0bd811f6494f2e1649e5ff947f8223 Mon Sep 17 00:00:00 2001 From: Christopher Scott Date: Thu, 4 Feb 2021 13:22:05 -0600 Subject: [PATCH 22/22] delete AzureCoreSharedEventSource --- sdk/core/Azure.Core/src/Azure.Core.csproj | 1 - .../src/Shared/AzureCoreSharedEventSource.cs | 25 ------------------- ...earerTokenChallengeAuthenticationPolicy.cs | 4 +-- 3 files changed, 2 insertions(+), 28 deletions(-) delete mode 100644 sdk/core/Azure.Core/src/Shared/AzureCoreSharedEventSource.cs diff --git a/sdk/core/Azure.Core/src/Azure.Core.csproj b/sdk/core/Azure.Core/src/Azure.Core.csproj index 25fa99c96dade..6125e5d33ea21 100644 --- a/sdk/core/Azure.Core/src/Azure.Core.csproj +++ b/sdk/core/Azure.Core/src/Azure.Core.csproj @@ -26,7 +26,6 @@ - diff --git a/sdk/core/Azure.Core/src/Shared/AzureCoreSharedEventSource.cs b/sdk/core/Azure.Core/src/Shared/AzureCoreSharedEventSource.cs deleted file mode 100644 index 510d2948e70cd..0000000000000 --- a/sdk/core/Azure.Core/src/Shared/AzureCoreSharedEventSource.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Diagnostics.Tracing; - -namespace Azure.Core.Diagnostics -{ - [EventSource(Name = EventSourceName)] - internal sealed class AzureCoreSharedEventSource : EventSource - { - private const string EventSourceName = "Azure-Core-Shared"; - - private const int BackgroundRefreshFailedEvent = 19; - - private AzureCoreSharedEventSource() : base(EventSourceName, EventSourceSettings.Default, AzureEventSourceListener.TraitName, AzureEventSourceListener.TraitValue) { } - - public static AzureCoreSharedEventSource Singleton { get; } = new AzureCoreSharedEventSource(); - - [Event(BackgroundRefreshFailedEvent, Level = EventLevel.Informational, Message = "Background token refresh [{0}] failed with exception {1}")] - public void BackgroundRefreshFailed(string requestId, string exception) - { - WriteEvent(BackgroundRefreshFailedEvent, requestId, exception); - } - } -} diff --git a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs index 1a1e5d44d1fa6..f3a85b89fbc0b 100644 --- a/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs +++ b/sdk/core/Azure.Core/src/Shared/BearerTokenChallengeAuthenticationPolicy.cs @@ -440,12 +440,12 @@ private async ValueTask GetHeaderValueFromCredentialInBackgroundAsync(TaskComple catch (OperationCanceledException oce) when (cts.IsCancellationRequested) { backgroundUpdateTcs.SetResult(new HeaderValueInfo(info.HeaderValue, info.ExpiresOn, DateTimeOffset.UtcNow)); - AzureCoreSharedEventSource.Singleton.BackgroundRefreshFailed(context.ParentRequestId ?? string.Empty, oce.ToString()); + AzureCoreEventSource.Singleton.BackgroundRefreshFailed(context.ParentRequestId ?? string.Empty, oce.ToString()); } catch (Exception e) { backgroundUpdateTcs.SetResult(new HeaderValueInfo(info.HeaderValue, info.ExpiresOn, DateTimeOffset.UtcNow + _tokenRefreshRetryDelay)); - AzureCoreSharedEventSource.Singleton.BackgroundRefreshFailed(context.ParentRequestId ?? string.Empty, e.ToString()); + AzureCoreEventSource.Singleton.BackgroundRefreshFailed(context.ParentRequestId ?? string.Empty, e.ToString()); } finally {