diff --git a/Microsoft.Azure.Cosmos/src/AuthorizationHelper.cs b/Microsoft.Azure.Cosmos/src/Authorization/AuthorizationHelper.cs similarity index 100% rename from Microsoft.Azure.Cosmos/src/AuthorizationHelper.cs rename to Microsoft.Azure.Cosmos/src/Authorization/AuthorizationHelper.cs diff --git a/Microsoft.Azure.Cosmos/src/Authorization/AuthorizationTokenProvider.cs b/Microsoft.Azure.Cosmos/src/Authorization/AuthorizationTokenProvider.cs new file mode 100644 index 0000000000..0121b59498 --- /dev/null +++ b/Microsoft.Azure.Cosmos/src/Authorization/AuthorizationTokenProvider.cs @@ -0,0 +1,73 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +//------------------------------------------------------------ + +namespace Microsoft.Azure.Cosmos +{ + using System; + using System.Globalization; + using System.Net.Http; + using System.Text; + using System.Threading.Tasks; + using Microsoft.Azure.Documents; + using Microsoft.Azure.Documents.Collections; + + internal abstract class AuthorizationTokenProvider : ICosmosAuthorizationTokenProvider, IAuthorizationTokenProvider, IDisposable + { + public async Task AddSystemAuthorizationHeaderAsync( + DocumentServiceRequest request, + string federationId, + string verb, + string resourceId) + { + request.Headers[HttpConstants.HttpHeaders.XDate] = DateTime.UtcNow.ToString("r", CultureInfo.InvariantCulture); + + request.Headers[HttpConstants.HttpHeaders.Authorization] = (await this.GetUserAuthorizationAsync( + resourceId ?? request.ResourceAddress, + PathsHelper.GetResourcePath(request.ResourceType), + verb, + request.Headers, + request.RequestAuthorizationTokenType)).token; + } + + public abstract ValueTask AddAuthorizationHeaderAsync( + INameValueCollection headersCollection, + Uri requestAddress, + string verb, + AuthorizationTokenType tokenType); + + public abstract ValueTask<(string token, string payload)> GetUserAuthorizationAsync( + string resourceAddress, + string resourceType, + string requestVerb, + INameValueCollection headers, + AuthorizationTokenType tokenType); + + public abstract ValueTask GetUserAuthorizationTokenAsync( + string resourceAddress, + string resourceType, + string requestVerb, + INameValueCollection headers, + AuthorizationTokenType tokenType, + CosmosDiagnosticsContext diagnosticsContext); + + public abstract void TraceUnauthorized( + DocumentClientException dce, + string authorizationToken, + string payload); + + public static AuthorizationTokenProvider CreateWithResourceTokenOrAuthKey(string authKeyOrResourceToken) + { + if (AuthorizationHelper.IsResourceToken(authKeyOrResourceToken)) + { + return new AuthorizationTokenProviderResourceToken(authKeyOrResourceToken); + } + else + { + return new AuthorizationTokenProviderMasterKey(authKeyOrResourceToken); + } + } + + public abstract void Dispose(); + } +} diff --git a/Microsoft.Azure.Cosmos/src/Authorization/AuthorizationTokenProviderMasterKey.cs b/Microsoft.Azure.Cosmos/src/Authorization/AuthorizationTokenProviderMasterKey.cs new file mode 100644 index 0000000000..3cabb1e795 --- /dev/null +++ b/Microsoft.Azure.Cosmos/src/Authorization/AuthorizationTokenProviderMasterKey.cs @@ -0,0 +1,202 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +//------------------------------------------------------------ + +namespace Microsoft.Azure.Cosmos +{ + using System; + using System.Globalization; + using System.Net; + using System.Net.Http; + using System.Security; + using System.Text; + using System.Threading.Tasks; + using Microsoft.Azure.Cosmos.Core.Trace; + using Microsoft.Azure.Documents; + using Microsoft.Azure.Documents.Collections; + + internal sealed class AuthorizationTokenProviderMasterKey : AuthorizationTokenProvider + { + ////The MAC signature found in the HTTP request is not the same as the computed signature.Server used following string to sign + ////The input authorization token can't serve the request. Please check that the expected payload is built as per the protocol, and check the key being used. Server used the following payload to sign + private const string MacSignatureString = "to sign"; + private const string EnableAuthFailureTracesConfig = "enableAuthFailureTraces"; + private readonly Lazy enableAuthFailureTraces; + private readonly IComputeHash authKeyHashFunction; + private bool isDisposed = false; + + public AuthorizationTokenProviderMasterKey(IComputeHash computeHash) + { + this.authKeyHashFunction = computeHash ?? throw new ArgumentNullException(nameof(computeHash)); + this.enableAuthFailureTraces = new Lazy(() => + { +#if NETSTANDARD20 + // GetEntryAssembly returns null when loaded from native netstandard2.0 + if (System.Reflection.Assembly.GetEntryAssembly() == null) + { + return false; + } +#endif + string enableAuthFailureTracesString = System.Configuration.ConfigurationManager.AppSettings[EnableAuthFailureTracesConfig]; + if (string.IsNullOrEmpty(enableAuthFailureTracesString) || + !bool.TryParse(enableAuthFailureTracesString, out bool enableAuthFailureTracesFlag)) + { + return false; + } + + return enableAuthFailureTracesFlag; + }); + } + + public AuthorizationTokenProviderMasterKey(SecureString authKey) + : this(new SecureStringHMACSHA256Helper(authKey)) + { + } + + public AuthorizationTokenProviderMasterKey(string authKey) + : this(new StringHMACSHA256Hash(authKey)) + { + } + + public override ValueTask<(string token, string payload)> GetUserAuthorizationAsync( + string resourceAddress, + string resourceType, + string requestVerb, + INameValueCollection headers, + AuthorizationTokenType tokenType) + { + // this is masterkey authZ + headers[HttpConstants.HttpHeaders.XDate] = DateTime.UtcNow.ToString("r", CultureInfo.InvariantCulture); + + string authorizationToken = AuthorizationHelper.GenerateKeyAuthorizationSignature( + requestVerb, + resourceAddress, + resourceType, + headers, + this.authKeyHashFunction, + out AuthorizationHelper.ArrayOwner arrayOwner); + + using (arrayOwner) + { + string payload = null; + if (arrayOwner.Buffer.Count > 0) + { + payload = Encoding.UTF8.GetString(arrayOwner.Buffer.Array, arrayOwner.Buffer.Offset, (int)arrayOwner.Buffer.Count); + } + + return new ValueTask<(string token, string payload)>((authorizationToken, payload)); + } + } + + public override ValueTask GetUserAuthorizationTokenAsync( + string resourceAddress, + string resourceType, + string requestVerb, + INameValueCollection headers, + AuthorizationTokenType tokenType, + CosmosDiagnosticsContext diagnosticsContext) + { + // this is masterkey authZ + headers[HttpConstants.HttpHeaders.XDate] = DateTime.UtcNow.ToString("r", CultureInfo.InvariantCulture); + + string authorizationToken = AuthorizationHelper.GenerateKeyAuthorizationSignature( + requestVerb, + resourceAddress, + resourceType, + headers, + this.authKeyHashFunction, + out AuthorizationHelper.ArrayOwner arrayOwner); + + using (arrayOwner) + { + return new ValueTask(authorizationToken); + } + } + + public override ValueTask AddAuthorizationHeaderAsync( + INameValueCollection headersCollection, + Uri requestAddress, + string verb, + AuthorizationTokenType tokenType) + { + string dateTime = DateTime.UtcNow.ToString("r", CultureInfo.InvariantCulture); + headersCollection[HttpConstants.HttpHeaders.XDate] = dateTime; + + string token = AuthorizationHelper.GenerateKeyAuthorizationSignature( + verb, + requestAddress, + headersCollection, + this.authKeyHashFunction); + + headersCollection.Add(HttpConstants.HttpHeaders.Authorization, token); + return default; + } + + public override void TraceUnauthorized( + DocumentClientException dce, + string authorizationToken, + string payload) + { + if (payload != null + && dce.Message != null + && dce.StatusCode.HasValue + && dce.StatusCode.Value == HttpStatusCode.Unauthorized + && dce.Message.Contains(AuthorizationTokenProviderMasterKey.MacSignatureString)) + { + // The following code is added such that we get trace data on unexpected 401/HMAC errors and it is + // disabled by default. The trace will be trigger only when "enableAuthFailureTraces" named configuration + // is set to true (currently true for CTL runs). + // For production we will work directly with specific customers in order to enable this configuration. + string normalizedPayload = AuthorizationTokenProviderMasterKey.NormalizeAuthorizationPayload(payload); + if (this.enableAuthFailureTraces.Value) + { + string tokenFirst5 = HttpUtility.UrlDecode(authorizationToken).Split('&')[2].Split('=')[1].Substring(0, 5); + ulong authHash = 0; + if (this.authKeyHashFunction?.Key != null) + { + byte[] bytes = Encoding.UTF8.GetBytes(this.authKeyHashFunction?.Key?.ToString()); + authHash = Documents.Routing.MurmurHash3.Hash64(bytes, bytes.Length); + } + DefaultTrace.TraceError("Un-expected authorization payload mis-match. Actual payload={0}, token={1}..., hash={2:X}..., error={3}", + normalizedPayload, tokenFirst5, authHash, dce.Message); + } + else + { + DefaultTrace.TraceError("Un-expected authorization payload mis-match. Actual {0} service expected {1}", normalizedPayload, dce.Message); + } + } + } + + public override void Dispose() + { + if (!this.isDisposed) + { + this.authKeyHashFunction.Dispose(); + this.isDisposed = true; + } + } + + private static string NormalizeAuthorizationPayload(string input) + { + const int expansionBuffer = 12; + StringBuilder builder = new StringBuilder(input.Length + expansionBuffer); + for (int i = 0; i < input.Length; i++) + { + switch (input[i]) + { + case '\n': + builder.Append("\\n"); + break; + case '/': + builder.Append("\\/"); + break; + default: + builder.Append(input[i]); + break; + } + } + + return builder.ToString(); + } + } +} diff --git a/Microsoft.Azure.Cosmos/src/Authorization/AuthorizationTokenProviderResourceToken.cs b/Microsoft.Azure.Cosmos/src/Authorization/AuthorizationTokenProviderResourceToken.cs new file mode 100644 index 0000000000..4c4963933e --- /dev/null +++ b/Microsoft.Azure.Cosmos/src/Authorization/AuthorizationTokenProviderResourceToken.cs @@ -0,0 +1,75 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +//------------------------------------------------------------ + +namespace Microsoft.Azure.Cosmos +{ + using System; + using System.Net.Http; + using System.Threading.Tasks; + using Microsoft.Azure.Cosmos.Core.Trace; + using Microsoft.Azure.Documents; + using Microsoft.Azure.Documents.Collections; + + internal sealed class AuthorizationTokenProviderResourceToken : AuthorizationTokenProvider + { + private readonly string urlEncodedAuthKeyResourceToken; + private readonly ValueTask urlEncodedAuthKeyResourceTokenValueTask; + private readonly ValueTask<(string, string)> urlEncodedAuthKeyResourceTokenValueTaskWithPayload; + private readonly ValueTask defaultValueTask; + + public AuthorizationTokenProviderResourceToken( + string authKeyResourceToken) + { + this.urlEncodedAuthKeyResourceToken = HttpUtility.UrlEncode(authKeyResourceToken); + this.urlEncodedAuthKeyResourceTokenValueTask = new ValueTask(this.urlEncodedAuthKeyResourceToken); + this.urlEncodedAuthKeyResourceTokenValueTaskWithPayload = new ValueTask<(string, string)>((this.urlEncodedAuthKeyResourceToken, default)); + this.defaultValueTask = new ValueTask(); + } + + public override ValueTask<(string token, string payload)> GetUserAuthorizationAsync( + string resourceAddress, + string resourceType, + string requestVerb, + INameValueCollection headers, + AuthorizationTokenType tokenType) + { + // If the input auth token is a resource token, then use it as a bearer-token. + return this.urlEncodedAuthKeyResourceTokenValueTaskWithPayload; + } + + public override ValueTask GetUserAuthorizationTokenAsync( + string resourceAddress, + string resourceType, + string requestVerb, + INameValueCollection headers, + AuthorizationTokenType tokenType, + CosmosDiagnosticsContext diagnosticsContext) + { + // If the input auth token is a resource token, then use it as a bearer-token. + return this.urlEncodedAuthKeyResourceTokenValueTask; + } + + public override ValueTask AddAuthorizationHeaderAsync( + INameValueCollection headersCollection, + Uri requestAddress, + string verb, + AuthorizationTokenType tokenType) + { + headersCollection.Add(HttpConstants.HttpHeaders.Authorization, this.urlEncodedAuthKeyResourceToken); + return this.defaultValueTask; + } + + public override void TraceUnauthorized( + DocumentClientException dce, + string authorizationToken, + string payload) + { + DefaultTrace.TraceError($"Un-expected authorization for resource token. {dce.Message}"); + } + + public override void Dispose() + { + } + } +} diff --git a/Microsoft.Azure.Cosmos/src/Authorization/AuthorizationTokenProviderTokenCredential.cs b/Microsoft.Azure.Cosmos/src/Authorization/AuthorizationTokenProviderTokenCredential.cs new file mode 100644 index 0000000000..6b51fcc2eb --- /dev/null +++ b/Microsoft.Azure.Cosmos/src/Authorization/AuthorizationTokenProviderTokenCredential.cs @@ -0,0 +1,94 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +//------------------------------------------------------------ + +namespace Microsoft.Azure.Cosmos +{ + using System; + using System.Globalization; + using System.Threading.Tasks; + using global::Azure.Core; + using Microsoft.Azure.Cosmos.Core.Trace; + using Microsoft.Azure.Documents; + using Microsoft.Azure.Documents.Collections; + + internal sealed class AuthorizationTokenProviderTokenCredential : AuthorizationTokenProvider + { + internal readonly TokenCredentialCache tokenCredentialCache; + private bool isDisposed = false; + + public AuthorizationTokenProviderTokenCredential( + TokenCredential tokenCredential, + string accountEndpointHost, + TimeSpan? backgroundTokenCredentialRefreshInterval) + { + this.tokenCredentialCache = new TokenCredentialCache( + tokenCredential, + accountEndpointHost, + backgroundTokenCredentialRefreshInterval); + } + + public override async ValueTask<(string token, string payload)> GetUserAuthorizationAsync( + string resourceAddress, + string resourceType, + string requestVerb, + INameValueCollection headers, + AuthorizationTokenType tokenType) + { + string token = AuthorizationTokenProviderTokenCredential.GenerateAadAuthorizationSignature( + await this.tokenCredentialCache.GetTokenAsync(EmptyCosmosDiagnosticsContext.Singleton)); + return (token, default); + } + + public override async ValueTask GetUserAuthorizationTokenAsync( + string resourceAddress, + string resourceType, + string requestVerb, + INameValueCollection headers, + AuthorizationTokenType tokenType, + CosmosDiagnosticsContext diagnosticsContext) + { + return AuthorizationTokenProviderTokenCredential.GenerateAadAuthorizationSignature( + await this.tokenCredentialCache.GetTokenAsync(diagnosticsContext)); + } + + public override async ValueTask AddAuthorizationHeaderAsync( + INameValueCollection headersCollection, + Uri requestAddress, + string verb, + AuthorizationTokenType tokenType) + { + string token = AuthorizationTokenProviderTokenCredential.GenerateAadAuthorizationSignature( + await this.tokenCredentialCache.GetTokenAsync(EmptyCosmosDiagnosticsContext.Singleton)); + + headersCollection.Add(HttpConstants.HttpHeaders.Authorization, token); + } + + public override void TraceUnauthorized( + DocumentClientException dce, + string authorizationToken, + string payload) + { + DefaultTrace.TraceError($"Un-expected authorization for token credential. {dce.Message}"); + } + + public static string GenerateAadAuthorizationSignature(string aadToken) + { + return HttpUtility.UrlEncode(string.Format( + CultureInfo.InvariantCulture, + Constants.Properties.AuthorizationFormat, + Constants.Properties.AadToken, + Constants.Properties.TokenVersion, + aadToken)); + } + + public override void Dispose() + { + if (!this.isDisposed) + { + this.isDisposed = true; + this.tokenCredentialCache.Dispose(); + } + } + } +} diff --git a/Microsoft.Azure.Cosmos/src/IComputeHash.cs b/Microsoft.Azure.Cosmos/src/Authorization/IComputeHash.cs similarity index 100% rename from Microsoft.Azure.Cosmos/src/IComputeHash.cs rename to Microsoft.Azure.Cosmos/src/Authorization/IComputeHash.cs diff --git a/Microsoft.Azure.Cosmos/src/ICosmosAuthorizationTokenProvider.cs b/Microsoft.Azure.Cosmos/src/Authorization/ICosmosAuthorizationTokenProvider.cs similarity index 80% rename from Microsoft.Azure.Cosmos/src/ICosmosAuthorizationTokenProvider.cs rename to Microsoft.Azure.Cosmos/src/Authorization/ICosmosAuthorizationTokenProvider.cs index 08d91dd357..f7c47e73f1 100644 --- a/Microsoft.Azure.Cosmos/src/ICosmosAuthorizationTokenProvider.cs +++ b/Microsoft.Azure.Cosmos/src/Authorization/ICosmosAuthorizationTokenProvider.cs @@ -4,6 +4,7 @@ namespace Microsoft.Azure.Cosmos { + using System.Threading.Tasks; using Microsoft.Azure.Documents; using Microsoft.Azure.Documents.Collections; @@ -16,11 +17,12 @@ internal interface ICosmosAuthorizationTokenProvider /// /// Generates a Authorization Token for a given resource type, address and action. /// - string GetUserAuthorizationToken( + ValueTask GetUserAuthorizationTokenAsync( string resourceAddress, string resourceType, string requestVerb, INameValueCollection headers, - AuthorizationTokenType tokenType); + AuthorizationTokenType tokenType, + CosmosDiagnosticsContext diagnosticsContext); } } diff --git a/Microsoft.Azure.Cosmos/src/MurmurHash3.cs b/Microsoft.Azure.Cosmos/src/Authorization/MurmurHash3.cs similarity index 100% rename from Microsoft.Azure.Cosmos/src/MurmurHash3.cs rename to Microsoft.Azure.Cosmos/src/Authorization/MurmurHash3.cs diff --git a/Microsoft.Azure.Cosmos/src/SecureStringHMACSHA256Helper.cs b/Microsoft.Azure.Cosmos/src/Authorization/SecureStringHMACSHA256Helper.cs similarity index 100% rename from Microsoft.Azure.Cosmos/src/SecureStringHMACSHA256Helper.cs rename to Microsoft.Azure.Cosmos/src/Authorization/SecureStringHMACSHA256Helper.cs diff --git a/Microsoft.Azure.Cosmos/src/StringHMACSHA256Hash.cs b/Microsoft.Azure.Cosmos/src/Authorization/StringHMACSHA256Hash.cs similarity index 100% rename from Microsoft.Azure.Cosmos/src/StringHMACSHA256Hash.cs rename to Microsoft.Azure.Cosmos/src/Authorization/StringHMACSHA256Hash.cs diff --git a/Microsoft.Azure.Cosmos/src/Authorization/TokenCredentialCache.cs b/Microsoft.Azure.Cosmos/src/Authorization/TokenCredentialCache.cs new file mode 100644 index 0000000000..035b6d1a85 --- /dev/null +++ b/Microsoft.Azure.Cosmos/src/Authorization/TokenCredentialCache.cs @@ -0,0 +1,252 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +//------------------------------------------------------------ + +namespace Microsoft.Azure.Cosmos +{ + using System; + using System.Diagnostics; + using System.Net; + using System.Threading; + using System.Threading.Tasks; + using global::Azure; + using global::Azure.Core; + using Microsoft.Azure.Cosmos.Core.Trace; + using Microsoft.Azure.Cosmos.Diagnostics; + using Microsoft.Azure.Cosmos.Resource.CosmosExceptions; + using Microsoft.Azure.Documents; + + /// + /// This is a token credential cache. + /// It starts a background task that refreshes the token at a set interval. + /// This way refreshing the token does not cause additional latency and also + /// allows for transient issue to resolve before the token expires. + /// + internal sealed class TokenCredentialCache : IDisposable + { + // Default token expiration time is 1hr. + // Making the default 25% of the token life span. This gives 75% of the tokens life for transient error + // to get resolved before the token expires. + public static readonly double DefaultBackgroundTokenCredentialRefreshIntervalPercentage = .25; + + private const string ScopeFormat = "https://{0}/.default"; + private readonly TokenRequestContext tokenRequestContext; + private readonly TokenCredential tokenCredential; + private readonly CancellationTokenSource cancellationTokenSource; + private readonly CancellationToken cancellationToken; + private readonly TimeSpan? userDefinedBackgroundTokenCredentialRefreshInterval; + + private readonly SemaphoreSlim getTokenRefreshLock = new SemaphoreSlim(1); + private readonly SemaphoreSlim backgroundRefreshLock = new SemaphoreSlim(1); + + private TimeSpan? systemBackgroundTokenCredentialRefreshInterval; + private AccessToken cachedAccessToken; + private bool isBackgroundTaskRunning = false; + private bool isDisposed = false; + + internal TokenCredentialCache( + TokenCredential tokenCredential, + string accountEndpointHost, + TimeSpan? backgroundTokenCredentialRefreshInterval) + { + this.tokenCredential = tokenCredential; + this.tokenRequestContext = new TokenRequestContext(new string[] + { + string.Format(TokenCredentialCache.ScopeFormat, accountEndpointHost) + }); + + this.userDefinedBackgroundTokenCredentialRefreshInterval = backgroundTokenCredentialRefreshInterval; + this.cancellationTokenSource = new CancellationTokenSource(); + this.cancellationToken = this.cancellationTokenSource.Token; + } + + public TimeSpan? BackgroundTokenCredentialRefreshInterval => + this.userDefinedBackgroundTokenCredentialRefreshInterval ?? this.systemBackgroundTokenCredentialRefreshInterval; + + internal async ValueTask GetTokenAsync( + CosmosDiagnosticsContext diagnosticsContext) + { + if (this.isDisposed) + { + throw new ObjectDisposedException("TokenCredentialCache"); + } + + if (this.cachedAccessToken.ExpiresOn <= DateTime.UtcNow) + { + await this.getTokenRefreshLock.WaitAsync(); + + // Don't refresh if another thread already updated it. + if (this.cachedAccessToken.ExpiresOn <= DateTime.UtcNow) + { + try + { + await this.RefreshCachedTokenWithRetryHelperAsync(diagnosticsContext); + this.StartRefreshToken(); + } + finally + { + this.getTokenRefreshLock.Release(); + } + } + } + + return this.cachedAccessToken.Token; + } + + public void Dispose() + { + if (this.isDisposed) + { + return; + } + + this.cancellationTokenSource.Cancel(); + this.cancellationTokenSource.Dispose(); + this.isDisposed = true; + } + + private async ValueTask RefreshCachedTokenWithRetryHelperAsync( + CosmosDiagnosticsContext diagnosticsContext) + { + // A different thread is already updating the access token + bool skipRefreshBecause = this.backgroundRefreshLock.CurrentCount == 1; + await this.backgroundRefreshLock.WaitAsync(); + try + { + // Token was already refreshed successfully from another thread. + if (skipRefreshBecause && this.cachedAccessToken.ExpiresOn > DateTime.UtcNow) + { + return; + } + + Exception lastException = null; + const int totalRetryCount = 3; + for (int retry = 0; retry < totalRetryCount; retry++) + { + if (this.cancellationToken.IsCancellationRequested) + { + DefaultTrace.TraceInformation( + "Stop RefreshTokenWithIndefiniteRetries because cancellation is requested"); + + break; + } + + try + { + using (diagnosticsContext.CreateScope(nameof(this.RefreshCachedTokenWithRetryHelperAsync))) + { + this.cachedAccessToken = await this.tokenCredential.GetTokenAsync( + this.tokenRequestContext, + this.cancellationToken); + if (!this.userDefinedBackgroundTokenCredentialRefreshInterval.HasValue) + { + double totalSecondUntilExpire = (this.cachedAccessToken.ExpiresOn - DateTimeOffset.UtcNow).TotalSeconds * DefaultBackgroundTokenCredentialRefreshIntervalPercentage; + this.systemBackgroundTokenCredentialRefreshInterval = TimeSpan.FromSeconds(totalSecondUntilExpire); + } + + return; + } + } + catch (RequestFailedException requestFailedException) + { + lastException = requestFailedException; + diagnosticsContext.AddDiagnosticsInternal( + new PointOperationStatistics( + activityId: Trace.CorrelationManager.ActivityId.ToString(), + statusCode: (HttpStatusCode)requestFailedException.Status, + subStatusCode: SubStatusCodes.Unknown, + responseTimeUtc: DateTime.UtcNow, + requestCharge: default, + errorMessage: requestFailedException.ToString(), + method: default, + requestUri: null, + requestSessionToken: default, + responseSessionToken: default)); + + DefaultTrace.TraceError($"TokenCredential.GetToken() failed with RequestFailedException. scope = {string.Join(";", this.tokenRequestContext.Scopes)}, retry = {retry}, Exception = {lastException}"); + + // Don't retry on auth failures + if (requestFailedException.Status == (int)HttpStatusCode.Unauthorized || + requestFailedException.Status == (int)HttpStatusCode.Forbidden) + { + this.cachedAccessToken = default; + throw; + } + + } + catch (Exception exception) + { + lastException = exception; + diagnosticsContext.AddDiagnosticsInternal( + new PointOperationStatistics( + activityId: Trace.CorrelationManager.ActivityId.ToString(), + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: SubStatusCodes.Unknown, + responseTimeUtc: DateTime.UtcNow, + requestCharge: default, + errorMessage: exception.ToString(), + method: default, + requestUri: default, + requestSessionToken: default, + responseSessionToken: default)); + + DefaultTrace.TraceError( + $"TokenCredential.GetToken() failed. scope = {string.Join(";", this.tokenRequestContext.Scopes)}, retry = {retry}, Exception = {lastException}"); + } + + DefaultTrace.TraceError( + $"TokenCredential.GetToken() failed. scope = {string.Join(";", this.tokenRequestContext.Scopes)}, retry = {retry}, Exception = {lastException}"); + } + + throw CosmosExceptionFactory.CreateUnauthorizedException( + ClientResources.FailedToGetAadToken, + (int)SubStatusCodes.FailedToGetAadToken, + lastException); + } + finally + { + this.backgroundRefreshLock.Release(); + } + } + +#pragma warning disable VSTHRD100 // Avoid async void methods + private async void StartRefreshToken() +#pragma warning restore VSTHRD100 // Avoid async void methods + { + if (this.isBackgroundTaskRunning) + { + return; + } + + this.isBackgroundTaskRunning = true; + while (!this.cancellationTokenSource.IsCancellationRequested) + { + try + { + if (!this.BackgroundTokenCredentialRefreshInterval.HasValue) + { + throw new ArgumentException(nameof(this.BackgroundTokenCredentialRefreshInterval)); + } + + await Task.Delay(this.BackgroundTokenCredentialRefreshInterval.Value, this.cancellationToken); + + DefaultTrace.TraceInformation("StartRefreshToken() - Invoking refresh"); + + await this.RefreshCachedTokenWithRetryHelperAsync(EmptyCosmosDiagnosticsContext.Singleton); + } + catch (Exception ex) + { + if (this.cancellationTokenSource.IsCancellationRequested && + (ex is TaskCanceledException || ex is ObjectDisposedException)) + { + return; + } + + DefaultTrace.TraceWarning( + "StartRefreshToken() - Unable to refresh token credential cache. Exception: {0}", + ex.ToString()); + } + } + } + } +} diff --git a/Microsoft.Azure.Cosmos/src/ClientResources.Designer.cs b/Microsoft.Azure.Cosmos/src/ClientResources.Designer.cs index 197335b4bc..eee1a9ef8c 100644 --- a/Microsoft.Azure.Cosmos/src/ClientResources.Designer.cs +++ b/Microsoft.Azure.Cosmos/src/ClientResources.Designer.cs @@ -312,6 +312,17 @@ internal static string FailedToEvaluateSpatialExpression { } } + /// + /// Looks up a localized string similar to Failed to get AAD token from the provided Azure.Core.TokenCredential.. + /// + internal static string FailedToGetAadToken + { + get + { + return ResourceManager.GetString("FailedToGetAadToken", resourceCulture); + } + } + /// /// Looks up a localized string similar to Cannot define EffectivePartitionKeyRouting and FeedRange simultaneously.. /// @@ -573,6 +584,15 @@ internal static string StringCompareToInvalidOperator { } } + /// + /// Looks up a localized string similar to Token refresh in progress.. + /// + internal static string TokenRefreshInProgress { + get { + return ResourceManager.GetString("TokenRefreshInProgress", resourceCulture); + } + } + /// /// Looks up a localized string similar to Type check operations can be used in Linq expressions only and are evaluated in Azure CosmosDB server.. /// diff --git a/Microsoft.Azure.Cosmos/src/ClientResources.resx b/Microsoft.Azure.Cosmos/src/ClientResources.resx index 6d2f699b5d..becffe27f7 100644 --- a/Microsoft.Azure.Cosmos/src/ClientResources.resx +++ b/Microsoft.Azure.Cosmos/src/ClientResources.resx @@ -198,6 +198,9 @@ Expression tree cannot be processed because evaluation of Spatial expression failed. + + Failed to get AAD token from the provided Azure.Core.TokenCredential. + Input is not of type IDocumentQuery. @@ -270,6 +273,9 @@ Invalid operator for string.CompareTo(). Vaid operators are ('==', '<', '<=', '>' or '>=') + + Token refresh in progress. + Type check operations can be used in Linq expressions only and are evaluated in Azure CosmosDB server. diff --git a/Microsoft.Azure.Cosmos/src/ConnectionPolicy.cs b/Microsoft.Azure.Cosmos/src/ConnectionPolicy.cs index 33490963ea..ccad93945b 100644 --- a/Microsoft.Azure.Cosmos/src/ConnectionPolicy.cs +++ b/Microsoft.Azure.Cosmos/src/ConnectionPolicy.cs @@ -16,13 +16,13 @@ namespace Microsoft.Azure.Cosmos /// internal sealed class ConnectionPolicy { - internal UserAgentContainer UserAgentContainer; private const int defaultRequestTimeout = 10; // defaultMediaRequestTimeout is based upon the blob client timeout and the retry policy. private const int defaultMediaRequestTimeout = 300; private const int defaultMaxConcurrentFanoutRequests = 32; private const int defaultMaxConcurrentConnectionLimit = 50; + internal UserAgentContainer UserAgentContainer; private static ConnectionPolicy defaultPolicy; private Protocol connectionProtocol; diff --git a/Microsoft.Azure.Cosmos/src/CosmosClient.cs b/Microsoft.Azure.Cosmos/src/CosmosClient.cs index 8448c3a3d8..c76dddd1d9 100644 --- a/Microsoft.Azure.Cosmos/src/CosmosClient.cs +++ b/Microsoft.Azure.Cosmos/src/CosmosClient.cs @@ -9,6 +9,7 @@ namespace Microsoft.Azure.Cosmos using System.Text; using System.Threading; using System.Threading.Tasks; + using global::Azure.Core; using Microsoft.Azure.Cosmos.Handlers; using Microsoft.Azure.Documents; @@ -204,6 +205,47 @@ public CosmosClient( this.Endpoint = new Uri(accountEndpoint); this.AccountKey = authKeyOrResourceToken; + this.AuthorizationTokenProvider = AuthorizationTokenProvider.CreateWithResourceTokenOrAuthKey(authKeyOrResourceToken); + + this.ClientContext = ClientContextCore.Create( + this, + clientOptions); + } + + /// + /// Creates a new CosmosClient with the account endpoint URI string and TokenCredential. + /// + /// CosmosClient is thread-safe. Its recommended to maintain a single instance of CosmosClient per lifetime + /// of the application which enables efficient connection management and performance. Please refer to the + /// performance guide. + /// + /// The cosmos service endpoint to use. + /// The token to provide AAD token for authorization. + /// (Optional) client options +#if PREVIEW + public +#else + internal +#endif + CosmosClient( + string accountEndpoint, + TokenCredential tokenCredential, + CosmosClientOptions clientOptions = null) + { + if (accountEndpoint == null) + { + throw new ArgumentNullException(nameof(accountEndpoint)); + } + + if (tokenCredential == null) + { + throw new ArgumentNullException(nameof(tokenCredential)); + } + + this.Endpoint = new Uri(accountEndpoint); + this.AuthorizationTokenProvider = new AuthorizationTokenProviderTokenCredential(tokenCredential, + accountEndpoint, + clientOptions?.TokenCredentialBackgroundRefreshInterval); this.ClientContext = ClientContextCore.Create( this, @@ -213,7 +255,7 @@ public CosmosClient( /// /// Used for unit testing only. /// - /// This constructor should be removed at some point. The mocking should happen in a derivied class. + /// This constructor should be removed at some point. The mocking should happen in a derived class. internal CosmosClient( string accountEndpoint, string authKeyOrResourceToken, @@ -242,6 +284,7 @@ internal CosmosClient( this.Endpoint = new Uri(accountEndpoint); this.AccountKey = authKeyOrResourceToken; + this.AuthorizationTokenProvider = AuthorizationTokenProvider.CreateWithResourceTokenOrAuthKey(authKeyOrResourceToken); this.ClientContext = ClientContextCore.Create( this, @@ -286,6 +329,11 @@ internal CosmosClient( /// internal string AccountKey { get; } + /// + /// Gets the AuthorizationTokenProvider used to generate the authorization token + /// + internal AuthorizationTokenProvider AuthorizationTokenProvider { get; } + internal DocumentClient DocumentClient => this.ClientContext.DocumentClient; internal RequestInvokerHandler RequestHandler => this.ClientContext.RequestHandler; internal CosmosClientContext ClientContext { get; } diff --git a/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs b/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs index 649532cae8..47a8cc5b55 100644 --- a/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs +++ b/Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs @@ -77,6 +77,7 @@ public CosmosClientOptions() { this.GatewayModeMaxConnectionLimit = ConnectionPolicy.Default.MaxConnectionLimit; this.RequestTimeout = ConnectionPolicy.Default.RequestTimeout; + this.TokenCredentialBackgroundRefreshInterval = null; this.ConnectionMode = CosmosClientOptions.DefaultConnectionMode; this.ConnectionProtocol = CosmosClientOptions.DefaultProtocol; this.ApiType = CosmosClientOptions.DefaultApiType; @@ -154,6 +155,20 @@ public int GatewayModeMaxConnectionLimit /// public TimeSpan RequestTimeout { get; set; } + /// + /// The SDK does a background refresh based on the time interval set to refresh the token credentials. + /// This avoids latency issues because the old token is used until the new token is retrieved. + /// + /// + /// The recommended minimum value is 5 minutes. The default value is 25% of the token expire time. + /// +#if PREVIEW + public +#else + internal +#endif + TimeSpan? TokenCredentialBackgroundRefreshInterval { get; set; } + /// /// Gets the handlers run before the process /// diff --git a/Microsoft.Azure.Cosmos/src/DocumentClient.cs b/Microsoft.Azure.Cosmos/src/DocumentClient.cs index e9f7417996..8a915e3fc8 100644 --- a/Microsoft.Azure.Cosmos/src/DocumentClient.cs +++ b/Microsoft.Azure.Cosmos/src/DocumentClient.cs @@ -18,6 +18,7 @@ namespace Microsoft.Azure.Cosmos using System.Text; using System.Threading; using System.Threading.Tasks; + using global::Azure.Core; using Microsoft.Azure.Cosmos.Common; using Microsoft.Azure.Cosmos.Core.Trace; using Microsoft.Azure.Cosmos.Query; @@ -93,11 +94,6 @@ internal partial class DocumentClient : IDisposable, IAuthorizationTokenProvider private const string RntbdReceiveHangDetectionTimeConfig = "CosmosDbTcpReceiveHangDetectionTimeSeconds"; private const string RntbdSendHangDetectionTimeConfig = "CosmosDbTcpSendHangDetectionTimeSeconds"; private const string EnableCpuMonitorConfig = "CosmosDbEnableCpuMonitor"; - private const string EnableAuthFailureTracesConfig = "enableAuthFailureTraces"; - - ////The MAC signature found in the HTTP request is not the same as the computed signature.Server used following string to sign - ////The input authorization token can't serve the request. Please check that the expected payload is built as per the protocol, and check the key being used. Server used the following payload to sign - private const string MacSignatureString = "to sign"; private const int MaxConcurrentConnectionOpenRequestsPerProcessor = 25; private const int DefaultMaxRequestsPerRntbdChannel = 30; @@ -109,10 +105,11 @@ internal partial class DocumentClient : IDisposable, IAuthorizationTokenProvider private const int DefaultRntbdReceiveHangDetectionTimeSeconds = 65; private const int DefaultRntbdSendHangDetectionTimeSeconds = 10; private const bool DefaultEnableCpuMonitor = true; - private const bool EnableAuthFailureTraces = false; - + + //Auth + private readonly AuthorizationTokenProvider cosmosAuthorization; + // Gateway has backoff/retry logic to hide transient errors. - private readonly IDictionary> resourceTokens; private RetryPolicy retryPolicy; private bool allowOverrideStrongerConsistency = false; private int maxConcurrentConnectionOpenRequests = Environment.ProcessorCount * MaxConcurrentConnectionOpenRequestsPerProcessor; @@ -129,10 +126,6 @@ internal partial class DocumentClient : IDisposable, IAuthorizationTokenProvider private int rntbdReceiveHangDetectionTimeSeconds = DefaultRntbdReceiveHangDetectionTimeSeconds; private int rntbdSendHangDetectionTimeSeconds = DefaultRntbdSendHangDetectionTimeSeconds; private bool enableCpuMonitor = DefaultEnableCpuMonitor; - private bool enableAuthFailureTraces = EnableAuthFailureTraces; - - //Auth - private IComputeHash authKeyHashFunction; //Consistency private Documents.ConsistencyLevel? desiredConsistencyLevel; @@ -165,8 +158,6 @@ internal partial class DocumentClient : IDisposable, IAuthorizationTokenProvider //SessionContainer. internal ISessionContainer sessionContainer; - private readonly bool hasAuthKeyResourceToken; - private readonly string authKeyResourceToken = string.Empty; private AsyncLazy queryPartitionProvider; private DocumentClientEventSource eventSource; @@ -217,7 +208,7 @@ public DocumentClient(Uri serviceEndpoint, if (authKey != null) { - this.authKeyHashFunction = new SecureStringHMACSHA256Helper(authKey); + this.cosmosAuthorization = new AuthorizationTokenProviderMasterKey(authKey); } this.Initialize(serviceEndpoint, connectionPolicy, desiredConsistencyLevel); @@ -368,13 +359,42 @@ public DocumentClient(Uri serviceEndpoint, { } + internal DocumentClient(Uri serviceEndpoint, + string authKeyOrResourceToken, + EventHandler sendingRequestEventArgs, + ConnectionPolicy connectionPolicy = null, + Documents.ConsistencyLevel? desiredConsistencyLevel = null, + JsonSerializerSettings serializerSettings = null, + ApiType apitype = ApiType.None, + EventHandler receivedResponseEventArgs = null, + HttpMessageHandler handler = null, + ISessionContainer sessionContainer = null, + bool? enableCpuMonitor = null, + Func transportClientHandlerFactory = null, + IStoreClientFactory storeClientFactory = null) + : this(serviceEndpoint, + AuthorizationTokenProvider.CreateWithResourceTokenOrAuthKey(authKeyOrResourceToken), + sendingRequestEventArgs, + connectionPolicy, + desiredConsistencyLevel, + serializerSettings, + apitype, + receivedResponseEventArgs, + handler, + sessionContainer, + enableCpuMonitor, + transportClientHandlerFactory, + storeClientFactory) + { + } + /// /// Initializes a new instance of the class using the /// specified service endpoint, an authorization key (or resource token) and a connection policy /// for the Azure Cosmos DB service. /// /// The service endpoint to use to create the client. - /// The authorization key or resource token to use to create the client. + /// The cosmos authorization for the client. /// The event handler to be invoked before the request is sent. /// The event handler to be invoked after a response has been received. /// (Optional) The connection policy for the client. @@ -389,7 +409,7 @@ public DocumentClient(Uri serviceEndpoint, /// /// The service endpoint can be obtained from the Azure Management Portal. /// If you are connecting using one of the Master Keys, these can be obtained along with the endpoint from the Azure Management Portal - /// If however you are connecting as a specific Azure Cosmos DB User, the value passed to is the ResourceToken obtained from the permission feed for the user. + /// If however you are connecting as a specific Azure Cosmos DB User, the value passed to is the ResourceToken obtained from the permission feed for the user. /// /// Using Direct connectivity, wherever possible, is recommended. /// @@ -398,7 +418,7 @@ public DocumentClient(Uri serviceEndpoint, /// /// internal DocumentClient(Uri serviceEndpoint, - string authKeyOrResourceToken, + AuthorizationTokenProvider cosmosAuthorization, EventHandler sendingRequestEventArgs, ConnectionPolicy connectionPolicy = null, Documents.ConsistencyLevel? desiredConsistencyLevel = null, @@ -411,11 +431,6 @@ internal DocumentClient(Uri serviceEndpoint, Func transportClientHandlerFactory = null, IStoreClientFactory storeClientFactory = null) { - if (authKeyOrResourceToken == null) - { - throw new ArgumentNullException("authKeyOrResourceToken"); - } - if (sendingRequestEventArgs != null) { this.sendingRequest += sendingRequestEventArgs; @@ -433,16 +448,7 @@ internal DocumentClient(Uri serviceEndpoint, this.receivedResponse += receivedResponseEventArgs; } - if (AuthorizationHelper.IsResourceToken(authKeyOrResourceToken)) - { - this.hasAuthKeyResourceToken = true; - this.authKeyResourceToken = authKeyOrResourceToken; - } - else - { - this.authKeyHashFunction = new StringHMACSHA256Hash(authKeyOrResourceToken); - } - + this.cosmosAuthorization = cosmosAuthorization ?? throw new ArgumentNullException(nameof(cosmosAuthorization)); this.transportClientHandlerFactory = transportClientHandlerFactory; this.Initialize( @@ -520,176 +526,6 @@ public DocumentClient(Uri serviceEndpoint, this.serializerSettings = serializerSettings; } - /// - /// Initializes a new instance of the class using the - /// specified Azure Cosmos DB service endpoint for the Azure Cosmos DB service, a list of permission objects and a connection policy. - /// - /// The service endpoint to use to create the client. - /// A list of Permission objects to use to create the client. - /// (Optional) The to use for this connection. - /// (Optional) The default consistency policy for client operations. - /// If is not supplied. - /// If is not a valid permission link. - /// - /// If no is provided, then the default will be used. - /// Using Direct connectivity, wherever possible, is recommended. - /// - /// - /// - /// - /// - public DocumentClient( - Uri serviceEndpoint, - IList permissionFeed, - ConnectionPolicy connectionPolicy = null, - Documents.ConsistencyLevel? desiredConsistencyLevel = null) - : this(serviceEndpoint, - GetResourceTokens(permissionFeed), - connectionPolicy, - desiredConsistencyLevel) - { - } - - private static List GetResourceTokens(IList permissionFeed) - { - if (permissionFeed == null) - { - throw new ArgumentNullException("permissionFeed"); - } - - return permissionFeed.Select( - permission => new ResourceToken - { - ResourceLink = permission.ResourceLink, - ResourcePartitionKey = permission.ResourcePartitionKey != null ? permission.ResourcePartitionKey.InternalKey.ToObjectArray() : null, - Token = permission.Token - }).ToList(); - } - - /// - /// Initializes a new instance of the class using the - /// specified Azure Cosmos DB service endpoint, a list of objects and a connection policy. - /// - /// The service endpoint to use to create the client. - /// A list of objects to use to create the client. - /// (Optional) The to use for this connection. - /// (Optional) The default consistency policy for client operations. - /// If is not supplied. - /// If is not a valid permission link. - /// - /// If no is provided, then the default will be used. - /// Using Direct connectivity, wherever possible, is recommended. - /// - /// - /// - /// - /// - internal DocumentClient(Uri serviceEndpoint, - IList resourceTokens, - ConnectionPolicy connectionPolicy = null, - Documents.ConsistencyLevel? desiredConsistencyLevel = null) - { - if (resourceTokens == null) - { - throw new ArgumentNullException("resourceTokens"); - } - - this.resourceTokens = new Dictionary>(); - - foreach (ResourceToken resourceToken in resourceTokens) - { - bool isNameBasedRequest = false; - bool isFeedRequest = false; - string resourceTypeString; - string resourceIdOrFullName; - if (!PathsHelper.TryParsePathSegments(resourceToken.ResourceLink, out isFeedRequest, out resourceTypeString, out resourceIdOrFullName, out isNameBasedRequest)) - { - throw new ArgumentException(RMResources.BadUrl, "resourceToken.ResourceLink"); - } - - List tokenList; - if (!this.resourceTokens.TryGetValue(resourceIdOrFullName, out tokenList)) - { - tokenList = new List(); - this.resourceTokens.Add(resourceIdOrFullName, tokenList); - } - - tokenList.Add(new PartitionKeyAndResourceTokenPair( - resourceToken.ResourcePartitionKey != null ? PartitionKeyInternal.FromObjectArray(resourceToken.ResourcePartitionKey, true) : PartitionKeyInternal.Empty, - resourceToken.Token)); - } - - if (!this.resourceTokens.Any()) - { - throw new ArgumentException("permissionFeed"); - } - - string firstToken = resourceTokens.First().Token; - - if (AuthorizationHelper.IsResourceToken(firstToken)) - { - this.hasAuthKeyResourceToken = true; - this.authKeyResourceToken = firstToken; - this.Initialize(serviceEndpoint, connectionPolicy, desiredConsistencyLevel); - } - else - { - this.authKeyHashFunction = new StringHMACSHA256Hash(firstToken); - this.Initialize(serviceEndpoint, connectionPolicy, desiredConsistencyLevel); - } - } - - /// - /// Initializes a new instance of the Microsoft.Azure.Cosmos.DocumentClient class using the - /// specified Azure Cosmos DB service endpoint, a dictionary of resource tokens and a connection policy. - /// - /// The service endpoint to use to create the client. - /// A dictionary of resource ids and resource tokens. - /// (Optional) The connection policy for the client. - /// (Optional) The default consistency policy for client operations. - /// Using Direct connectivity, wherever possible, is recommended - /// - /// - /// - [Obsolete("Please use the constructor that takes a permission list or a resource token list.")] - public DocumentClient(Uri serviceEndpoint, - IDictionary resourceTokens, - ConnectionPolicy connectionPolicy = null, - Documents.ConsistencyLevel? desiredConsistencyLevel = null) - { - if (resourceTokens == null) - { - throw new ArgumentNullException("resourceTokens"); - } - - if (resourceTokens.Count() == 0) - { - throw new DocumentClientException(RMResources.InsufficientResourceTokens, null, null); - } - - this.resourceTokens = resourceTokens.ToDictionary( - pair => pair.Key, - pair => new List { new PartitionKeyAndResourceTokenPair(PartitionKeyInternal.Empty, pair.Value) }); - - string firstToken = resourceTokens.ElementAt(0).Value; - if (string.IsNullOrEmpty(firstToken)) - { - throw new DocumentClientException(RMResources.InsufficientResourceTokens, null, null); - } - - if (AuthorizationHelper.IsResourceToken(firstToken)) - { - this.hasAuthKeyResourceToken = true; - this.authKeyResourceToken = firstToken; - this.Initialize(serviceEndpoint, connectionPolicy, desiredConsistencyLevel); - } - else - { - this.authKeyHashFunction = new StringHMACSHA256Hash(firstToken); - this.Initialize(serviceEndpoint, connectionPolicy, desiredConsistencyLevel); - } - } - /// /// Internal constructor purely for unit-testing /// @@ -790,7 +626,8 @@ internal virtual void Initialize(Uri serviceEndpoint, HttpMessageHandler handler = null, ISessionContainer sessionContainer = null, bool? enableCpuMonitor = null, - IStoreClientFactory storeClientFactory = null) + IStoreClientFactory storeClientFactory = null, + TokenCredential tokenCredential = null) { if (serviceEndpoint == null) { @@ -972,16 +809,6 @@ internal virtual void Initialize(Uri serviceEndpoint, } } } - - string enableAuthFailureTracesString = System.Configuration.ConfigurationManager.AppSettings[EnableAuthFailureTracesConfig]; - if (!string.IsNullOrEmpty(enableAuthFailureTracesString)) - { - bool enableAuthFailureTracesFlag = false; - if (bool.TryParse(enableAuthFailureTracesString, out enableAuthFailureTracesFlag)) - { - this.enableAuthFailureTraces = enableAuthFailureTracesFlag; - } - } #if NETSTANDARD20 } #endif @@ -1282,24 +1109,6 @@ public Uri ReadEndpoint /// public ConnectionPolicy ConnectionPolicy { get; private set; } - /// - /// Gets a dictionary of resource tokens used by the client from the Azure Cosmos DB service. - /// - /// - /// A dictionary of resource tokens used by the client. - /// - /// - [Obsolete] - public IDictionary ResourceTokens - { - get - { - // NOTE: if DocumentClient was created using construction taking permission feed and there - // are duplicate resource links, we will choose arbitrary token for it here. - return (this.resourceTokens != null) ? this.resourceTokens.ToDictionary(pair => pair.Key, pair => pair.Value.First().ResourceToken) : null; - } - } - /// /// Gets the AuthKey used by the client from the Azure Cosmos DB service. /// @@ -1307,20 +1116,7 @@ public IDictionary ResourceTokens /// The AuthKey used by the client. /// /// - public SecureString AuthKey - { - get - { - if (this.authKeyHashFunction != null) - { - return this.authKeyHashFunction.Key; - } - else - { - return null; - } - } - } + public SecureString AuthKey => throw new NotSupportedException("Please use CosmosAuthorization"); /// /// Gets the configured consistency level of the client from the Azure Cosmos DB service. @@ -1393,10 +1189,9 @@ public void Dispose() this.httpClient = null; } - if (this.authKeyHashFunction != null) + if (this.cosmosAuthorization != null) { - this.authKeyHashFunction.Dispose(); - this.authKeyHashFunction = null; + this.cosmosAuthorization.Dispose(); } if (this.GlobalEndpointManager != null) @@ -1485,7 +1280,7 @@ internal async Task ProcessRequestAsync( throw new ArgumentNullException(nameof(verb)); } - (string authorization, string payload) = await ((IAuthorizationTokenProvider)this).GetUserAuthorizationAsync( + (string authorization, string payload) = await this.cosmosAuthorization.GetUserAuthorizationAsync( request.ResourceAddress, PathsHelper.GetResourcePath(request.ResourceType), verb, @@ -1506,34 +1301,10 @@ internal async Task ProcessRequestAsync( } catch (DocumentClientException dce) { - if (payload != null - && dce.Message != null - && dce.StatusCode.HasValue - && dce.StatusCode.Value == HttpStatusCode.Unauthorized - && dce.Message.Contains(DocumentClient.MacSignatureString)) - { - // The following code is added such that we get trace data on unexpected 401/HMAC errors and it is - // disabled by default. The trace will be trigger only when "enableAuthFailureTraces" named configuration - // is set to true (currently true for CTL runs). - // For production we will work directly with specific customers in order to enable this configuration. - string normalizedPayload = DocumentClient.NormalizeAuthorizationPayload(payload); - if (this.enableAuthFailureTraces) - { - string tokenFirst5 = HttpUtility.UrlDecode(authorization).Split('&')[2].Split('=')[1].Substring(0, 5); - ulong authHash = 0; - if (this.authKeyHashFunction?.Key != null) - { - byte[] bytes = Encoding.UTF8.GetBytes(this.authKeyHashFunction?.Key?.ToString()); - authHash = Documents.Routing.MurmurHash3.Hash64(bytes, bytes.Length); - } - DefaultTrace.TraceError("Un-expected authorization payload mis-match. Actual payload={0}, token={1}..., hash={2:X}..., error={3}", - normalizedPayload, tokenFirst5, authHash, dce.Message); - } - else - { - DefaultTrace.TraceError("Un-expected authorization payload mis-match. Actual {0} service expected {1}", normalizedPayload, dce.Message); - } - } + this.cosmosAuthorization.TraceUnauthorized( + dce, + authorization, + payload); throw; } @@ -6303,24 +6074,6 @@ private async Task> UpsertUserDefinedTypePriva #region IAuthorizationTokenProvider - private bool TryGetResourceToken(string resourceAddress, PartitionKeyInternal partitionKey, out string resourceToken) - { - resourceToken = null; - List partitionKeyTokenPairs; - bool isPartitionKeyAndTokenPairListAvailable = this.resourceTokens.TryGetValue(resourceAddress, out partitionKeyTokenPairs); - if (isPartitionKeyAndTokenPairListAvailable) - { - PartitionKeyAndResourceTokenPair partitionKeyTokenPair = partitionKeyTokenPairs.FirstOrDefault(pair => pair.PartitionKey.Contains(partitionKey)); - if (partitionKeyTokenPair != null) - { - resourceToken = partitionKeyTokenPair.ResourceToken; - return true; - } - } - - return false; - } - ValueTask<(string token, string payload)> IAuthorizationTokenProvider.GetUserAuthorizationAsync( string resourceAddress, string resourceType, @@ -6328,209 +6081,42 @@ private bool TryGetResourceToken(string resourceAddress, PartitionKeyInternal pa INameValueCollection headers, AuthorizationTokenType tokenType) { - string authorizationToken = this.GetUserAuthorizationTokenCore( + return this.cosmosAuthorization.GetUserAuthorizationAsync( resourceAddress, resourceType, requestVerb, headers, - tokenType, - out AuthorizationHelper.ArrayOwner arrayOwner); - using (arrayOwner) - { - if (arrayOwner.Buffer.Count == 0) - { - return new ValueTask<(string token, string payload)>((authorizationToken, null)); - } - - string payload = Encoding.UTF8.GetString(arrayOwner.Buffer.Array, arrayOwner.Buffer.Offset, (int)arrayOwner.Buffer.Count); - return new ValueTask<(string token, string payload)>((authorizationToken, payload)); - } + tokenType); } - string ICosmosAuthorizationTokenProvider.GetUserAuthorizationToken( + ValueTask ICosmosAuthorizationTokenProvider.GetUserAuthorizationTokenAsync( string resourceAddress, string resourceType, string requestVerb, INameValueCollection headers, - AuthorizationTokenType tokenType) + AuthorizationTokenType tokenType, + CosmosDiagnosticsContext diagnosticsContext) { - string authorizationToken = this.GetUserAuthorizationTokenCore( + return this.cosmosAuthorization.GetUserAuthorizationTokenAsync( resourceAddress, resourceType, requestVerb, headers, tokenType, - out AuthorizationHelper.ArrayOwner arrayOwner); - using (arrayOwner) - { - return authorizationToken; - } + diagnosticsContext); } - private string GetUserAuthorizationTokenCore( - string resourceAddress, - string resourceType, - string requestVerb, - INameValueCollection headers, - AuthorizationTokenType tokenType, - out AuthorizationHelper.ArrayOwner payload) // unused, use token based upon what is passed in constructor - { - if (this.hasAuthKeyResourceToken && this.resourceTokens == null) - { - // If the input auth token is a resource token, then use it as a bearer-token. - payload = default; - return HttpUtility.UrlEncode(this.authKeyResourceToken); - } - - if (this.authKeyHashFunction != null) - { - // this is masterkey authZ - headers[HttpConstants.HttpHeaders.XDate] = DateTime.UtcNow.ToString("r", CultureInfo.InvariantCulture); - - return AuthorizationHelper.GenerateKeyAuthorizationSignature( - requestVerb, resourceAddress, resourceType, headers, this.authKeyHashFunction, out payload); - } - else - { - PartitionKeyInternal partitionKey = PartitionKeyInternal.Empty; - string partitionKeyString = headers[HttpConstants.HttpHeaders.PartitionKey]; - if (partitionKeyString != null) - { - partitionKey = PartitionKeyInternal.FromJsonString(partitionKeyString); - } - - if (PathsHelper.IsNameBased(resourceAddress)) - { - string resourceToken = null; - bool isTokenAvailable = false; - - for (int index = 2; index < ResourceId.MaxPathFragment; index = index + 2) - { - string resourceParent = PathsHelper.GetParentByIndex(resourceAddress, index); - if (resourceParent == null) - break; - - isTokenAvailable = this.TryGetResourceToken(resourceParent, partitionKey, out resourceToken); - if (isTokenAvailable) - break; - } - - // Get or Head for collection can be done with any child token - if (!isTokenAvailable && PathsHelper.GetCollectionPath(resourceAddress) == resourceAddress - && (requestVerb == HttpConstants.HttpMethods.Get - || requestVerb == HttpConstants.HttpMethods.Head)) - { - string resourceAddressWithSlash = resourceAddress.EndsWith("/", StringComparison.Ordinal) - ? resourceAddress - : resourceAddress + "/"; - foreach (KeyValuePair> pair in this.resourceTokens) - { - if (pair.Key.StartsWith(resourceAddressWithSlash, StringComparison.Ordinal)) - { - resourceToken = pair.Value[0].ResourceToken; - isTokenAvailable = true; - break; - } - } - } - - if (!isTokenAvailable) - { - throw new UnauthorizedException(string.Format( - CultureInfo.InvariantCulture, ClientResources.AuthTokenNotFound, resourceAddress)); - } - - payload = default; - return HttpUtility.UrlEncode(resourceToken); - } - else - { - string resourceToken = null; - - // In case there is no directly matching token, look for parent's token. - ResourceId resourceId = ResourceId.Parse(resourceAddress); - - bool isTokenAvailable = false; - if (resourceId.Attachment != 0 || resourceId.Permission != 0 || resourceId.StoredProcedure != 0 - || resourceId.Trigger != 0 || resourceId.UserDefinedFunction != 0) - { - // Use the leaf ID - attachment/permission/sproc/trigger/udf - isTokenAvailable = this.TryGetResourceToken(resourceAddress, partitionKey, out resourceToken); - } - - if (!isTokenAvailable && - (resourceId.Attachment != 0 || resourceId.Document != 0)) - { - // Use DocumentID for attachment/document - isTokenAvailable = this.TryGetResourceToken(resourceId.DocumentId.ToString(), partitionKey, out resourceToken); - } - - if (!isTokenAvailable && - (resourceId.Attachment != 0 || resourceId.Document != 0 || resourceId.StoredProcedure != 0 || resourceId.Trigger != 0 - || resourceId.UserDefinedFunction != 0 || resourceId.DocumentCollection != 0)) - { - // Use CollectionID for attachment/document/sproc/trigger/udf/collection - isTokenAvailable = this.TryGetResourceToken(resourceId.DocumentCollectionId.ToString(), partitionKey, out resourceToken); - } - - if (!isTokenAvailable && - (resourceId.Permission != 0 || resourceId.User != 0)) - { - // Use UserID for permission/user - isTokenAvailable = this.TryGetResourceToken(resourceId.UserId.ToString(), partitionKey, out resourceToken); - } - - if (!isTokenAvailable) - { - // Use DatabaseId if all else fail - isTokenAvailable = this.TryGetResourceToken(resourceId.DatabaseId.ToString(), partitionKey, out resourceToken); - } - - // Get or Head for collection can be done with any child token - if (!isTokenAvailable && resourceId.DocumentCollection != 0 - && (requestVerb == HttpConstants.HttpMethods.Get - || requestVerb == HttpConstants.HttpMethods.Head)) - { - foreach (KeyValuePair> pair in this.resourceTokens) - { - ResourceId tokenRid; - if (!PathsHelper.IsNameBased(pair.Key) && - ResourceId.TryParse(pair.Key, out tokenRid) && - tokenRid.DocumentCollectionId.Equals(resourceId)) - { - resourceToken = pair.Value[0].ResourceToken; - isTokenAvailable = true; - break; - } - } - } - - if (!isTokenAvailable) - { - throw new UnauthorizedException(string.Format( - CultureInfo.InvariantCulture, ClientResources.AuthTokenNotFound, resourceAddress)); - } - - payload = default; - return HttpUtility.UrlEncode(resourceToken); - } - } - } - - async Task IAuthorizationTokenProvider.AddSystemAuthorizationHeaderAsync( + Task IAuthorizationTokenProvider.AddSystemAuthorizationHeaderAsync( DocumentServiceRequest request, string federationId, string verb, string resourceId) { - request.Headers[HttpConstants.HttpHeaders.XDate] = DateTime.UtcNow.ToString("r", CultureInfo.InvariantCulture); - - request.Headers[HttpConstants.HttpHeaders.Authorization] = (await ((IAuthorizationTokenProvider)this).GetUserAuthorizationAsync( - resourceId ?? request.ResourceAddress, - PathsHelper.GetResourcePath(request.ResourceType), + return this.cosmosAuthorization.AddSystemAuthorizationHeaderAsync( + request, + federationId, verb, - request.Headers, - request.RequestAuthorizationTokenType)).token; + resourceId); } #endregion @@ -6670,38 +6256,29 @@ Task IDocumentClientInternal.GetDatabaseAccountInternalAsync( private async Task GetDatabaseAccountPrivateAsync(Uri serviceEndpoint, CancellationToken cancellationToken = default) { await this.EnsureValidClientAsync(); - GatewayStoreModel gatewayModel = this.GatewayStoreModel as GatewayStoreModel; - if (gatewayModel != null) + if (this.GatewayStoreModel is GatewayStoreModel gatewayModel) { - ValueTask CreateRequestMessage() + async ValueTask CreateRequestMessage() { - HttpRequestMessage request = new HttpRequestMessage(); - INameValueCollection headersCollection = new DictionaryNameValueCollection(); - string xDate = DateTime.UtcNow.ToString("r"); - headersCollection.Add(HttpConstants.HttpHeaders.XDate, xDate); - request.Headers.Add(HttpConstants.HttpHeaders.XDate, xDate); - - // Retrieve the CosmosAccountSettings from the gateway. - string authorizationToken; - - if (this.hasAuthKeyResourceToken) + HttpRequestMessage request = new HttpRequestMessage { - authorizationToken = HttpUtility.UrlEncode(this.authKeyResourceToken); - } - else + Method = HttpMethod.Get, + RequestUri = serviceEndpoint + }; + + INameValueCollection headersCollection = new StoreResponseNameValueCollection(); + await this.cosmosAuthorization.AddAuthorizationHeaderAsync( + headersCollection, + serviceEndpoint, + "GET", + AuthorizationTokenType.PrimaryMasterKey); + + foreach (string key in headersCollection.AllKeys()) { - authorizationToken = AuthorizationHelper.GenerateKeyAuthorizationSignature( - HttpConstants.HttpMethods.Get, - serviceEndpoint, - headersCollection, - this.authKeyHashFunction); + request.Headers.Add(key, headersCollection[key]); } - request.Headers.Add(HttpConstants.HttpHeaders.Authorization, authorizationToken); - - request.Method = HttpMethod.Get; - request.RequestUri = serviceEndpoint; - return new ValueTask(request); + return request; } AccountProperties databaseAccount = await gatewayModel.GetDatabaseAccountAsync(CreateRequestMessage); @@ -6923,9 +6500,7 @@ private async Task InitializeGatewayConfigurationReaderAsync() { GatewayAccountReader accountReader = new GatewayAccountReader( serviceEndpoint: this.ServiceEndpoint, - stringHMACSHA256Helper: this.authKeyHashFunction, - hasResourceToken: this.hasAuthKeyResourceToken, - resourceToken: this.authKeyResourceToken, + cosmosAuthorization: this.cosmosAuthorization, connectionPolicy: this.ConnectionPolicy, httpClient: this.httpClient); diff --git a/Microsoft.Azure.Cosmos/src/GatewayAccountReader.cs b/Microsoft.Azure.Cosmos/src/GatewayAccountReader.cs index 08bcd23d7f..05e74fe416 100644 --- a/Microsoft.Azure.Cosmos/src/GatewayAccountReader.cs +++ b/Microsoft.Azure.Cosmos/src/GatewayAccountReader.cs @@ -15,49 +15,31 @@ namespace Microsoft.Azure.Cosmos internal sealed class GatewayAccountReader { private readonly ConnectionPolicy connectionPolicy; - private readonly IComputeHash authKeyHashFunction; - private readonly bool hasAuthKeyResourceToken = false; - private readonly string authKeyResourceToken = string.Empty; + private readonly AuthorizationTokenProvider cosmosAuthorization; private readonly CosmosHttpClient httpClient; private readonly Uri serviceEndpoint; + // Backlog: Auth abstractions are spilling through. 4 arguments for this CTOR are result of it. public GatewayAccountReader(Uri serviceEndpoint, - IComputeHash stringHMACSHA256Helper, - bool hasResourceToken, - string resourceToken, + AuthorizationTokenProvider cosmosAuthorization, ConnectionPolicy connectionPolicy, CosmosHttpClient httpClient) { this.httpClient = httpClient; this.serviceEndpoint = serviceEndpoint; - this.authKeyHashFunction = stringHMACSHA256Helper; - this.hasAuthKeyResourceToken = hasResourceToken; - this.authKeyResourceToken = resourceToken; + this.cosmosAuthorization = cosmosAuthorization ?? throw new ArgumentNullException(nameof(AuthorizationTokenProvider)); this.connectionPolicy = connectionPolicy; } private async Task GetDatabaseAccountAsync(Uri serviceEndpoint) { INameValueCollection headers = new DictionaryNameValueCollection(StringComparer.Ordinal); - string authorizationToken = string.Empty; - if (this.hasAuthKeyResourceToken) - { - authorizationToken = HttpUtility.UrlEncode(this.authKeyResourceToken); - } - else - { - // Retrieve the document service properties. - string xDate = DateTime.UtcNow.ToString("r", CultureInfo.InvariantCulture); - headers.Set(HttpConstants.HttpHeaders.XDate, xDate); - - authorizationToken = AuthorizationHelper.GenerateKeyAuthorizationSignature( - HttpConstants.HttpMethods.Get, - serviceEndpoint, - headers, - this.authKeyHashFunction); - } + await this.cosmosAuthorization.AddAuthorizationHeaderAsync( + headersCollection: headers, + serviceEndpoint, + HttpConstants.HttpMethods.Get, + AuthorizationTokenType.PrimaryMasterKey); - headers.Set(HttpConstants.HttpHeaders.Authorization, authorizationToken); using (HttpResponseMessage responseMessage = await this.httpClient.GetAsync( uri: serviceEndpoint, additionalHeaders: headers, diff --git a/Microsoft.Azure.Cosmos/src/Handler/TransportHandler.cs b/Microsoft.Azure.Cosmos/src/Handler/TransportHandler.cs index eaef0529d1..8d66349341 100644 --- a/Microsoft.Azure.Cosmos/src/Handler/TransportHandler.cs +++ b/Microsoft.Azure.Cosmos/src/Handler/TransportHandler.cs @@ -75,12 +75,13 @@ internal async Task ProcessMessageAsync( DocumentServiceRequest serviceRequest = request.ToDocumentServiceRequest(); //TODO: extrace auth into a separate handler - string authorization = ((ICosmosAuthorizationTokenProvider)this.client.DocumentClient).GetUserAuthorizationToken( + string authorization = await ((ICosmosAuthorizationTokenProvider)this.client.DocumentClient).GetUserAuthorizationTokenAsync( serviceRequest.ResourceAddress, PathsHelper.GetResourcePath(request.ResourceType), request.Method.ToString(), serviceRequest.Headers, - AuthorizationTokenType.PrimaryMasterKey); + AuthorizationTokenType.PrimaryMasterKey, + request.DiagnosticsContext); serviceRequest.Headers[HttpConstants.HttpHeaders.Authorization] = authorization; diff --git a/Microsoft.Azure.Cosmos/src/Microsoft.Azure.Cosmos.csproj b/Microsoft.Azure.Cosmos/src/Microsoft.Azure.Cosmos.csproj index b9cf819846..e92b354b17 100644 --- a/Microsoft.Azure.Cosmos/src/Microsoft.Azure.Cosmos.csproj +++ b/Microsoft.Azure.Cosmos/src/Microsoft.Azure.Cosmos.csproj @@ -93,6 +93,7 @@ + diff --git a/Microsoft.Azure.Cosmos/src/Resource/ClientContextCore.cs b/Microsoft.Azure.Cosmos/src/Resource/ClientContextCore.cs index 66ce771951..368ee80f48 100644 --- a/Microsoft.Azure.Cosmos/src/Resource/ClientContextCore.cs +++ b/Microsoft.Azure.Cosmos/src/Resource/ClientContextCore.cs @@ -65,7 +65,7 @@ internal static CosmosClientContext Create( DocumentClient documentClient = new DocumentClient( cosmosClient.Endpoint, - cosmosClient.AccountKey, + cosmosClient.AuthorizationTokenProvider, apitype: clientOptions.ApiType, sendingRequestEventArgs: clientOptions.SendingRequestEventArgs, transportClientHandlerFactory: clientOptions.TransportClientHandlerFactory, diff --git a/Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/CosmosExceptionFactory.cs b/Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/CosmosExceptionFactory.cs index 2fa83d4fcb..95381a010f 100644 --- a/Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/CosmosExceptionFactory.cs +++ b/Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/CosmosExceptionFactory.cs @@ -350,6 +350,32 @@ internal static CosmosException CreateBadRequestException( innerException); } + internal static CosmosException CreateUnauthorizedException( + string message, + int subStatusCode, + Exception innerException, + string stackTrace = default, + string activityId = default, + double requestCharge = default, + TimeSpan? retryAfter = default, + Headers headers = default, + CosmosDiagnosticsContext diagnosticsContext = default, + Error error = default) + { + return CosmosExceptionFactory.Create( + HttpStatusCode.Unauthorized, + subStatusCode, + message, + stackTrace, + activityId, + requestCharge, + retryAfter, + headers, + diagnosticsContext, + error, + innerException); + } + internal static CosmosException Create( HttpStatusCode statusCode, int subStatusCode, diff --git a/Microsoft.Azure.Cosmos/src/Routing/PartitionKeyRangeCache.cs b/Microsoft.Azure.Cosmos/src/Routing/PartitionKeyRangeCache.cs index a267cf5c85..fe89d74b45 100644 --- a/Microsoft.Azure.Cosmos/src/Routing/PartitionKeyRangeCache.cs +++ b/Microsoft.Azure.Cosmos/src/Routing/PartitionKeyRangeCache.cs @@ -242,7 +242,7 @@ private async Task ExecutePartitionKeyRangeReadChangeFe ////CosmosContainerSettings collection = await this.collectionCache.ResolveCollectionAsync(request, CancellationToken.None); ////authorizationToken = - //// this.authorizationTokenProvider.GetUserAuthorizationToken( + //// this.authorizationTokenProvider.GetUserAuthorizationTokenAsync( //// collection.AltLink, //// PathsHelper.GetResourcePath(request.ResourceType), //// HttpConstants.HttpMethods.Get, diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosAadTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosAadTests.cs new file mode 100644 index 0000000000..178e8fef6e --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosAadTests.cs @@ -0,0 +1,213 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +//------------------------------------------------------------ +namespace Microsoft.Azure.Cosmos.SDK.EmulatorTests +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Net; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using System.Web; + using Documents.Client; + using global::Azure; + using global::Azure.Core; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Microsoft.IdentityModel.Tokens; + + [TestClass] + public class CosmosAadTests + { + [TestMethod] + public async Task AadMockTest() + { + string databaseId = Guid.NewGuid().ToString(); + string containerId = Guid.NewGuid().ToString(); + using (CosmosClient cosmosClient = TestCommon.CreateCosmosClient()) + { + Database database = await cosmosClient.CreateDatabaseAsync(databaseId); + Container container = await database.CreateContainerAsync( + containerId, + "/id"); + } + + (string endpoint, string authKey) = TestCommon.GetAccountInfo(); + LocalEmulatorTokenCredential simpleEmulatorTokenCredential = new LocalEmulatorTokenCredential(authKey); + CosmosClientOptions clientOptions = new CosmosClientOptions() + { + ConnectionMode = ConnectionMode.Gateway, + ConnectionProtocol = Protocol.Https + }; + + using CosmosClient aadClient = new CosmosClient( + endpoint, + simpleEmulatorTokenCredential, + clientOptions); + + TokenCredentialCache tokenCredentialCache = ((AuthorizationTokenProviderTokenCredential)aadClient.AuthorizationTokenProvider).tokenCredentialCache; + + // The refresh interval changes slightly based on how fast machine calculate the interval based on the expire time. + Assert.IsTrue(15 <= tokenCredentialCache.BackgroundTokenCredentialRefreshInterval.Value.TotalMinutes, "Default background refresh should be 25% of the token life which is defaulted to 1hr"); + Assert.IsTrue(tokenCredentialCache.BackgroundTokenCredentialRefreshInterval.Value.TotalMinutes > 14.7 , "Default background refresh should be 25% of the token life which is defaulted to 1hr"); + + Database aadDatabase = await aadClient.GetDatabase(databaseId).ReadAsync(); + Container aadContainer = await aadDatabase.GetContainer(containerId).ReadContainerAsync(); + ToDoActivity toDoActivity = ToDoActivity.CreateRandomToDoActivity(); + ItemResponse itemResponse = await aadContainer.CreateItemAsync( + toDoActivity, + new PartitionKey(toDoActivity.id)); + + toDoActivity.cost = 42.42; + await aadContainer.ReplaceItemAsync( + toDoActivity, + toDoActivity.id, + new PartitionKey(toDoActivity.id)); + + await aadContainer.ReadItemAsync( + toDoActivity.id, + new PartitionKey(toDoActivity.id)); + + await aadContainer.UpsertItemAsync(toDoActivity); + + await aadContainer.DeleteItemAsync( + toDoActivity.id, + new PartitionKey(toDoActivity.id)); + } + + [TestMethod] + public async Task AadMockRefreshTest() + { + int getAadTokenCount = 0; + void GetAadTokenCallBack( + TokenRequestContext context, + CancellationToken token) + { + getAadTokenCount++; + } + + (string endpoint, string authKey) = TestCommon.GetAccountInfo(); + LocalEmulatorTokenCredential simpleEmulatorTokenCredential = new LocalEmulatorTokenCredential( + authKey, + GetAadTokenCallBack); + + CosmosClientOptions clientOptions = new CosmosClientOptions() + { + TokenCredentialBackgroundRefreshInterval = TimeSpan.FromSeconds(1) + }; + + Assert.AreEqual(0, getAadTokenCount); + using CosmosClient aadClient = new CosmosClient( + endpoint, + simpleEmulatorTokenCredential, + clientOptions); + + DocumentClient documentClient = aadClient.ClientContext.DocumentClient; + TokenCredentialCache tokenCredentialCache = ((AuthorizationTokenProviderTokenCredential)aadClient.AuthorizationTokenProvider).tokenCredentialCache; + + Assert.AreEqual(TimeSpan.FromSeconds(1), tokenCredentialCache.BackgroundTokenCredentialRefreshInterval); + Assert.AreEqual(1, getAadTokenCount); + + await aadClient.ReadAccountAsync(); + await aadClient.ReadAccountAsync(); + await aadClient.ReadAccountAsync(); + + // Should use cached token + Assert.AreEqual(1, getAadTokenCount); + + await Task.Delay(TimeSpan.FromSeconds(1)); + Assert.AreEqual(1, getAadTokenCount); + } + + [TestMethod] + public async Task AadMockRefreshRetryTest() + { + int getAadTokenCount = 0; + void GetAadTokenCallBack( + TokenRequestContext context, + CancellationToken token) + { + getAadTokenCount++; + if (getAadTokenCount <= 2) + { + throw new RequestFailedException( + 408, + "Test Failure"); + } + } + + (string endpoint, string authKey) = TestCommon.GetAccountInfo(); + LocalEmulatorTokenCredential simpleEmulatorTokenCredential = new LocalEmulatorTokenCredential( + authKey, + GetAadTokenCallBack); + + CosmosClientOptions clientOptions = new CosmosClientOptions() + { + TokenCredentialBackgroundRefreshInterval = TimeSpan.FromSeconds(60) + }; + + Assert.AreEqual(0, getAadTokenCount); + using (CosmosClient aadClient = new CosmosClient( + endpoint, + simpleEmulatorTokenCredential, + clientOptions)) + { + Assert.AreEqual(3, getAadTokenCount); + await Task.Delay(TimeSpan.FromSeconds(1)); + ResponseMessage responseMessage = await aadClient.GetDatabase(Guid.NewGuid().ToString()).ReadStreamAsync(); + Assert.IsNotNull(responseMessage); + + // Should use cached token + Assert.AreEqual(3, getAadTokenCount); + } + } + + [TestMethod] + public async Task AadMockNegativeRefreshRetryTest() + { + int getAadTokenCount = 0; + string errorMessage = "Test Failure" + Guid.NewGuid(); + void GetAadTokenCallBack( + TokenRequestContext context, + CancellationToken token) + { + getAadTokenCount++; + throw new RequestFailedException( + 408, + errorMessage); + } + + (string endpoint, string authKey) = TestCommon.GetAccountInfo(); + LocalEmulatorTokenCredential simpleEmulatorTokenCredential = new LocalEmulatorTokenCredential( + authKey, + GetAadTokenCallBack); + + CosmosClientOptions clientOptions = new CosmosClientOptions() + { + TokenCredentialBackgroundRefreshInterval = TimeSpan.FromSeconds(60) + }; + + Assert.AreEqual(0, getAadTokenCount); + using (CosmosClient aadClient = new CosmosClient( + endpoint, + simpleEmulatorTokenCredential, + clientOptions)) + { + Assert.AreEqual(3, getAadTokenCount); + await Task.Delay(TimeSpan.FromSeconds(1)); + try + { + ResponseMessage responseMessage = + await aadClient.GetDatabase(Guid.NewGuid().ToString()).ReadStreamAsync(); + Assert.Fail("Should throw auth error."); + } + catch (CosmosException ce) when (ce.StatusCode == HttpStatusCode.Unauthorized) + { + Assert.IsNotNull(ce.Message); + Assert.IsTrue(ce.ToString().Contains(errorMessage)); + } + } + } + } +} \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Microsoft.Azure.Cosmos.EmulatorTests.csproj b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Microsoft.Azure.Cosmos.EmulatorTests.csproj index 0e51dc718a..e6b37952b1 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Microsoft.Azure.Cosmos.EmulatorTests.csproj +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Microsoft.Azure.Cosmos.EmulatorTests.csproj @@ -26,6 +26,7 @@ + @@ -37,6 +38,7 @@ + diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Utils/LocalEmulatorTokenCredential.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Utils/LocalEmulatorTokenCredential.cs new file mode 100644 index 0000000000..75efbe51e5 --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Utils/LocalEmulatorTokenCredential.cs @@ -0,0 +1,83 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +//------------------------------------------------------------ +namespace Microsoft.Azure.Cosmos.SDK.EmulatorTests +{ + using System; + using System.Collections.Generic; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using global::Azure.Core; + using IdentityModel.Tokens; + + public class LocalEmulatorTokenCredential : TokenCredential + { + private readonly DateTime? DefaultDateTime = null; + private readonly Action GetTokenCallback; + private readonly string masterKey; + + internal LocalEmulatorTokenCredential( + string masterKey = null, + Action getTokenCallback = null, + DateTime? defaultDateTime = null) + { + this.masterKey = masterKey; + this.GetTokenCallback = getTokenCallback; + this.DefaultDateTime = defaultDateTime; + } + + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + return this.GetAccessToken(requestContext, cancellationToken); + } + + public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + return new ValueTask(this.GetAccessToken(requestContext, cancellationToken)); + } + + private AccessToken GetAccessToken(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + this.GetTokenCallback?.Invoke( + requestContext, + cancellationToken); + + DateTimeOffset dateTimeOffsetStart = this.DefaultDateTime ?? DateTimeOffset.UtcNow; + DateTimeOffset dateTimeOffsetExpiration = dateTimeOffsetStart.AddHours(1); + + string nbfValue = dateTimeOffsetStart.ToUnixTimeSeconds().ToString(); + string expValue = dateTimeOffsetExpiration.ToUnixTimeSeconds().ToString(); + + string header = @"{ + ""alg"":""RS256"", + ""kid"":""x_9KSusKU5YcHf4"", + ""typ"":""JWT"" + }"; + + string payload = @"{ + ""oid"":""96313034-4739-43cb-93cd-74193adbe5b6"", + ""scp"":""user_impersonation"", + ""groups"":[ + ""7ce1d003-4cb3-4879-b7c5-74062a35c66e"", + ""e99ff30c-c229-4c67-ab29-30a6aebc3e58"", + ""5549bb62-c77b-4305-bda9-9ec66b85d9e4"", + ""c44fd685-5c58-452c-aaf7-13ce75184f65"", + ""be895215-eab5-43b7-9536-9ef8fe130330"" + ], + ""nbf"":" + nbfValue + @", + ""exp"":" + expValue + @", + ""iat"":1596592335, + ""iss"":""https://sts.fake-issuer.net/7b1999a1-dfd7-440e-8204-00170979b984"", + ""aud"":""https://localhost.localhost"" + }"; + + string headerBase64 = Base64UrlEncoder.Encode(header); + string payloadBase64 = Base64UrlEncoder.Encode(payload); + + string token = headerBase64 + "." + payloadBase64 + "." + Base64UrlEncoder.Encode(this.masterKey); + + return new AccessToken(token, dateTimeOffsetExpiration); + } + } +} diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Performance.Tests/Mocks/MockDocumentClient.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Performance.Tests/Mocks/MockDocumentClient.cs index 26828a64a9..8082668c92 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Performance.Tests/Mocks/MockDocumentClient.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Performance.Tests/Mocks/MockDocumentClient.cs @@ -107,21 +107,28 @@ internal override Task GetPartitionKeyRangeCacheAsync() return Task.FromResult(this.partitionKeyRangeCache.Object); } - string ICosmosAuthorizationTokenProvider.GetUserAuthorizationToken( + ValueTask ICosmosAuthorizationTokenProvider.GetUserAuthorizationTokenAsync( string resourceAddress, string resourceType, string requestVerb, INameValueCollection headers, - AuthorizationTokenType tokenType) // unused, use token based upon what is passed in constructor + AuthorizationTokenType tokenType, + CosmosDiagnosticsContext diagnosticsContext) // unused, use token based upon what is passed in constructor { // this is masterkey authZ headers[HttpConstants.HttpHeaders.XDate] = DateTime.UtcNow.ToString("r", CultureInfo.InvariantCulture); string authorization = AuthorizationHelper.GenerateKeyAuthorizationSignature( - requestVerb, resourceAddress, resourceType, headers, this.authKeyHashFunction, out AuthorizationHelper.ArrayOwner payload); + verb: requestVerb, + resourceId: resourceAddress, + resourceType: resourceType, + headers: headers, + stringHMACSHA256Helper: this.authKeyHashFunction, + payload: out AuthorizationHelper.ArrayOwner payload); + using (payload) { - return authorization; + return new ValueTask(authorization); } } diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DirectContractTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DirectContractTests.cs index 4fb1c16418..c93a0689a1 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DirectContractTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DirectContractTests.cs @@ -108,6 +108,7 @@ public void ProjectPackageDependenciesTest() { "System.Threading.Tasks.Extensions", new Version(4, 5, 2) }, { "System.ValueTuple", new Version(4, 5, 0) }, { "Microsoft.Bcl.HashCode", new Version(1, 1, 0) }, + { "Azure.Core", new Version(1, 3, 0) }, }; Assert.AreEqual(projectDependencies.Count, baselineDependencies.Count); diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json index 4ba6d19b34..000efca237 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetPreviewSDKAPI.json @@ -151,6 +151,36 @@ "Type": "Property", "Attributes": [], "MethodInfo": null + }, + "Void .ctor(System.String, Azure.Core.TokenCredential, Microsoft.Azure.Cosmos.CosmosClientOptions)": { + "Type": "Constructor", + "Attributes": [], + "MethodInfo": "Void .ctor(System.String, Azure.Core.TokenCredential, Microsoft.Azure.Cosmos.CosmosClientOptions)" + } + }, + "NestedTypes": {} + }, + "CosmosClientOptions": { + "Subclasses": {}, + "Members": { + "System.Nullable`1[System.TimeSpan] get_TokenCredentialBackgroundRefreshInterval()[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { + "Type": "Method", + "Attributes": [ + "CompilerGeneratedAttribute" + ], + "MethodInfo": "System.Nullable`1[System.TimeSpan] get_TokenCredentialBackgroundRefreshInterval()" + }, + "System.Nullable`1[System.TimeSpan] TokenCredentialBackgroundRefreshInterval": { + "Type": "Property", + "Attributes": [], + "MethodInfo": null + }, + "Void set_TokenCredentialBackgroundRefreshInterval(System.Nullable`1[System.TimeSpan])[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { + "Type": "Method", + "Attributes": [ + "CompilerGeneratedAttribute" + ], + "MethodInfo": "Void set_TokenCredentialBackgroundRefreshInterval(System.Nullable`1[System.TimeSpan])" } }, "NestedTypes": {} diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosAuthorizationTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosAuthorizationTests.cs new file mode 100644 index 0000000000..cd90334c97 --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosAuthorizationTests.cs @@ -0,0 +1,127 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +//------------------------------------------------------------ + +namespace Microsoft.Azure.Cosmos.Tests +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Security.Cryptography.X509Certificates; + using System.Threading.Tasks; + using Microsoft.Azure.Cosmos.SDK.EmulatorTests; + using Microsoft.Azure.Documents; + using Microsoft.Azure.Documents.Collections; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class CosmosAuthorizationTests + { + public CosmosAuthorizationTests() + { + } + + [TestMethod] + public async Task ResourceTokenAsync() + { + AuthorizationTokenProvider cosmosAuthorization = new AuthorizationTokenProviderResourceToken("VGhpcyBpcyBhIHNhbXBsZSBzdHJpbmc="); + + { + StoreResponseNameValueCollection headers = new StoreResponseNameValueCollection(); + (string token, string payload) = await cosmosAuthorization.GetUserAuthorizationAsync( + "dbs\\test", + ResourceType.Database.ToResourceTypeString(), + "GET", + headers, + AuthorizationTokenType.PrimaryMasterKey); + + Assert.AreEqual("VGhpcyBpcyBhIHNhbXBsZSBzdHJpbmc%3d", token); + Assert.IsNull(payload); + } + + { + StoreResponseNameValueCollection headers = new StoreResponseNameValueCollection(); + (string token, string payload) = await cosmosAuthorization.GetUserAuthorizationAsync( + "dbs\\test\\colls\\abc", + ResourceType.Collection.ToResourceTypeString(), + "PUT", + headers, + AuthorizationTokenType.PrimaryMasterKey); + + Assert.AreEqual("VGhpcyBpcyBhIHNhbXBsZSBzdHJpbmc%3d", token); + Assert.IsNull(payload); + } + + { + StoreResponseNameValueCollection headers = new StoreResponseNameValueCollection(); + (string token, string payload) = await cosmosAuthorization.GetUserAuthorizationAsync( + "dbs\\test\\colls\\abc\\docs\\1234", + ResourceType.Document.ToResourceTypeString(), + "GET", + headers, + AuthorizationTokenType.PrimaryMasterKey); + + Assert.AreEqual("VGhpcyBpcyBhIHNhbXBsZSBzdHJpbmc%3d", token); + Assert.IsNull(payload); + } + } + + [TestMethod] + public async Task TokenAuthAsync() + { + LocalEmulatorTokenCredential simpleEmulatorTokenCredential = new LocalEmulatorTokenCredential( + "VGhpcyBpcyBhIHNhbXBsZSBzdHJpbmc=", + defaultDateTime: new DateTime(2020, 9, 21, 9, 9, 9, DateTimeKind.Utc)); + + AuthorizationTokenProvider cosmosAuthorization = new AuthorizationTokenProviderTokenCredential( + simpleEmulatorTokenCredential, + "https://localhost:8081", + backgroundTokenCredentialRefreshInterval: TimeSpan.FromSeconds(1)); + + { + StoreResponseNameValueCollection headers = new StoreResponseNameValueCollection(); + (string token, string payload) = await cosmosAuthorization.GetUserAuthorizationAsync( + "dbs\\test", + ResourceType.Database.ToResourceTypeString(), + "GET", + headers, + AuthorizationTokenType.PrimaryMasterKey); + + Assert.AreEqual( + "type%3daad%26ver%3d1.0%26sig%3dew0KICAgICAgICAgICAgICAgICJhbGciOiJSUzI1NiIsDQogICAgICAgICAgICAgICAgImtpZCI6InhfOUtTdXNLVTVZY0hmNCIsDQogICAgICAgICAgICAgICAgInR5cCI6IkpXVCINCiAgICAgICAgICAgIH0.ew0KICAgICAgICAgICAgICAgICJvaWQiOiI5NjMxMzAzNC00NzM5LTQzY2ItOTNjZC03NDE5M2FkYmU1YjYiLA0KICAgICAgICAgICAgICAgICJzY3AiOiJ1c2VyX2ltcGVyc29uYXRpb24iLA0KICAgICAgICAgICAgICAgICJncm91cHMiOlsNCiAgICAgICAgICAgICAgICAgICAgIjdjZTFkMDAzLTRjYjMtNDg3OS1iN2M1LTc0MDYyYTM1YzY2ZSIsDQogICAgICAgICAgICAgICAgICAgICJlOTlmZjMwYy1jMjI5LTRjNjctYWIyOS0zMGE2YWViYzNlNTgiLA0KICAgICAgICAgICAgICAgICAgICAiNTU0OWJiNjItYzc3Yi00MzA1LWJkYTktOWVjNjZiODVkOWU0IiwNCiAgICAgICAgICAgICAgICAgICAgImM0NGZkNjg1LTVjNTgtNDUyYy1hYWY3LTEzY2U3NTE4NGY2NSIsDQogICAgICAgICAgICAgICAgICAgICJiZTg5NTIxNS1lYWI1LTQzYjctOTUzNi05ZWY4ZmUxMzAzMzAiDQogICAgICAgICAgICAgICAgXSwNCiAgICAgICAgICAgICAgICAibmJmIjoxNjAwNjc5MzQ5LA0KICAgICAgICAgICAgICAgICJleHAiOjE2MDA2ODI5NDksDQogICAgICAgICAgICAgICAgImlhdCI6MTU5NjU5MjMzNSwNCiAgICAgICAgICAgICAgICAiaXNzIjoiaHR0cHM6Ly9zdHMuZmFrZS1pc3N1ZXIubmV0LzdiMTk5OWExLWRmZDctNDQwZS04MjA0LTAwMTcwOTc5Yjk4NCIsDQogICAgICAgICAgICAgICAgImF1ZCI6Imh0dHBzOi8vbG9jYWxob3N0LmxvY2FsaG9zdCINCiAgICAgICAgICAgIH0.VkdocGN5QnBjeUJoSUhOaGJYQnNaU0J6ZEhKcGJtYz0" + , token); + Assert.IsNull(payload); + } + + { + StoreResponseNameValueCollection headers = new StoreResponseNameValueCollection(); + (string token, string payload) = await cosmosAuthorization.GetUserAuthorizationAsync( + "dbs\\test\\colls\\abc", + ResourceType.Collection.ToResourceTypeString(), + "PUT", + headers, + AuthorizationTokenType.PrimaryMasterKey); + + Assert.AreEqual( + "type%3daad%26ver%3d1.0%26sig%3dew0KICAgICAgICAgICAgICAgICJhbGciOiJSUzI1NiIsDQogICAgICAgICAgICAgICAgImtpZCI6InhfOUtTdXNLVTVZY0hmNCIsDQogICAgICAgICAgICAgICAgInR5cCI6IkpXVCINCiAgICAgICAgICAgIH0.ew0KICAgICAgICAgICAgICAgICJvaWQiOiI5NjMxMzAzNC00NzM5LTQzY2ItOTNjZC03NDE5M2FkYmU1YjYiLA0KICAgICAgICAgICAgICAgICJzY3AiOiJ1c2VyX2ltcGVyc29uYXRpb24iLA0KICAgICAgICAgICAgICAgICJncm91cHMiOlsNCiAgICAgICAgICAgICAgICAgICAgIjdjZTFkMDAzLTRjYjMtNDg3OS1iN2M1LTc0MDYyYTM1YzY2ZSIsDQogICAgICAgICAgICAgICAgICAgICJlOTlmZjMwYy1jMjI5LTRjNjctYWIyOS0zMGE2YWViYzNlNTgiLA0KICAgICAgICAgICAgICAgICAgICAiNTU0OWJiNjItYzc3Yi00MzA1LWJkYTktOWVjNjZiODVkOWU0IiwNCiAgICAgICAgICAgICAgICAgICAgImM0NGZkNjg1LTVjNTgtNDUyYy1hYWY3LTEzY2U3NTE4NGY2NSIsDQogICAgICAgICAgICAgICAgICAgICJiZTg5NTIxNS1lYWI1LTQzYjctOTUzNi05ZWY4ZmUxMzAzMzAiDQogICAgICAgICAgICAgICAgXSwNCiAgICAgICAgICAgICAgICAibmJmIjoxNjAwNjc5MzQ5LA0KICAgICAgICAgICAgICAgICJleHAiOjE2MDA2ODI5NDksDQogICAgICAgICAgICAgICAgImlhdCI6MTU5NjU5MjMzNSwNCiAgICAgICAgICAgICAgICAiaXNzIjoiaHR0cHM6Ly9zdHMuZmFrZS1pc3N1ZXIubmV0LzdiMTk5OWExLWRmZDctNDQwZS04MjA0LTAwMTcwOTc5Yjk4NCIsDQogICAgICAgICAgICAgICAgImF1ZCI6Imh0dHBzOi8vbG9jYWxob3N0LmxvY2FsaG9zdCINCiAgICAgICAgICAgIH0.VkdocGN5QnBjeUJoSUhOaGJYQnNaU0J6ZEhKcGJtYz0" + , token); + Assert.IsNull(payload); + } + + { + StoreResponseNameValueCollection headers = new StoreResponseNameValueCollection(); + (string token, string payload) = await cosmosAuthorization.GetUserAuthorizationAsync( + "dbs\\test\\colls\\abc\\docs\\1234", + ResourceType.Document.ToResourceTypeString(), + "GET", + headers, + AuthorizationTokenType.PrimaryMasterKey); + + Assert.AreEqual( + "type%3daad%26ver%3d1.0%26sig%3dew0KICAgICAgICAgICAgICAgICJhbGciOiJSUzI1NiIsDQogICAgICAgICAgICAgICAgImtpZCI6InhfOUtTdXNLVTVZY0hmNCIsDQogICAgICAgICAgICAgICAgInR5cCI6IkpXVCINCiAgICAgICAgICAgIH0.ew0KICAgICAgICAgICAgICAgICJvaWQiOiI5NjMxMzAzNC00NzM5LTQzY2ItOTNjZC03NDE5M2FkYmU1YjYiLA0KICAgICAgICAgICAgICAgICJzY3AiOiJ1c2VyX2ltcGVyc29uYXRpb24iLA0KICAgICAgICAgICAgICAgICJncm91cHMiOlsNCiAgICAgICAgICAgICAgICAgICAgIjdjZTFkMDAzLTRjYjMtNDg3OS1iN2M1LTc0MDYyYTM1YzY2ZSIsDQogICAgICAgICAgICAgICAgICAgICJlOTlmZjMwYy1jMjI5LTRjNjctYWIyOS0zMGE2YWViYzNlNTgiLA0KICAgICAgICAgICAgICAgICAgICAiNTU0OWJiNjItYzc3Yi00MzA1LWJkYTktOWVjNjZiODVkOWU0IiwNCiAgICAgICAgICAgICAgICAgICAgImM0NGZkNjg1LTVjNTgtNDUyYy1hYWY3LTEzY2U3NTE4NGY2NSIsDQogICAgICAgICAgICAgICAgICAgICJiZTg5NTIxNS1lYWI1LTQzYjctOTUzNi05ZWY4ZmUxMzAzMzAiDQogICAgICAgICAgICAgICAgXSwNCiAgICAgICAgICAgICAgICAibmJmIjoxNjAwNjc5MzQ5LA0KICAgICAgICAgICAgICAgICJleHAiOjE2MDA2ODI5NDksDQogICAgICAgICAgICAgICAgImlhdCI6MTU5NjU5MjMzNSwNCiAgICAgICAgICAgICAgICAiaXNzIjoiaHR0cHM6Ly9zdHMuZmFrZS1pc3N1ZXIubmV0LzdiMTk5OWExLWRmZDctNDQwZS04MjA0LTAwMTcwOTc5Yjk4NCIsDQogICAgICAgICAgICAgICAgImF1ZCI6Imh0dHBzOi8vbG9jYWxob3N0LmxvY2FsaG9zdCINCiAgICAgICAgICAgIH0.VkdocGN5QnBjeUJoSUhOaGJYQnNaU0J6ZEhKcGJtYz0" + , token); + Assert.IsNull(payload); + } + } + } +} diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosClientOptionsUnitTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosClientOptionsUnitTests.cs index 0c2702bd08..c71482330d 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosClientOptionsUnitTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosClientOptionsUnitTests.cs @@ -28,7 +28,7 @@ public class CosmosClientOptionsUnitTests public void VerifyCosmosConfigurationPropertiesGetUpdated() { string endpoint = AccountEndpoint; - string key = Guid.NewGuid().ToString(); + string key = MockCosmosUtil.RandomInvalidCorrectlyFormatedAuthKey; string region = Regions.WestCentralUS; ConnectionMode connectionMode = ConnectionMode.Gateway; TimeSpan requestTimeout = TimeSpan.FromDays(1); @@ -404,10 +404,9 @@ public void VerifyHttpClientFactoryWebProxySet() public void HttpClientFactoryBuildsConnectionPolicy() { string endpoint = AccountEndpoint; - string key = Guid.NewGuid().ToString(); CosmosClientBuilder cosmosClientBuilder = new CosmosClientBuilder( accountEndpoint: endpoint, - authKeyOrResourceToken: key) + authKeyOrResourceToken: MockCosmosUtil.RandomInvalidCorrectlyFormatedAuthKey) .WithHttpClientFactory(this.HttpClientFactoryDelegate); CosmosClient cosmosClient = cosmosClientBuilder.Build(new MockDocumentClient()); CosmosClientOptions clientOptions = cosmosClient.ClientOptions; @@ -422,7 +421,7 @@ public void WithLimitToEndpointAffectsEndpointDiscovery() { CosmosClientBuilder cosmosClientBuilder = new CosmosClientBuilder( accountEndpoint: AccountEndpoint, - authKeyOrResourceToken: Guid.NewGuid().ToString()); + authKeyOrResourceToken: MockCosmosUtil.RandomInvalidCorrectlyFormatedAuthKey); CosmosClientOptions cosmosClientOptions = cosmosClientBuilder.Build(new MockDocumentClient()).ClientOptions; Assert.IsFalse(cosmosClientOptions.LimitToEndpoint); diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/GatewayAccountReaderTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/GatewayAccountReaderTests.cs index 087b155f27..acfe5dbb20 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/GatewayAccountReaderTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/GatewayAccountReaderTests.cs @@ -2,8 +2,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. //------------------------------------------------------------ -using Microsoft.Azure.Documents.Collections; - namespace Microsoft.Azure.Cosmos { using System; @@ -14,6 +12,7 @@ namespace Microsoft.Azure.Cosmos using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Microsoft.Azure.Documents; + using Microsoft.Azure.Documents.Collections; using Microsoft.Azure.Cosmos.Tests; /// @@ -29,12 +28,10 @@ public async Task GatewayAccountReader_MessageHandler() HttpClient staticHttpClient = new HttpClient(messageHandler); GatewayAccountReader accountReader = new GatewayAccountReader( - new Uri("https://localhost"), - Mock.Of(), - false, - null, - new ConnectionPolicy(), - MockCosmosUtil.CreateCosmosHttpClient(() => staticHttpClient)); + serviceEndpoint: new Uri("https://localhost"), + cosmosAuthorization: Mock.Of(), + connectionPolicy: new ConnectionPolicy(), + httpClient: MockCosmosUtil.CreateCosmosHttpClient(() => staticHttpClient)); DocumentClientException exception = await Assert.ThrowsExceptionAsync(() => accountReader.InitializeReaderAsync()); Assert.AreEqual(HttpStatusCode.Conflict, exception.StatusCode); diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Microsoft.Azure.Cosmos.Tests.csproj b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Microsoft.Azure.Cosmos.Tests.csproj index 5f761f6f00..be3e6f9962 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Microsoft.Azure.Cosmos.Tests.csproj +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Microsoft.Azure.Cosmos.Tests.csproj @@ -41,7 +41,13 @@ + + + + + + diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/RetryHandlerTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/RetryHandlerTests.cs index da6d794f66..3ceb8a9f7a 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/RetryHandlerTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/RetryHandlerTests.cs @@ -105,10 +105,10 @@ public async Task RetryHandlerDoesNotRetryOnException() [TestMethod] public async Task RetryHandlerHttpClientExceptionRefreshesLocations() { - DocumentClient dc = new MockDocumentClient(RetryHandlerTests.TestUri, "test"); + DocumentClient dc = new MockDocumentClient(RetryHandlerTests.TestUri, MockCosmosUtil.RandomInvalidCorrectlyFormatedAuthKey); CosmosClient client = new CosmosClient( - RetryHandlerTests.TestUri.OriginalString, - Guid.NewGuid().ToString(), + RetryHandlerTests.TestUri.OriginalString, + MockCosmosUtil.RandomInvalidCorrectlyFormatedAuthKey, new CosmosClientOptions(), dc); diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Utils/MockCosmosUtil.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Utils/MockCosmosUtil.cs index b7900361df..7517e9b0f9 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Utils/MockCosmosUtil.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Utils/MockCosmosUtil.cs @@ -24,6 +24,7 @@ namespace Microsoft.Azure.Cosmos.Tests internal class MockCosmosUtil { public static readonly CosmosSerializerCore Serializer = new CosmosSerializerCore(); + public static readonly string RandomInvalidCorrectlyFormatedAuthKey = "CV60UDtH10CFKR0GxBl/Wg=="; public static CosmosClient CreateMockCosmosClient( Action customizeClientBuilder = null, @@ -39,11 +40,8 @@ public static CosmosClient CreateMockCosmosClient( documentClient = new MockDocumentClient(); } - CosmosClientBuilder cosmosClientBuilder = new CosmosClientBuilder("http://localhost", Guid.NewGuid().ToString()); - if (customizeClientBuilder != null) - { - customizeClientBuilder(cosmosClientBuilder); - } + CosmosClientBuilder cosmosClientBuilder = new CosmosClientBuilder("http://localhost", MockCosmosUtil.RandomInvalidCorrectlyFormatedAuthKey); + customizeClientBuilder?.Invoke(cosmosClientBuilder); return cosmosClientBuilder.Build(documentClient); } diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Utils/MockDocumentClient.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Utils/MockDocumentClient.cs index 2fd3b4582d..c789399801 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Utils/MockDocumentClient.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Utils/MockDocumentClient.cs @@ -29,7 +29,7 @@ internal class MockDocumentClient : DocumentClient, IAuthorizationTokenProvider, private Cosmos.ConsistencyLevel accountConsistencyLevel; public MockDocumentClient() - : base(new Uri("http://localhost"), null) + : base(new Uri("http://localhost"), MockCosmosUtil.RandomInvalidCorrectlyFormatedAuthKey) { this.Init(); } @@ -53,12 +53,6 @@ public MockDocumentClient(Uri serviceEndpoint, string authKeyOrResourceToken, Co this.Init(); } - public MockDocumentClient(Uri serviceEndpoint, IList permissionFeed, ConnectionPolicy connectionPolicy = null, Documents.ConsistencyLevel? desiredConsistencyLevel = null) - : base(serviceEndpoint, permissionFeed, connectionPolicy, desiredConsistencyLevel) - { - this.Init(); - } - public MockDocumentClient(Uri serviceEndpoint, SecureString authKey, JsonSerializerSettings serializerSettings, ConnectionPolicy connectionPolicy = null, Documents.ConsistencyLevel? desiredConsistencyLevel = null) : base(serviceEndpoint, authKey, serializerSettings, connectionPolicy, desiredConsistencyLevel) { @@ -71,12 +65,6 @@ public MockDocumentClient(Uri serviceEndpoint, string authKeyOrResourceToken, Js this.Init(); } - internal MockDocumentClient(Uri serviceEndpoint, IList resourceTokens, ConnectionPolicy connectionPolicy = null, Documents.ConsistencyLevel? desiredConsistencyLevel = null) - : base(serviceEndpoint, resourceTokens, connectionPolicy, desiredConsistencyLevel) - { - this.Init(); - } - internal MockDocumentClient( Uri serviceEndpoint, string authKeyOrResourceToken, @@ -146,14 +134,15 @@ internal override Task QueryPartitionProvider return new ValueTask<(string token, string payload)>((null, null)); } - string ICosmosAuthorizationTokenProvider.GetUserAuthorizationToken( + ValueTask ICosmosAuthorizationTokenProvider.GetUserAuthorizationTokenAsync( string resourceAddress, string resourceType, string requestVerb, INameValueCollection headers, - AuthorizationTokenType tokenType) /* unused, use token based upon what is passed in constructor */ + AuthorizationTokenType tokenType, + CosmosDiagnosticsContext diagnosticsContext) /* unused, use token based upon what is passed in constructor */ { - return null; + return new ValueTask((string)null); } internal virtual IReadOnlyList ResolveOverlapingPartitionKeyRanges(string collectionRid, Documents.Routing.Range range, bool forceRefresh) diff --git a/templates/emulator-setup.yml b/templates/emulator-setup.yml index 29e46fc969..533e07a19d 100644 --- a/templates/emulator-setup.yml +++ b/templates/emulator-setup.yml @@ -11,7 +11,7 @@ steps: mkdir "$env:temp\Azure Cosmos DB Emulator" lessmsi x "$env:temp\azure-cosmosdb-emulator.msi" "$env:temp\Azure Cosmos DB Emulator\" Write-Host "Starting Comsos DB Emulator" -ForegroundColor green - Start-Process "$env:temp\Azure Cosmos DB Emulator\SourceDir\Azure Cosmos DB Emulator\CosmosDB.Emulator.exe" "/NoExplorer /NoUI /DisableRateLimiting /PartitionCount=100 /Consistency=Strong /enableRio /EnablePreview" -Verb RunAs + Start-Process "$env:temp\Azure Cosmos DB Emulator\SourceDir\Azure Cosmos DB Emulator\CosmosDB.Emulator.exe" "/NoExplorer /NoUI /DisableRateLimiting /PartitionCount=100 /Consistency=Strong /enableRio /EnablePreview /EnableAadAuthentication" -Verb RunAs Import-Module "$env:temp\Azure Cosmos DB Emulator\SourceDir\Azure Cosmos DB Emulator\PSModules\Microsoft.Azure.CosmosDB.Emulator" Get-Item env:* | Sort-Object -Property Name for ($i=0; $i -lt 10; $i++) {