From fd6b2576c7b35847bc680e5089b6e998172735d7 Mon Sep 17 00:00:00 2001 From: "Arooshi Avasthy (from Dev Box)" Date: Thu, 19 Jun 2025 15:49:48 -0700 Subject: [PATCH 1/4] Update AAD audience scope. --- .../src/Authorization/TokenCredentialCache.cs | 18 +++--- .../Utils/LocalEmulatorTokenCredential.cs | 12 ++-- .../CosmosAuthorizationTests.cs | 62 ++++++++++++++++++- 3 files changed, 77 insertions(+), 15 deletions(-) diff --git a/Microsoft.Azure.Cosmos/src/Authorization/TokenCredentialCache.cs b/Microsoft.Azure.Cosmos/src/Authorization/TokenCredentialCache.cs index 588a2814db..201309dd7c 100644 --- a/Microsoft.Azure.Cosmos/src/Authorization/TokenCredentialCache.cs +++ b/Microsoft.Azure.Cosmos/src/Authorization/TokenCredentialCache.cs @@ -35,8 +35,10 @@ internal sealed class TokenCredentialCache : IDisposable // The token refresh retries half the time. Given default of 1hr it will retry at 30m, 15, 7.5, 3.75, 1.875 // If the background refresh fails with less than a minute then just allow the request to hit the exception. public static readonly TimeSpan MinimumTimeBetweenBackgroundRefreshInterval = TimeSpan.FromMinutes(1); - - private const string ScopeFormat = "https://{0}/.default"; + + private const string DefaultScopeFormat = "https://{0}/.default"; + private const string FabricScope = "https://cosmos.azure.com/.default"; + private readonly TokenRequestContext tokenRequestContext; private readonly TokenCredential tokenCredential; private readonly CancellationTokenSource cancellationTokenSource; @@ -62,13 +64,15 @@ internal TokenCredentialCache( if (accountEndpoint == null) { throw new ArgumentNullException(nameof(accountEndpoint)); - } - + } + this.tokenRequestContext = new TokenRequestContext(new string[] { - string.Format(TokenCredentialCache.ScopeFormat, accountEndpoint.Host) - }); - + accountEndpoint.Host.EndsWith("cosmos.fabric.microsoft.com", StringComparison.OrdinalIgnoreCase) + ? FabricScope + : string.Format(DefaultScopeFormat, accountEndpoint.Host) + }); + if (backgroundTokenCredentialRefreshInterval.HasValue) { if (backgroundTokenCredentialRefreshInterval.Value <= TimeSpan.Zero) 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 index e71eea6ca0..6d0a1b2556 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Utils/LocalEmulatorTokenCredential.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Utils/LocalEmulatorTokenCredential.cs @@ -18,15 +18,18 @@ public class LocalEmulatorTokenCredential : TokenCredential private readonly DateTime? DefaultDateTime = null; private readonly Action GetTokenCallback; private readonly string masterKey; + private readonly string expectedScope; internal LocalEmulatorTokenCredential( string masterKey = null, Action getTokenCallback = null, - DateTime? defaultDateTime = null) + DateTime? defaultDateTime = null, + string expectedScope = "https://127.0.0.1/.default") { this.masterKey = masterKey; this.GetTokenCallback = getTokenCallback; - this.DefaultDateTime = defaultDateTime; + this.DefaultDateTime = defaultDateTime; + this.expectedScope = expectedScope; } public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) @@ -40,9 +43,8 @@ public override ValueTask GetTokenAsync(TokenRequestContext request } private AccessToken GetAccessToken(TokenRequestContext requestContext, CancellationToken cancellationToken) - { - // Verify that the request context is a valid URI - Assert.AreEqual("https://127.0.0.1/.default", requestContext.Scopes.First()); + { + Assert.AreEqual(this.expectedScope, requestContext.Scopes.First()); this.GetTokenCallback?.Invoke( requestContext, diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosAuthorizationTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosAuthorizationTests.cs index bdf4fcbe29..e8ddb0f55c 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosAuthorizationTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosAuthorizationTests.cs @@ -103,7 +103,7 @@ public async Task TokenAuthAsync() AuthorizationTokenType.PrimaryMasterKey); Assert.AreEqual( - "type%3daad%26ver%3d1.0%26sig%3dew0KICAgICAgICAgICAgICAgICJhbGciOiJSUzI1NiIsDQogICAgICAgICAgICAgICAgImtpZCI6InhfOUtTdXNLVTVZY0hmNCIsDQogICAgICAgICAgICAgICAgInR5cCI6IkpXVCINCiAgICAgICAgICAgIH0.ew0KICAgICAgICAgICAgICAgICJvaWQiOiI5NjMxMzAzNC00NzM5LTQzY2ItOTNjZC03NDE5M2FkYmU1YjYiLA0KICAgICAgICAgICAgICAgICJ0aWQiOiI3YjE5OTlhMS1kZmQ3LTQ0MGUtODIwNC0wMDE3MDk3OWI5ODQiLA0KICAgICAgICAgICAgICAgICJzY3AiOiJ1c2VyX2ltcGVyc29uYXRpb24iLA0KICAgICAgICAgICAgICAgICJncm91cHMiOlsNCiAgICAgICAgICAgICAgICAgICAgIjdjZTFkMDAzLTRjYjMtNDg3OS1iN2M1LTc0MDYyYTM1YzY2ZSIsDQogICAgICAgICAgICAgICAgICAgICJlOTlmZjMwYy1jMjI5LTRjNjctYWIyOS0zMGE2YWViYzNlNTgiLA0KICAgICAgICAgICAgICAgICAgICAiNTU0OWJiNjItYzc3Yi00MzA1LWJkYTktOWVjNjZiODVkOWU0IiwNCiAgICAgICAgICAgICAgICAgICAgImM0NGZkNjg1LTVjNTgtNDUyYy1hYWY3LTEzY2U3NTE4NGY2NSIsDQogICAgICAgICAgICAgICAgICAgICJiZTg5NTIxNS1lYWI1LTQzYjctOTUzNi05ZWY4ZmUxMzAzMzAiDQogICAgICAgICAgICAgICAgXSwNCiAgICAgICAgICAgICAgICAibmJmIjoxOTE2MjEyMTQ5LA0KICAgICAgICAgICAgICAgICJleHAiOjE5MTYyMTU3NDksDQogICAgICAgICAgICAgICAgImlhdCI6MTU5NjU5MjMzNSwNCiAgICAgICAgICAgICAgICAiaXNzIjoiaHR0cHM6Ly9zdHMuZmFrZS1pc3N1ZXIubmV0LzdiMTk5OWExLWRmZDctNDQwZS04MjA0LTAwMTcwOTc5Yjk4NCIsDQogICAgICAgICAgICAgICAgImF1ZCI6Imh0dHBzOi8vbG9jYWxob3N0LmxvY2FsaG9zdCINCiAgICAgICAgICAgIH0.VkdocGN5QnBjeUJoSUhOaGJYQnNaU0J6ZEhKcGJtYz0", + "type%3daad%26ver%3d1.0%26sig%3dewogICAgICAgICAgICAgICAgImFsZyI6IlJTMjU2IiwKICAgICAgICAgICAgICAgICJraWQiOiJ4XzlLU3VzS1U1WWNIZjQiLAogICAgICAgICAgICAgICAgInR5cCI6IkpXVCIKICAgICAgICAgICAgfQ.ewogICAgICAgICAgICAgICAgIm9pZCI6Ijk2MzEzMDM0LTQ3MzktNDNjYi05M2NkLTc0MTkzYWRiZTViNiIsCiAgICAgICAgICAgICAgICAidGlkIjoiN2IxOTk5YTEtZGZkNy00NDBlLTgyMDQtMDAxNzA5NzliOTg0IiwKICAgICAgICAgICAgICAgICJzY3AiOiJ1c2VyX2ltcGVyc29uYXRpb24iLAogICAgICAgICAgICAgICAgImdyb3VwcyI6WwogICAgICAgICAgICAgICAgICAgICI3Y2UxZDAwMy00Y2IzLTQ4NzktYjdjNS03NDA2MmEzNWM2NmUiLAogICAgICAgICAgICAgICAgICAgICJlOTlmZjMwYy1jMjI5LTRjNjctYWIyOS0zMGE2YWViYzNlNTgiLAogICAgICAgICAgICAgICAgICAgICI1NTQ5YmI2Mi1jNzdiLTQzMDUtYmRhOS05ZWM2NmI4NWQ5ZTQiLAogICAgICAgICAgICAgICAgICAgICJjNDRmZDY4NS01YzU4LTQ1MmMtYWFmNy0xM2NlNzUxODRmNjUiLAogICAgICAgICAgICAgICAgICAgICJiZTg5NTIxNS1lYWI1LTQzYjctOTUzNi05ZWY4ZmUxMzAzMzAiCiAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgIm5iZiI6MTkxNjIxMjE0OSwKICAgICAgICAgICAgICAgICJleHAiOjE5MTYyMTU3NDksCiAgICAgICAgICAgICAgICAiaWF0IjoxNTk2NTkyMzM1LAogICAgICAgICAgICAgICAgImlzcyI6Imh0dHBzOi8vc3RzLmZha2UtaXNzdWVyLm5ldC83YjE5OTlhMS1kZmQ3LTQ0MGUtODIwNC0wMDE3MDk3OWI5ODQiLAogICAgICAgICAgICAgICAgImF1ZCI6Imh0dHBzOi8vbG9jYWxob3N0LmxvY2FsaG9zdCIKICAgICAgICAgICAgfQ.VkdocGN5QnBjeUJoSUhOaGJYQnNaU0J6ZEhKcGJtYz0", token); Assert.IsNull(payload); } @@ -118,7 +118,7 @@ public async Task TokenAuthAsync() AuthorizationTokenType.PrimaryMasterKey); Assert.AreEqual( - "type%3daad%26ver%3d1.0%26sig%3dew0KICAgICAgICAgICAgICAgICJhbGciOiJSUzI1NiIsDQogICAgICAgICAgICAgICAgImtpZCI6InhfOUtTdXNLVTVZY0hmNCIsDQogICAgICAgICAgICAgICAgInR5cCI6IkpXVCINCiAgICAgICAgICAgIH0.ew0KICAgICAgICAgICAgICAgICJvaWQiOiI5NjMxMzAzNC00NzM5LTQzY2ItOTNjZC03NDE5M2FkYmU1YjYiLA0KICAgICAgICAgICAgICAgICJ0aWQiOiI3YjE5OTlhMS1kZmQ3LTQ0MGUtODIwNC0wMDE3MDk3OWI5ODQiLA0KICAgICAgICAgICAgICAgICJzY3AiOiJ1c2VyX2ltcGVyc29uYXRpb24iLA0KICAgICAgICAgICAgICAgICJncm91cHMiOlsNCiAgICAgICAgICAgICAgICAgICAgIjdjZTFkMDAzLTRjYjMtNDg3OS1iN2M1LTc0MDYyYTM1YzY2ZSIsDQogICAgICAgICAgICAgICAgICAgICJlOTlmZjMwYy1jMjI5LTRjNjctYWIyOS0zMGE2YWViYzNlNTgiLA0KICAgICAgICAgICAgICAgICAgICAiNTU0OWJiNjItYzc3Yi00MzA1LWJkYTktOWVjNjZiODVkOWU0IiwNCiAgICAgICAgICAgICAgICAgICAgImM0NGZkNjg1LTVjNTgtNDUyYy1hYWY3LTEzY2U3NTE4NGY2NSIsDQogICAgICAgICAgICAgICAgICAgICJiZTg5NTIxNS1lYWI1LTQzYjctOTUzNi05ZWY4ZmUxMzAzMzAiDQogICAgICAgICAgICAgICAgXSwNCiAgICAgICAgICAgICAgICAibmJmIjoxOTE2MjEyMTQ5LA0KICAgICAgICAgICAgICAgICJleHAiOjE5MTYyMTU3NDksDQogICAgICAgICAgICAgICAgImlhdCI6MTU5NjU5MjMzNSwNCiAgICAgICAgICAgICAgICAiaXNzIjoiaHR0cHM6Ly9zdHMuZmFrZS1pc3N1ZXIubmV0LzdiMTk5OWExLWRmZDctNDQwZS04MjA0LTAwMTcwOTc5Yjk4NCIsDQogICAgICAgICAgICAgICAgImF1ZCI6Imh0dHBzOi8vbG9jYWxob3N0LmxvY2FsaG9zdCINCiAgICAgICAgICAgIH0.VkdocGN5QnBjeUJoSUhOaGJYQnNaU0J6ZEhKcGJtYz0", token); + "type%3daad%26ver%3d1.0%26sig%3dewogICAgICAgICAgICAgICAgImFsZyI6IlJTMjU2IiwKICAgICAgICAgICAgICAgICJraWQiOiJ4XzlLU3VzS1U1WWNIZjQiLAogICAgICAgICAgICAgICAgInR5cCI6IkpXVCIKICAgICAgICAgICAgfQ.ewogICAgICAgICAgICAgICAgIm9pZCI6Ijk2MzEzMDM0LTQ3MzktNDNjYi05M2NkLTc0MTkzYWRiZTViNiIsCiAgICAgICAgICAgICAgICAidGlkIjoiN2IxOTk5YTEtZGZkNy00NDBlLTgyMDQtMDAxNzA5NzliOTg0IiwKICAgICAgICAgICAgICAgICJzY3AiOiJ1c2VyX2ltcGVyc29uYXRpb24iLAogICAgICAgICAgICAgICAgImdyb3VwcyI6WwogICAgICAgICAgICAgICAgICAgICI3Y2UxZDAwMy00Y2IzLTQ4NzktYjdjNS03NDA2MmEzNWM2NmUiLAogICAgICAgICAgICAgICAgICAgICJlOTlmZjMwYy1jMjI5LTRjNjctYWIyOS0zMGE2YWViYzNlNTgiLAogICAgICAgICAgICAgICAgICAgICI1NTQ5YmI2Mi1jNzdiLTQzMDUtYmRhOS05ZWM2NmI4NWQ5ZTQiLAogICAgICAgICAgICAgICAgICAgICJjNDRmZDY4NS01YzU4LTQ1MmMtYWFmNy0xM2NlNzUxODRmNjUiLAogICAgICAgICAgICAgICAgICAgICJiZTg5NTIxNS1lYWI1LTQzYjctOTUzNi05ZWY4ZmUxMzAzMzAiCiAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgIm5iZiI6MTkxNjIxMjE0OSwKICAgICAgICAgICAgICAgICJleHAiOjE5MTYyMTU3NDksCiAgICAgICAgICAgICAgICAiaWF0IjoxNTk2NTkyMzM1LAogICAgICAgICAgICAgICAgImlzcyI6Imh0dHBzOi8vc3RzLmZha2UtaXNzdWVyLm5ldC83YjE5OTlhMS1kZmQ3LTQ0MGUtODIwNC0wMDE3MDk3OWI5ODQiLAogICAgICAgICAgICAgICAgImF1ZCI6Imh0dHBzOi8vbG9jYWxob3N0LmxvY2FsaG9zdCIKICAgICAgICAgICAgfQ.VkdocGN5QnBjeUJoSUhOaGJYQnNaU0J6ZEhKcGJtYz0", token); Assert.IsNull(payload); } @@ -132,10 +132,66 @@ public async Task TokenAuthAsync() AuthorizationTokenType.PrimaryMasterKey); Assert.AreEqual( - "type%3daad%26ver%3d1.0%26sig%3dew0KICAgICAgICAgICAgICAgICJhbGciOiJSUzI1NiIsDQogICAgICAgICAgICAgICAgImtpZCI6InhfOUtTdXNLVTVZY0hmNCIsDQogICAgICAgICAgICAgICAgInR5cCI6IkpXVCINCiAgICAgICAgICAgIH0.ew0KICAgICAgICAgICAgICAgICJvaWQiOiI5NjMxMzAzNC00NzM5LTQzY2ItOTNjZC03NDE5M2FkYmU1YjYiLA0KICAgICAgICAgICAgICAgICJ0aWQiOiI3YjE5OTlhMS1kZmQ3LTQ0MGUtODIwNC0wMDE3MDk3OWI5ODQiLA0KICAgICAgICAgICAgICAgICJzY3AiOiJ1c2VyX2ltcGVyc29uYXRpb24iLA0KICAgICAgICAgICAgICAgICJncm91cHMiOlsNCiAgICAgICAgICAgICAgICAgICAgIjdjZTFkMDAzLTRjYjMtNDg3OS1iN2M1LTc0MDYyYTM1YzY2ZSIsDQogICAgICAgICAgICAgICAgICAgICJlOTlmZjMwYy1jMjI5LTRjNjctYWIyOS0zMGE2YWViYzNlNTgiLA0KICAgICAgICAgICAgICAgICAgICAiNTU0OWJiNjItYzc3Yi00MzA1LWJkYTktOWVjNjZiODVkOWU0IiwNCiAgICAgICAgICAgICAgICAgICAgImM0NGZkNjg1LTVjNTgtNDUyYy1hYWY3LTEzY2U3NTE4NGY2NSIsDQogICAgICAgICAgICAgICAgICAgICJiZTg5NTIxNS1lYWI1LTQzYjctOTUzNi05ZWY4ZmUxMzAzMzAiDQogICAgICAgICAgICAgICAgXSwNCiAgICAgICAgICAgICAgICAibmJmIjoxOTE2MjEyMTQ5LA0KICAgICAgICAgICAgICAgICJleHAiOjE5MTYyMTU3NDksDQogICAgICAgICAgICAgICAgImlhdCI6MTU5NjU5MjMzNSwNCiAgICAgICAgICAgICAgICAiaXNzIjoiaHR0cHM6Ly9zdHMuZmFrZS1pc3N1ZXIubmV0LzdiMTk5OWExLWRmZDctNDQwZS04MjA0LTAwMTcwOTc5Yjk4NCIsDQogICAgICAgICAgICAgICAgImF1ZCI6Imh0dHBzOi8vbG9jYWxob3N0LmxvY2FsaG9zdCINCiAgICAgICAgICAgIH0.VkdocGN5QnBjeUJoSUhOaGJYQnNaU0J6ZEhKcGJtYz0", token); + "type%3daad%26ver%3d1.0%26sig%3dewogICAgICAgICAgICAgICAgImFsZyI6IlJTMjU2IiwKICAgICAgICAgICAgICAgICJraWQiOiJ4XzlLU3VzS1U1WWNIZjQiLAogICAgICAgICAgICAgICAgInR5cCI6IkpXVCIKICAgICAgICAgICAgfQ.ewogICAgICAgICAgICAgICAgIm9pZCI6Ijk2MzEzMDM0LTQ3MzktNDNjYi05M2NkLTc0MTkzYWRiZTViNiIsCiAgICAgICAgICAgICAgICAidGlkIjoiN2IxOTk5YTEtZGZkNy00NDBlLTgyMDQtMDAxNzA5NzliOTg0IiwKICAgICAgICAgICAgICAgICJzY3AiOiJ1c2VyX2ltcGVyc29uYXRpb24iLAogICAgICAgICAgICAgICAgImdyb3VwcyI6WwogICAgICAgICAgICAgICAgICAgICI3Y2UxZDAwMy00Y2IzLTQ4NzktYjdjNS03NDA2MmEzNWM2NmUiLAogICAgICAgICAgICAgICAgICAgICJlOTlmZjMwYy1jMjI5LTRjNjctYWIyOS0zMGE2YWViYzNlNTgiLAogICAgICAgICAgICAgICAgICAgICI1NTQ5YmI2Mi1jNzdiLTQzMDUtYmRhOS05ZWM2NmI4NWQ5ZTQiLAogICAgICAgICAgICAgICAgICAgICJjNDRmZDY4NS01YzU4LTQ1MmMtYWFmNy0xM2NlNzUxODRmNjUiLAogICAgICAgICAgICAgICAgICAgICJiZTg5NTIxNS1lYWI1LTQzYjctOTUzNi05ZWY4ZmUxMzAzMzAiCiAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgIm5iZiI6MTkxNjIxMjE0OSwKICAgICAgICAgICAgICAgICJleHAiOjE5MTYyMTU3NDksCiAgICAgICAgICAgICAgICAiaWF0IjoxNTk2NTkyMzM1LAogICAgICAgICAgICAgICAgImlzcyI6Imh0dHBzOi8vc3RzLmZha2UtaXNzdWVyLm5ldC83YjE5OTlhMS1kZmQ3LTQ0MGUtODIwNC0wMDE3MDk3OWI5ODQiLAogICAgICAgICAgICAgICAgImF1ZCI6Imh0dHBzOi8vbG9jYWxob3N0LmxvY2FsaG9zdCIKICAgICAgICAgICAgfQ.VkdocGN5QnBjeUJoSUhOaGJYQnNaU0J6ZEhKcGJtYz0", token); Assert.IsNull(payload); } } + + [TestMethod] + public async Task TokenCredentialCache_SetsCorrectScope_BasedOnHost() + { + // Fabric host scenario + string fabricHost = "test.zb9.msit-sql.cosmos.fabric.microsoft.com"; + Uri fabricUri = new Uri($"https://{fabricHost}"); + string expectedFabricScope = "https://cosmos.azure.com/.default"; + + LocalEmulatorTokenCredential fabricCredential = new LocalEmulatorTokenCredential( + masterKey: "testkey", + expectedScope: expectedFabricScope); + + using (AuthorizationTokenProvider fabricAuthorization = new AuthorizationTokenProviderTokenCredential( + fabricCredential, + fabricUri, + backgroundTokenCredentialRefreshInterval: TimeSpan.FromSeconds(1))) + { + StoreResponseNameValueCollection headers = new StoreResponseNameValueCollection(); + (string token, string payload) = await fabricAuthorization.GetUserAuthorizationAsync( + "dbs\\test", + ResourceType.Database.ToResourceTypeString(), + "GET", + headers, + AuthorizationTokenType.PrimaryMasterKey); + + Assert.IsFalse(string.IsNullOrEmpty(token)); + Assert.IsNull(payload); + } + + // Non-fabric host scenario + string customHost = "myaccount.documents.azure.com"; + Uri customUri = new Uri($"https://{customHost}"); + string expectedCustomScope = $"https://{customHost}/.default"; + + LocalEmulatorTokenCredential customCredential = new LocalEmulatorTokenCredential( + masterKey: "testkey", + expectedScope: expectedCustomScope); + + using (AuthorizationTokenProvider customAuthorization = new AuthorizationTokenProviderTokenCredential( + customCredential, + customUri, + backgroundTokenCredentialRefreshInterval: TimeSpan.FromSeconds(1))) + { + StoreResponseNameValueCollection headers = new StoreResponseNameValueCollection(); + (string token, string payload) = await customAuthorization.GetUserAuthorizationAsync( + "dbs\\test", + ResourceType.Database.ToResourceTypeString(), + "GET", + headers, + AuthorizationTokenType.PrimaryMasterKey); + + Assert.IsFalse(string.IsNullOrEmpty(token)); + Assert.IsNull(payload); + } + } [TestMethod] public void TestTokenCredentialCacheMaxAndMinValues() From 224fc0c3e16d605aca935fbdb2cf90b5d183727d Mon Sep 17 00:00:00 2001 From: "Arooshi Avasthy (from Dev Box)" Date: Fri, 20 Jun 2025 15:22:28 -0700 Subject: [PATCH 2/4] Added an environment vaibale for this change. --- .../src/Authorization/TokenCredentialCache.cs | 12 ++++--- .../src/Util/ConfigurationManager.cs | 21 ++++++++++- .../CosmosAuthorizationTests.cs | 36 ++++++++++++++++++- 3 files changed, 63 insertions(+), 6 deletions(-) diff --git a/Microsoft.Azure.Cosmos/src/Authorization/TokenCredentialCache.cs b/Microsoft.Azure.Cosmos/src/Authorization/TokenCredentialCache.cs index 201309dd7c..f57397b5a2 100644 --- a/Microsoft.Azure.Cosmos/src/Authorization/TokenCredentialCache.cs +++ b/Microsoft.Azure.Cosmos/src/Authorization/TokenCredentialCache.cs @@ -37,7 +37,7 @@ internal sealed class TokenCredentialCache : IDisposable public static readonly TimeSpan MinimumTimeBetweenBackgroundRefreshInterval = TimeSpan.FromMinutes(1); private const string DefaultScopeFormat = "https://{0}/.default"; - private const string FabricScope = "https://cosmos.azure.com/.default"; + private const string FabricScope = "https://cosmos.azure.com/.default"; private readonly TokenRequestContext tokenRequestContext; private readonly TokenCredential tokenCredential; @@ -66,11 +66,15 @@ internal TokenCredentialCache( throw new ArgumentNullException(nameof(accountEndpoint)); } + string? scopeOverride = ConfigurationManager.AADScopeOverrideValue(defaultValue: null); + this.tokenRequestContext = new TokenRequestContext(new string[] { - accountEndpoint.Host.EndsWith("cosmos.fabric.microsoft.com", StringComparison.OrdinalIgnoreCase) - ? FabricScope - : string.Format(DefaultScopeFormat, accountEndpoint.Host) + !string.IsNullOrEmpty(scopeOverride) + ? scopeOverride + : (accountEndpoint.Host.EndsWith("cosmos.fabric.microsoft.com", StringComparison.OrdinalIgnoreCase) + ? FabricScope + : string.Format(DefaultScopeFormat, accountEndpoint.Host)) }); if (backgroundTokenCredentialRefreshInterval.HasValue) diff --git a/Microsoft.Azure.Cosmos/src/Util/ConfigurationManager.cs b/Microsoft.Azure.Cosmos/src/Util/ConfigurationManager.cs index 622f81c81b..f9e8dbe93b 100644 --- a/Microsoft.Azure.Cosmos/src/Util/ConfigurationManager.cs +++ b/Microsoft.Azure.Cosmos/src/Util/ConfigurationManager.cs @@ -43,7 +43,12 @@ internal static class ConfigurationManager /// /// Environment variable name to enable thin client mode. /// - internal static readonly string ThinClientModeEnabled = "AZURE_COSMOS_THIN_CLIENT_ENABLED"; + internal static readonly string ThinClientModeEnabled = "AZURE_COSMOS_THIN_CLIENT_ENABLED"; + + /// + /// Environment variable to override AAD scope. + /// + internal static readonly string AADScopeOverride = "AZURE_COSMOS_AAD_SCOPE_OVERRIDE"; /// /// A read-only string containing the environment variable name for capturing the consecutive failure count for reads, before triggering per partition @@ -183,6 +188,20 @@ public static bool IsThinClientEnabled( .GetEnvironmentVariable( variable: ConfigurationManager.ThinClientModeEnabled, defaultValue: defaultValue); + } + + /// + /// Gets the AAD scope value to override. + /// + /// Emoty string for AAD scope if no scope value is provided. + /// AAD scope value. + public static string AADScopeOverrideValue( + string defaultValue) + { + return ConfigurationManager + .GetEnvironmentVariable( + variable: ConfigurationManager.AADScopeOverride, + defaultValue: defaultValue); } /// diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosAuthorizationTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosAuthorizationTests.cs index e8ddb0f55c..3343101001 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosAuthorizationTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosAuthorizationTests.cs @@ -138,7 +138,7 @@ public async Task TokenAuthAsync() } [TestMethod] - public async Task TokenCredentialCache_SetsCorrectScope_BasedOnHost() + public async Task TokenCredentialCache_SetsCorrectFabricScope() { // Fabric host scenario string fabricHost = "test.zb9.msit-sql.cosmos.fabric.microsoft.com"; @@ -192,6 +192,40 @@ public async Task TokenCredentialCache_SetsCorrectScope_BasedOnHost() Assert.IsNull(payload); } } + + [DataTestMethod] + [DataRow("https://env-override/.default", "https://env-override/.default", DisplayName = "EnvVarOverride")] + [DataRow(null, "https://anyhost.documents.azure.com/.default", DisplayName = "NoEnvVar_DefaultScope")] + public async Task TokenCredentialCache_SetsCorrectScope_EnvOverrideOrDefault(string envVarValue, string expectedScope) + { + Environment.SetEnvironmentVariable("AZURE_COSMOS_AAD_SCOPE_OVERRIDE", envVarValue); + + string anyHost = "anyhost.documents.azure.com"; + Uri anyUri = new Uri($"https://{anyHost}"); + + LocalEmulatorTokenCredential credential = new LocalEmulatorTokenCredential( + masterKey: "testkey", + expectedScope: expectedScope); + + using (AuthorizationTokenProvider authorization = new AuthorizationTokenProviderTokenCredential( + credential, + anyUri, + backgroundTokenCredentialRefreshInterval: TimeSpan.FromSeconds(1))) + { + StoreResponseNameValueCollection headers = new StoreResponseNameValueCollection(); + (string token, string payload) = await authorization.GetUserAuthorizationAsync( + "dbs\\test", + ResourceType.Database.ToResourceTypeString(), + "GET", + headers, + AuthorizationTokenType.PrimaryMasterKey); + + Assert.IsFalse(string.IsNullOrEmpty(token)); + Assert.IsNull(payload); + } + + Environment.SetEnvironmentVariable("AZURE_COSMOS_AAD_SCOPE_OVERRIDE", null); + } [TestMethod] public void TestTokenCredentialCacheMaxAndMinValues() From f85b3fdf0d3e454423871a95f98827d9a2d7e593 Mon Sep 17 00:00:00 2001 From: "Arooshi Avasthy (from Dev Box)" Date: Mon, 23 Jun 2025 12:37:51 -0700 Subject: [PATCH 3/4] Remove logic for specifically checking fabric scope. --- .../src/Authorization/TokenCredentialCache.cs | 15 ++--- .../CosmosAuthorizationTests.cs | 57 +------------------ 2 files changed, 7 insertions(+), 65 deletions(-) diff --git a/Microsoft.Azure.Cosmos/src/Authorization/TokenCredentialCache.cs b/Microsoft.Azure.Cosmos/src/Authorization/TokenCredentialCache.cs index f57397b5a2..81dd62bd2c 100644 --- a/Microsoft.Azure.Cosmos/src/Authorization/TokenCredentialCache.cs +++ b/Microsoft.Azure.Cosmos/src/Authorization/TokenCredentialCache.cs @@ -37,7 +37,6 @@ internal sealed class TokenCredentialCache : IDisposable public static readonly TimeSpan MinimumTimeBetweenBackgroundRefreshInterval = TimeSpan.FromMinutes(1); private const string DefaultScopeFormat = "https://{0}/.default"; - private const string FabricScope = "https://cosmos.azure.com/.default"; private readonly TokenRequestContext tokenRequestContext; private readonly TokenCredential tokenCredential; @@ -66,15 +65,13 @@ internal TokenCredentialCache( throw new ArgumentNullException(nameof(accountEndpoint)); } - string? scopeOverride = ConfigurationManager.AADScopeOverrideValue(defaultValue: null); + string? scopeOverride = ConfigurationManager.AADScopeOverrideValue(defaultValue: null); - this.tokenRequestContext = new TokenRequestContext(new string[] - { - !string.IsNullOrEmpty(scopeOverride) - ? scopeOverride - : (accountEndpoint.Host.EndsWith("cosmos.fabric.microsoft.com", StringComparison.OrdinalIgnoreCase) - ? FabricScope - : string.Format(DefaultScopeFormat, accountEndpoint.Host)) + this.tokenRequestContext = new TokenRequestContext(new string[] + { + !string.IsNullOrEmpty(scopeOverride) + ? scopeOverride + : string.Format(DefaultScopeFormat, accountEndpoint.Host) }); if (backgroundTokenCredentialRefreshInterval.HasValue) diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosAuthorizationTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosAuthorizationTests.cs index 3343101001..6d0f436680 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosAuthorizationTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosAuthorizationTests.cs @@ -135,66 +135,11 @@ public async Task TokenAuthAsync() "type%3daad%26ver%3d1.0%26sig%3dewogICAgICAgICAgICAgICAgImFsZyI6IlJTMjU2IiwKICAgICAgICAgICAgICAgICJraWQiOiJ4XzlLU3VzS1U1WWNIZjQiLAogICAgICAgICAgICAgICAgInR5cCI6IkpXVCIKICAgICAgICAgICAgfQ.ewogICAgICAgICAgICAgICAgIm9pZCI6Ijk2MzEzMDM0LTQ3MzktNDNjYi05M2NkLTc0MTkzYWRiZTViNiIsCiAgICAgICAgICAgICAgICAidGlkIjoiN2IxOTk5YTEtZGZkNy00NDBlLTgyMDQtMDAxNzA5NzliOTg0IiwKICAgICAgICAgICAgICAgICJzY3AiOiJ1c2VyX2ltcGVyc29uYXRpb24iLAogICAgICAgICAgICAgICAgImdyb3VwcyI6WwogICAgICAgICAgICAgICAgICAgICI3Y2UxZDAwMy00Y2IzLTQ4NzktYjdjNS03NDA2MmEzNWM2NmUiLAogICAgICAgICAgICAgICAgICAgICJlOTlmZjMwYy1jMjI5LTRjNjctYWIyOS0zMGE2YWViYzNlNTgiLAogICAgICAgICAgICAgICAgICAgICI1NTQ5YmI2Mi1jNzdiLTQzMDUtYmRhOS05ZWM2NmI4NWQ5ZTQiLAogICAgICAgICAgICAgICAgICAgICJjNDRmZDY4NS01YzU4LTQ1MmMtYWFmNy0xM2NlNzUxODRmNjUiLAogICAgICAgICAgICAgICAgICAgICJiZTg5NTIxNS1lYWI1LTQzYjctOTUzNi05ZWY4ZmUxMzAzMzAiCiAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgIm5iZiI6MTkxNjIxMjE0OSwKICAgICAgICAgICAgICAgICJleHAiOjE5MTYyMTU3NDksCiAgICAgICAgICAgICAgICAiaWF0IjoxNTk2NTkyMzM1LAogICAgICAgICAgICAgICAgImlzcyI6Imh0dHBzOi8vc3RzLmZha2UtaXNzdWVyLm5ldC83YjE5OTlhMS1kZmQ3LTQ0MGUtODIwNC0wMDE3MDk3OWI5ODQiLAogICAgICAgICAgICAgICAgImF1ZCI6Imh0dHBzOi8vbG9jYWxob3N0LmxvY2FsaG9zdCIKICAgICAgICAgICAgfQ.VkdocGN5QnBjeUJoSUhOaGJYQnNaU0J6ZEhKcGJtYz0", token); Assert.IsNull(payload); } - } - - [TestMethod] - public async Task TokenCredentialCache_SetsCorrectFabricScope() - { - // Fabric host scenario - string fabricHost = "test.zb9.msit-sql.cosmos.fabric.microsoft.com"; - Uri fabricUri = new Uri($"https://{fabricHost}"); - string expectedFabricScope = "https://cosmos.azure.com/.default"; - - LocalEmulatorTokenCredential fabricCredential = new LocalEmulatorTokenCredential( - masterKey: "testkey", - expectedScope: expectedFabricScope); - - using (AuthorizationTokenProvider fabricAuthorization = new AuthorizationTokenProviderTokenCredential( - fabricCredential, - fabricUri, - backgroundTokenCredentialRefreshInterval: TimeSpan.FromSeconds(1))) - { - StoreResponseNameValueCollection headers = new StoreResponseNameValueCollection(); - (string token, string payload) = await fabricAuthorization.GetUserAuthorizationAsync( - "dbs\\test", - ResourceType.Database.ToResourceTypeString(), - "GET", - headers, - AuthorizationTokenType.PrimaryMasterKey); - - Assert.IsFalse(string.IsNullOrEmpty(token)); - Assert.IsNull(payload); - } - - // Non-fabric host scenario - string customHost = "myaccount.documents.azure.com"; - Uri customUri = new Uri($"https://{customHost}"); - string expectedCustomScope = $"https://{customHost}/.default"; - - LocalEmulatorTokenCredential customCredential = new LocalEmulatorTokenCredential( - masterKey: "testkey", - expectedScope: expectedCustomScope); - - using (AuthorizationTokenProvider customAuthorization = new AuthorizationTokenProviderTokenCredential( - customCredential, - customUri, - backgroundTokenCredentialRefreshInterval: TimeSpan.FromSeconds(1))) - { - StoreResponseNameValueCollection headers = new StoreResponseNameValueCollection(); - (string token, string payload) = await customAuthorization.GetUserAuthorizationAsync( - "dbs\\test", - ResourceType.Database.ToResourceTypeString(), - "GET", - headers, - AuthorizationTokenType.PrimaryMasterKey); - - Assert.IsFalse(string.IsNullOrEmpty(token)); - Assert.IsNull(payload); - } } [DataTestMethod] [DataRow("https://env-override/.default", "https://env-override/.default", DisplayName = "EnvVarOverride")] + [DataRow("https://cosmos.azure.com/.default", "https://cosmos.azure.com/.default", DisplayName = "EnvVarOverride_Fabric")] [DataRow(null, "https://anyhost.documents.azure.com/.default", DisplayName = "NoEnvVar_DefaultScope")] public async Task TokenCredentialCache_SetsCorrectScope_EnvOverrideOrDefault(string envVarValue, string expectedScope) { From 1bfe2bd850d9d47fcab81529780348087776371c Mon Sep 17 00:00:00 2001 From: "Arooshi Avasthy (from Dev Box)" Date: Mon, 23 Jun 2025 15:44:36 -0700 Subject: [PATCH 4/4] Update tests based on review comments. --- .../src/Authorization/TokenCredentialCache.cs | 4 +- .../CosmosAadTests.cs | 17 +++--- .../Utils/LocalEmulatorTokenCredential.cs | 6 +- .../CosmosAuthorizationTests.cs | 56 +++++++++++-------- 4 files changed, 48 insertions(+), 35 deletions(-) diff --git a/Microsoft.Azure.Cosmos/src/Authorization/TokenCredentialCache.cs b/Microsoft.Azure.Cosmos/src/Authorization/TokenCredentialCache.cs index 81dd62bd2c..e844dbdec8 100644 --- a/Microsoft.Azure.Cosmos/src/Authorization/TokenCredentialCache.cs +++ b/Microsoft.Azure.Cosmos/src/Authorization/TokenCredentialCache.cs @@ -36,7 +36,7 @@ internal sealed class TokenCredentialCache : IDisposable // If the background refresh fails with less than a minute then just allow the request to hit the exception. public static readonly TimeSpan MinimumTimeBetweenBackgroundRefreshInterval = TimeSpan.FromMinutes(1); - private const string DefaultScopeFormat = "https://{0}/.default"; + private const string ScopeFormat = "https://{0}/.default"; private readonly TokenRequestContext tokenRequestContext; private readonly TokenCredential tokenCredential; @@ -71,7 +71,7 @@ internal TokenCredentialCache( { !string.IsNullOrEmpty(scopeOverride) ? scopeOverride - : string.Format(DefaultScopeFormat, accountEndpoint.Host) + : string.Format(TokenCredentialCache.ScopeFormat, accountEndpoint.Host) }); if (backgroundTokenCredentialRefreshInterval.HasValue) diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosAadTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosAadTests.cs index 01ffcdc674..40e9a27e90 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosAadTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosAadTests.cs @@ -38,7 +38,7 @@ public async Task AadMockTest(ConnectionMode connectionMode) try { (string endpoint, string authKey) = TestCommon.GetAccountInfo(); - LocalEmulatorTokenCredential simpleEmulatorTokenCredential = new LocalEmulatorTokenCredential(authKey); + LocalEmulatorTokenCredential simpleEmulatorTokenCredential = new LocalEmulatorTokenCredential(expectedScope: "https://127.0.0.1/.default", masterKey: authKey); CosmosClientOptions clientOptions = new CosmosClientOptions() { ConnectionMode = connectionMode, @@ -140,8 +140,9 @@ void GetAadTokenCallBack( (string endpoint, string authKey) = TestCommon.GetAccountInfo(); LocalEmulatorTokenCredential simpleEmulatorTokenCredential = new LocalEmulatorTokenCredential( - authKey, - GetAadTokenCallBack); + expectedScope: "https://127.0.0.1/.default", + masterKey: authKey, + getTokenCallback: GetAadTokenCallBack); CosmosClientOptions clientOptions = new CosmosClientOptions() { @@ -191,8 +192,9 @@ void GetAadTokenCallBack( (string endpoint, string authKey) = TestCommon.GetAccountInfo(); LocalEmulatorTokenCredential simpleEmulatorTokenCredential = new LocalEmulatorTokenCredential( - authKey, - GetAadTokenCallBack); + expectedScope: "https://127.0.0.1/.default", + masterKey: authKey, + getTokenCallback: GetAadTokenCallBack); CosmosClientOptions clientOptions = new CosmosClientOptions() { @@ -232,8 +234,9 @@ void GetAadTokenCallBack( (string endpoint, string authKey) = TestCommon.GetAccountInfo(); LocalEmulatorTokenCredential simpleEmulatorTokenCredential = new LocalEmulatorTokenCredential( - authKey, - GetAadTokenCallBack); + expectedScope: "https://127.0.0.1/.default", + masterKey: authKey, + getTokenCallback: GetAadTokenCallBack); CosmosClientOptions clientOptions = new CosmosClientOptions() { 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 index 6d0a1b2556..c40a5e331f 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Utils/LocalEmulatorTokenCredential.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Utils/LocalEmulatorTokenCredential.cs @@ -20,11 +20,11 @@ public class LocalEmulatorTokenCredential : TokenCredential private readonly string masterKey; private readonly string expectedScope; - internal LocalEmulatorTokenCredential( + internal LocalEmulatorTokenCredential( + string expectedScope, string masterKey = null, Action getTokenCallback = null, - DateTime? defaultDateTime = null, - string expectedScope = "https://127.0.0.1/.default") + DateTime? defaultDateTime = null) { this.masterKey = masterKey; this.GetTokenCallback = getTokenCallback; diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosAuthorizationTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosAuthorizationTests.cs index 6d0f436680..b98c339402 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosAuthorizationTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosAuthorizationTests.cs @@ -84,8 +84,9 @@ public async Task ResourceTokenAsync() [TestMethod] public async Task TokenAuthAsync() { - LocalEmulatorTokenCredential simpleEmulatorTokenCredential = new LocalEmulatorTokenCredential( - "VGhpcyBpcyBhIHNhbXBsZSBzdHJpbmc=", + LocalEmulatorTokenCredential simpleEmulatorTokenCredential = new LocalEmulatorTokenCredential( + expectedScope: "https://127.0.0.1/.default", + masterKey: "VGhpcyBpcyBhIHNhbXBsZSBzdHJpbmc=", defaultDateTime: new DateTime(2030, 9, 21, 9, 9, 9, DateTimeKind.Utc)); using AuthorizationTokenProvider cosmosAuthorization = new AuthorizationTokenProviderTokenCredential( @@ -145,31 +146,40 @@ public async Task TokenCredentialCache_SetsCorrectScope_EnvOverrideOrDefault(str { Environment.SetEnvironmentVariable("AZURE_COSMOS_AAD_SCOPE_OVERRIDE", envVarValue); - string anyHost = "anyhost.documents.azure.com"; - Uri anyUri = new Uri($"https://{anyHost}"); + try + { + string anyHost = "anyhost.documents.azure.com"; + Uri anyUri = new Uri($"https://{anyHost}"); - LocalEmulatorTokenCredential credential = new LocalEmulatorTokenCredential( - masterKey: "testkey", - expectedScope: expectedScope); + LocalEmulatorTokenCredential credential = new LocalEmulatorTokenCredential( + masterKey: "testkey", + expectedScope: expectedScope); - using (AuthorizationTokenProvider authorization = new AuthorizationTokenProviderTokenCredential( - credential, - anyUri, - backgroundTokenCredentialRefreshInterval: TimeSpan.FromSeconds(1))) - { - StoreResponseNameValueCollection headers = new StoreResponseNameValueCollection(); - (string token, string payload) = await authorization.GetUserAuthorizationAsync( - "dbs\\test", - ResourceType.Database.ToResourceTypeString(), - "GET", - headers, - AuthorizationTokenType.PrimaryMasterKey); + using (AuthorizationTokenProvider authorization = new AuthorizationTokenProviderTokenCredential( + credential, + anyUri, + backgroundTokenCredentialRefreshInterval: TimeSpan.FromSeconds(1))) + { + StoreResponseNameValueCollection headers = new StoreResponseNameValueCollection(); + (string token, string payload) = await authorization.GetUserAuthorizationAsync( + "dbs\\test", + ResourceType.Database.ToResourceTypeString(), + "GET", + headers, + AuthorizationTokenType.PrimaryMasterKey); - Assert.IsFalse(string.IsNullOrEmpty(token)); - Assert.IsNull(payload); + Assert.IsFalse(string.IsNullOrEmpty(token)); + Assert.IsNull(payload); + } + } + catch (Exception ex) + { + Assert.Fail($"Test failed with exception: {ex}"); + } + finally + { + Environment.SetEnvironmentVariable("AZURE_COSMOS_AAD_SCOPE_OVERRIDE", null); } - - Environment.SetEnvironmentVariable("AZURE_COSMOS_AAD_SCOPE_OVERRIDE", null); } [TestMethod]