From 71190d0d27e470021d08df7af318247410718459 Mon Sep 17 00:00:00 2001 From: Jonathan Cardenas Date: Mon, 16 Sep 2024 18:15:52 -0700 Subject: [PATCH 01/27] Add flag and enable CAE to AuthorizeRequestInternal --- .../src/ChallengeBasedAuthenticationPolicy.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs b/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs index ec82ee2c1d63..61a5d1075ee4 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs @@ -15,15 +15,18 @@ internal class ChallengeBasedAuthenticationPolicy : BearerTokenAuthenticationPol private const string KeyVaultStashedContentKey = "KeyVaultContent"; private readonly bool _verifyChallengeResource; + private readonly bool _enableCAE; + /// /// Challenges are cached using the Key Vault or Managed HSM endpoint URI authority as the key. /// private static readonly ConcurrentDictionary s_challengeCache = new(); private ChallengeParameters _challenge; - public ChallengeBasedAuthenticationPolicy(TokenCredential credential, bool disableChallengeResourceVerification) : base(credential, Array.Empty()) + public ChallengeBasedAuthenticationPolicy(TokenCredential credential, bool disableChallengeResourceVerification, bool enableCAE = false) : base(credential, Array.Empty()) { _verifyChallengeResource = !disableChallengeResourceVerification; + _enableCAE = enableCAE; } /// @@ -51,7 +54,7 @@ private async ValueTask AuthorizeRequestInternal(HttpMessage message, bool async if (_challenge != null) { // We fetched the challenge from the cache, but we have not initialized the Scopes in the base yet. - var context = new TokenRequestContext(_challenge.Scopes, parentRequestId: message.Request.ClientRequestId, tenantId: _challenge.TenantId); + var context = new TokenRequestContext(_challenge.Scopes, parentRequestId: message.Request.ClientRequestId, tenantId: _challenge.TenantId, claims: _challenge.Claims, isCaeEnabled: _enableCAE); if (async) { await AuthenticateAndAuthorizeRequestAsync(message, context).ConfigureAwait(false); @@ -155,11 +158,12 @@ private async ValueTask AuthorizeRequestOnChallengeAsyncInternal(HttpMessa internal class ChallengeParameters { - internal ChallengeParameters(Uri authorizationUri, string[] scopes) + internal ChallengeParameters(Uri authorizationUri, string[] scopes, string claims = null) { AuthorizationUri = authorizationUri; TenantId = authorizationUri.Segments[1].Trim('/'); Scopes = scopes; + Claims = claims; } /// @@ -176,6 +180,11 @@ internal ChallengeParameters(Uri authorizationUri, string[] scopes) /// Gets the tenant ID from . /// public string TenantId { get; } + + /// + /// Gets the "claims" parameter from the challenge response if Continuous Access Evaluation is enabled. + /// + public string Claims { get; set; } } internal static void ClearCache() From 36643e2d2f12d3323b50f9ee1aec0d315637954e Mon Sep 17 00:00:00 2001 From: Jonathan Cardenas Date: Tue, 17 Sep 2024 12:23:13 -0700 Subject: [PATCH 02/27] Enable CAE for AuthorizeRequestOnChallenge --- .../src/ChallengeBasedAuthenticationPolicy.cs | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs b/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs index 61a5d1075ee4..fea23686cb98 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs @@ -15,7 +15,7 @@ internal class ChallengeBasedAuthenticationPolicy : BearerTokenAuthenticationPol private const string KeyVaultStashedContentKey = "KeyVaultContent"; private readonly bool _verifyChallengeResource; - private readonly bool _enableCAE; + private readonly bool _enableCAE = false; /// /// Challenges are cached using the Key Vault or Managed HSM endpoint URI authority as the key. @@ -96,6 +96,21 @@ private async ValueTask AuthorizeRequestOnChallengeAsyncInternal(HttpMessa string authority = GetRequestAuthority(message.Request); string scope = AuthorizationChallengeParser.GetChallengeParameterFromResponse(message.Response, "Bearer", "resource"); + + string error = AuthorizationChallengeParser.GetChallengeParameterFromResponse(message.Response, "Bearer", "error"); + string claims = null; + + if (error != null) + { + // The challenge response contained an error. + string base64Claims = AuthorizationChallengeParser.GetChallengeParameterFromResponse(message.Response, "Bearer", "claims"); + + if (error == "insufficient_claims" && base64Claims != null) + { + claims = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(base64Claims)); + } + } + if (scope != null) { scope += "/.default"; @@ -111,6 +126,11 @@ private async ValueTask AuthorizeRequestOnChallengeAsyncInternal(HttpMessa { return false; } + else if (claims is not null) + { + _challenge.Claims = claims; + s_challengeCache[authority] = _challenge; + } } else { @@ -139,11 +159,11 @@ private async ValueTask AuthorizeRequestOnChallengeAsyncInternal(HttpMessa throw new UriFormatException($"The challenge authorization URI '{authorization}' is invalid."); } - _challenge = new ChallengeParameters(authorizationUri, new string[] { scope }); + _challenge = new ChallengeParameters(authorizationUri, new string[] { scope }, claims); s_challengeCache[authority] = _challenge; } - var context = new TokenRequestContext(_challenge.Scopes, parentRequestId: message.Request.ClientRequestId, tenantId: _challenge.TenantId); + var context = new TokenRequestContext(_challenge.Scopes, parentRequestId: message.Request.ClientRequestId, tenantId: _challenge.TenantId, isCaeEnabled: _enableCAE, claims: _challenge.Claims); if (async) { await AuthenticateAndAuthorizeRequestAsync(message, context).ConfigureAwait(false); From 02a805da8c3c4e48f2e863e1e455a96d9076dd7c Mon Sep 17 00:00:00 2001 From: Jonathan Cardenas Date: Tue, 17 Sep 2024 12:23:58 -0700 Subject: [PATCH 03/27] Add flag in SecretClientOption and SecretClient --- .../Azure.Security.KeyVault.Secrets/src/SecretClient.cs | 2 +- .../src/SecretClientOptions.cs | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets/src/SecretClient.cs b/sdk/keyvault/Azure.Security.KeyVault.Secrets/src/SecretClient.cs index 43c8dd5e030e..84c895b47534 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Secrets/src/SecretClient.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets/src/SecretClient.cs @@ -65,7 +65,7 @@ public SecretClient(Uri vaultUri, TokenCredential credential, SecretClientOption string apiVersion = options.GetVersionString(); HttpPipeline pipeline = HttpPipelineBuilder.Build(options, - new ChallengeBasedAuthenticationPolicy(credential, options.DisableChallengeResourceVerification)); + new ChallengeBasedAuthenticationPolicy(credential, options.DisableChallengeResourceVerification, options.enableCAE)); _pipeline = new KeyVaultPipeline(vaultUri, apiVersion, pipeline, new ClientDiagnostics(options)); } diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets/src/SecretClientOptions.cs b/sdk/keyvault/Azure.Security.KeyVault.Secrets/src/SecretClientOptions.cs index 66dc3d1b6072..814524921a29 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Secrets/src/SecretClientOptions.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets/src/SecretClientOptions.cs @@ -87,6 +87,11 @@ public SecretClientOptions(ServiceVersion version = LatestVersion) /// public bool DisableChallengeResourceVerification { get; set; } + /// + /// Enable Continuous Access Evaluation (CAE) for the client. + /// + public bool enableCAE { get; set; } = false; + internal string GetVersionString() { return Version switch From 8bd442a614f936fa47e3ec6f046a22c1d4d5c1c9 Mon Sep 17 00:00:00 2001 From: Jonathan Cardenas Date: Tue, 17 Sep 2024 12:31:01 -0700 Subject: [PATCH 04/27] Revert "Add flag in SecretClientOption and SecretClient" This reverts commit 02a805da8c3c4e48f2e863e1e455a96d9076dd7c. --- .../Azure.Security.KeyVault.Secrets/src/SecretClient.cs | 2 +- .../src/SecretClientOptions.cs | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets/src/SecretClient.cs b/sdk/keyvault/Azure.Security.KeyVault.Secrets/src/SecretClient.cs index 84c895b47534..43c8dd5e030e 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Secrets/src/SecretClient.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets/src/SecretClient.cs @@ -65,7 +65,7 @@ public SecretClient(Uri vaultUri, TokenCredential credential, SecretClientOption string apiVersion = options.GetVersionString(); HttpPipeline pipeline = HttpPipelineBuilder.Build(options, - new ChallengeBasedAuthenticationPolicy(credential, options.DisableChallengeResourceVerification, options.enableCAE)); + new ChallengeBasedAuthenticationPolicy(credential, options.DisableChallengeResourceVerification)); _pipeline = new KeyVaultPipeline(vaultUri, apiVersion, pipeline, new ClientDiagnostics(options)); } diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets/src/SecretClientOptions.cs b/sdk/keyvault/Azure.Security.KeyVault.Secrets/src/SecretClientOptions.cs index 814524921a29..66dc3d1b6072 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Secrets/src/SecretClientOptions.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets/src/SecretClientOptions.cs @@ -87,11 +87,6 @@ public SecretClientOptions(ServiceVersion version = LatestVersion) /// public bool DisableChallengeResourceVerification { get; set; } - /// - /// Enable Continuous Access Evaluation (CAE) for the client. - /// - public bool enableCAE { get; set; } = false; - internal string GetVersionString() { return Version switch From c6a65dac9618ee7462e1d7fd45ae7bfdf7e74aa7 Mon Sep 17 00:00:00 2001 From: Jonathan Cardenas Date: Tue, 17 Sep 2024 12:32:00 -0700 Subject: [PATCH 05/27] Enable CAE by default --- .../src/ChallengeBasedAuthenticationPolicy.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs b/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs index fea23686cb98..12793a5255d4 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs @@ -15,8 +15,6 @@ internal class ChallengeBasedAuthenticationPolicy : BearerTokenAuthenticationPol private const string KeyVaultStashedContentKey = "KeyVaultContent"; private readonly bool _verifyChallengeResource; - private readonly bool _enableCAE = false; - /// /// Challenges are cached using the Key Vault or Managed HSM endpoint URI authority as the key. /// @@ -26,7 +24,6 @@ internal class ChallengeBasedAuthenticationPolicy : BearerTokenAuthenticationPol public ChallengeBasedAuthenticationPolicy(TokenCredential credential, bool disableChallengeResourceVerification, bool enableCAE = false) : base(credential, Array.Empty()) { _verifyChallengeResource = !disableChallengeResourceVerification; - _enableCAE = enableCAE; } /// @@ -54,7 +51,7 @@ private async ValueTask AuthorizeRequestInternal(HttpMessage message, bool async if (_challenge != null) { // We fetched the challenge from the cache, but we have not initialized the Scopes in the base yet. - var context = new TokenRequestContext(_challenge.Scopes, parentRequestId: message.Request.ClientRequestId, tenantId: _challenge.TenantId, claims: _challenge.Claims, isCaeEnabled: _enableCAE); + var context = new TokenRequestContext(_challenge.Scopes, parentRequestId: message.Request.ClientRequestId, tenantId: _challenge.TenantId, claims: _challenge.Claims, isCaeEnabled: true); if (async) { await AuthenticateAndAuthorizeRequestAsync(message, context).ConfigureAwait(false); @@ -163,7 +160,7 @@ private async ValueTask AuthorizeRequestOnChallengeAsyncInternal(HttpMessa s_challengeCache[authority] = _challenge; } - var context = new TokenRequestContext(_challenge.Scopes, parentRequestId: message.Request.ClientRequestId, tenantId: _challenge.TenantId, isCaeEnabled: _enableCAE, claims: _challenge.Claims); + var context = new TokenRequestContext(_challenge.Scopes, parentRequestId: message.Request.ClientRequestId, tenantId: _challenge.TenantId, isCaeEnabled: true, claims: _challenge.Claims); if (async) { await AuthenticateAndAuthorizeRequestAsync(message, context).ConfigureAwait(false); From 8b4e44cc60cbfef13f7e695a4f878efec2b533aa Mon Sep 17 00:00:00 2001 From: Jonathan Cardenas Date: Tue, 17 Sep 2024 14:49:45 -0700 Subject: [PATCH 06/27] Removing unused parameter --- .../src/ChallengeBasedAuthenticationPolicy.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs b/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs index 12793a5255d4..d24c6e4b44f7 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs @@ -21,7 +21,7 @@ internal class ChallengeBasedAuthenticationPolicy : BearerTokenAuthenticationPol private static readonly ConcurrentDictionary s_challengeCache = new(); private ChallengeParameters _challenge; - public ChallengeBasedAuthenticationPolicy(TokenCredential credential, bool disableChallengeResourceVerification, bool enableCAE = false) : base(credential, Array.Empty()) + public ChallengeBasedAuthenticationPolicy(TokenCredential credential, bool disableChallengeResourceVerification) : base(credential, Array.Empty()) { _verifyChallengeResource = !disableChallengeResourceVerification; } From a8772d510ca77cd87af0be0a347a4d9d08cb2c1d Mon Sep 17 00:00:00 2001 From: Jonathan Cardenas Date: Tue, 17 Sep 2024 14:50:13 -0700 Subject: [PATCH 07/27] Remove saving the claims in the cache --- .../src/ChallengeBasedAuthenticationPolicy.cs | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs b/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs index d24c6e4b44f7..2adb914307b0 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs @@ -51,7 +51,7 @@ private async ValueTask AuthorizeRequestInternal(HttpMessage message, bool async if (_challenge != null) { // We fetched the challenge from the cache, but we have not initialized the Scopes in the base yet. - var context = new TokenRequestContext(_challenge.Scopes, parentRequestId: message.Request.ClientRequestId, tenantId: _challenge.TenantId, claims: _challenge.Claims, isCaeEnabled: true); + var context = new TokenRequestContext(_challenge.Scopes, parentRequestId: message.Request.ClientRequestId, tenantId: _challenge.TenantId, isCaeEnabled: true); if (async) { await AuthenticateAndAuthorizeRequestAsync(message, context).ConfigureAwait(false); @@ -123,11 +123,6 @@ private async ValueTask AuthorizeRequestOnChallengeAsyncInternal(HttpMessa { return false; } - else if (claims is not null) - { - _challenge.Claims = claims; - s_challengeCache[authority] = _challenge; - } } else { @@ -156,11 +151,11 @@ private async ValueTask AuthorizeRequestOnChallengeAsyncInternal(HttpMessa throw new UriFormatException($"The challenge authorization URI '{authorization}' is invalid."); } - _challenge = new ChallengeParameters(authorizationUri, new string[] { scope }, claims); + _challenge = new ChallengeParameters(authorizationUri, new string[] { scope }); s_challengeCache[authority] = _challenge; } - var context = new TokenRequestContext(_challenge.Scopes, parentRequestId: message.Request.ClientRequestId, tenantId: _challenge.TenantId, isCaeEnabled: true, claims: _challenge.Claims); + var context = new TokenRequestContext(_challenge.Scopes, parentRequestId: message.Request.ClientRequestId, tenantId: _challenge.TenantId, isCaeEnabled: true, claims: claims); if (async) { await AuthenticateAndAuthorizeRequestAsync(message, context).ConfigureAwait(false); @@ -175,12 +170,11 @@ private async ValueTask AuthorizeRequestOnChallengeAsyncInternal(HttpMessa internal class ChallengeParameters { - internal ChallengeParameters(Uri authorizationUri, string[] scopes, string claims = null) + internal ChallengeParameters(Uri authorizationUri, string[] scopes) { AuthorizationUri = authorizationUri; TenantId = authorizationUri.Segments[1].Trim('/'); Scopes = scopes; - Claims = claims; } /// @@ -197,11 +191,6 @@ internal ChallengeParameters(Uri authorizationUri, string[] scopes, string claim /// Gets the tenant ID from . /// public string TenantId { get; } - - /// - /// Gets the "claims" parameter from the challenge response if Continuous Access Evaluation is enabled. - /// - public string Claims { get; set; } } internal static void ClearCache() From ee0b2c68fc1f2cbc9fd78dde527278712b4d3c5e Mon Sep 17 00:00:00 2001 From: Jonathan Cardenas Date: Tue, 17 Sep 2024 14:53:52 -0700 Subject: [PATCH 08/27] Update Changelog --- sdk/keyvault/Azure.Security.KeyVault.Administration/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/keyvault/Azure.Security.KeyVault.Administration/CHANGELOG.md b/sdk/keyvault/Azure.Security.KeyVault.Administration/CHANGELOG.md index 4b08ca6035e4..ce236ca692c9 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Administration/CHANGELOG.md +++ b/sdk/keyvault/Azure.Security.KeyVault.Administration/CHANGELOG.md @@ -6,6 +6,7 @@ - Added support for service API version `7.6-preview.1`. - Added new methods `StartPreRestoreAsync`, `StartPreRestore`, `StartPreBackupAsync`, and `StartPreBackupAsync` to the `KeyVaultBackupClient`. +- Added support for Continuous Access Evaluation (CAE). ### Breaking Changes From f645368d144e666bd08680c367edea6f3e031b03 Mon Sep 17 00:00:00 2001 From: Jonathan Cardenas Date: Wed, 18 Sep 2024 13:27:34 -0700 Subject: [PATCH 09/27] Update changelogs --- .../Azure.Security.KeyVault.Administration/CHANGELOG.md | 2 +- sdk/keyvault/Azure.Security.KeyVault.Certificates/CHANGELOG.md | 1 + sdk/keyvault/Azure.Security.KeyVault.Keys/CHANGELOG.md | 1 + sdk/keyvault/Azure.Security.KeyVault.Secrets/CHANGELOG.md | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/sdk/keyvault/Azure.Security.KeyVault.Administration/CHANGELOG.md b/sdk/keyvault/Azure.Security.KeyVault.Administration/CHANGELOG.md index ce236ca692c9..b917429847d2 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Administration/CHANGELOG.md +++ b/sdk/keyvault/Azure.Security.KeyVault.Administration/CHANGELOG.md @@ -6,7 +6,7 @@ - Added support for service API version `7.6-preview.1`. - Added new methods `StartPreRestoreAsync`, `StartPreRestore`, `StartPreBackupAsync`, and `StartPreBackupAsync` to the `KeyVaultBackupClient`. -- Added support for Continuous Access Evaluation (CAE). +- Support for Continuous Access Evaluation (CAE). ### Breaking Changes diff --git a/sdk/keyvault/Azure.Security.KeyVault.Certificates/CHANGELOG.md b/sdk/keyvault/Azure.Security.KeyVault.Certificates/CHANGELOG.md index 917ca4d22e1d..35ad51f55b5a 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Certificates/CHANGELOG.md +++ b/sdk/keyvault/Azure.Security.KeyVault.Certificates/CHANGELOG.md @@ -3,6 +3,7 @@ ## 4.7.0-beta.1 (Unreleased) ### Features Added +- Support for Continuous Access Evaluation (CAE). ### Breaking Changes diff --git a/sdk/keyvault/Azure.Security.KeyVault.Keys/CHANGELOG.md b/sdk/keyvault/Azure.Security.KeyVault.Keys/CHANGELOG.md index 1734a9546135..1b20dcc967a6 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Keys/CHANGELOG.md +++ b/sdk/keyvault/Azure.Security.KeyVault.Keys/CHANGELOG.md @@ -3,6 +3,7 @@ ## 4.7.0-beta.1 (Unreleased) ### Features Added +- Support for Continuous Access Evaluation (CAE). ### Breaking Changes diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets/CHANGELOG.md b/sdk/keyvault/Azure.Security.KeyVault.Secrets/CHANGELOG.md index 6e2354363b26..07b929203c42 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Secrets/CHANGELOG.md +++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets/CHANGELOG.md @@ -3,6 +3,7 @@ ## 4.7.0-beta.1 (Unreleased) ### Features Added +- Support for Continuous Access Evaluation (CAE). ### Breaking Changes From 1a744906b70a49ae14bb4de48e90a393fd7c9e79 Mon Sep 17 00:00:00 2001 From: Jonathan Cardenas Date: Wed, 18 Sep 2024 14:36:02 -0700 Subject: [PATCH 10/27] Simplify error checking logic Addressing comment in PR --- .../src/ChallengeBasedAuthenticationPolicy.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs b/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs index 2adb914307b0..e3e328837b30 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs @@ -97,15 +97,13 @@ private async ValueTask AuthorizeRequestOnChallengeAsyncInternal(HttpMessa string error = AuthorizationChallengeParser.GetChallengeParameterFromResponse(message.Response, "Bearer", "error"); string claims = null; - if (error != null) + if (error == "insufficient_claims") { - // The challenge response contained an error. - string base64Claims = AuthorizationChallengeParser.GetChallengeParameterFromResponse(message.Response, "Bearer", "claims"); - - if (error == "insufficient_claims" && base64Claims != null) + claims = AuthorizationChallengeParser.GetChallengeParameterFromResponse(message.Response, "Bearer", "claims") switch { - claims = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(base64Claims)); - } + { Length: 0 } => null, + string enc => System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(enc)) + }; } if (scope != null) From 5df40020aca923f4e8255fb55358ba1df0e6abdb Mon Sep 17 00:00:00 2001 From: Jonathan Cardenas Date: Mon, 23 Sep 2024 14:01:49 -0700 Subject: [PATCH 11/27] Add test for base64 claims --- ...ChallengeBasedAuthenticationPolicyTests.cs | 12 +++++++ .../src/ChallengeBasedAuthenticationPolicy.cs | 33 ++++++++++++++----- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs b/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs index 50c53bc6aed8..077e811e2b76 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs @@ -199,6 +199,18 @@ public async Task ReauthenticatesWhenTenantChanged() Assert.AreEqual("secret-value", response.Value.Value); } + [Test] + public void GetClaims() + { + MockResponse response401WithClaims = new MockResponse(401) + .WithHeader("WWW-Authenticate", @"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsiYWNycyI6eyJlc3NlbnRpYWwiOnRydWUsInZhbHVlIjoiY3AxIn19fQ=="""); + Assert.AreEqual(ChallengeBasedAuthenticationPolicy.getDecodedClaimsParameter("insufficient_claims", response401WithClaims), @"{""access_token"":{""acrs"":{""essential"":true,""value"":""cp1""}}}"); + + MockResponse response401 = new MockResponse(401) + .WithHeader("WWW-Authenticate", @"Bearer authorization=""https://login.windows.net/de763a21-49f7-4b08-a8e1-52c8fbc103b4"", resource=""https://vault.azure.net"""); + Assert.IsNull(ChallengeBasedAuthenticationPolicy.getDecodedClaimsParameter(null, response401)); + } + private class MockTransportBuilder { private const string AuthorizationHeader = "Authorization"; diff --git a/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs b/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs index e3e328837b30..cf7e914f7b5d 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs @@ -84,6 +84,29 @@ protected override ValueTask AuthorizeRequestOnChallengeAsync(HttpMessage protected override bool AuthorizeRequestOnChallenge(HttpMessage message) => AuthorizeRequestOnChallengeAsyncInternal(message, false).EnsureCompleted(); + /// + /// Gets the claims parameter from the challenge response. + /// If there are no claims, returns null. + /// + /// The error message from the service. + /// The response from the service which contains the headers. + /// A string with the decoded claims if present, otherwise null + internal static string getDecodedClaimsParameter(string error, Response response) + { + // According to docs https://learn.microsoft.com/en-us/entra/identity-platform/claims-challenge?tabs=dotnet#claims-challenge-header-format, + // the error message must be "insufficient_claims" when a claims challenge should be generated. + if (error == "insufficient_claims") + { + return AuthorizationChallengeParser.GetChallengeParameterFromResponse(response, "Bearer", "claims") switch + { + { Length: 0 } => null, + string enc => System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(enc)) + }; + } + + return null; + } + private async ValueTask AuthorizeRequestOnChallengeAsyncInternal(HttpMessage message, bool async) { if (message.Request.Content == null && message.TryGetProperty(KeyVaultStashedContentKey, out var content)) @@ -95,16 +118,8 @@ private async ValueTask AuthorizeRequestOnChallengeAsyncInternal(HttpMessa string scope = AuthorizationChallengeParser.GetChallengeParameterFromResponse(message.Response, "Bearer", "resource"); string error = AuthorizationChallengeParser.GetChallengeParameterFromResponse(message.Response, "Bearer", "error"); - string claims = null; - if (error == "insufficient_claims") - { - claims = AuthorizationChallengeParser.GetChallengeParameterFromResponse(message.Response, "Bearer", "claims") switch - { - { Length: 0 } => null, - string enc => System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(enc)) - }; - } + string claims = getDecodedClaimsParameter(error, message.Response); if (scope != null) { From 24513969de5ad1966526709afb784f8bc5be205c Mon Sep 17 00:00:00 2001 From: Jonathan Cardenas Date: Wed, 25 Sep 2024 13:51:13 -0700 Subject: [PATCH 12/27] Override Process function to handle the first CAE Challenge after a scope challenge --- .../src/ChallengeBasedAuthenticationPolicy.cs | 90 ++++++++++++++++++- 1 file changed, 86 insertions(+), 4 deletions(-) diff --git a/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs b/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs index cf7e914f7b5d..2f52721582b5 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Concurrent; using System.Globalization; +using System.Net; using System.Threading.Tasks; namespace Azure.Security.KeyVault @@ -114,13 +115,10 @@ private async ValueTask AuthorizeRequestOnChallengeAsyncInternal(HttpMessa message.Request.Content = content as RequestContent; } + string error = AuthorizationChallengeParser.GetChallengeParameterFromResponse(message.Response, "Bearer", "error"); string authority = GetRequestAuthority(message.Request); string scope = AuthorizationChallengeParser.GetChallengeParameterFromResponse(message.Response, "Bearer", "resource"); - string error = AuthorizationChallengeParser.GetChallengeParameterFromResponse(message.Response, "Bearer", "error"); - - string claims = getDecodedClaimsParameter(error, message.Response); - if (scope != null) { scope += "/.default"; @@ -130,6 +128,15 @@ private async ValueTask AuthorizeRequestOnChallengeAsyncInternal(HttpMessa scope = AuthorizationChallengeParser.GetChallengeParameterFromResponse(message.Response, "Bearer", "scope"); } + // Handle CAE Challenges + string claims = getDecodedClaimsParameter(error, message.Response); + if (claims != null) + { + // Get the scope from the cache + s_challengeCache.TryGetValue(authority, out _challenge); + scope = _challenge.Scopes[0]; + } + if (scope is null) { if (s_challengeCache.TryGetValue(authority, out _challenge)) @@ -181,6 +188,81 @@ private async ValueTask AuthorizeRequestOnChallengeAsyncInternal(HttpMessa return true; } + /// + public override ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline) + { + return ProcessAsyncInternal(message, pipeline, true); + } + + /// + public override void Process(HttpMessage message, ReadOnlyMemory pipeline) + { + ProcessAsyncInternal(message, pipeline, false).EnsureCompleted(); + } + + private async ValueTask ProcessAsyncInternal(HttpMessage message, ReadOnlyMemory pipeline, bool async) + { + if (message.Request.Uri.Scheme != Uri.UriSchemeHttps) + { + throw new InvalidOperationException("Bearer token authentication is not permitted for non TLS protected (https) endpoints."); + } + + if (async) + { + await AuthorizeRequestAsync(message).ConfigureAwait(false); + await ProcessNextAsync(message, pipeline).ConfigureAwait(false); + } + else + { + AuthorizeRequest(message); + ProcessNext(message, pipeline); + } + + // Check if we have received a challenge or we have not yet issued the first request. + if (message.Response.Status == (int)HttpStatusCode.Unauthorized && message.Response.Headers.Contains(HttpHeader.Names.WwwAuthenticate)) + { + // Attempt to get the TokenRequestContext based on the challenge. + // If we fail to get the context, the challenge was not present or invalid. + // If we succeed in getting the context, authenticate the request and pass it up the policy chain. + if (async) + { + if (await AuthorizeRequestOnChallengeAsync(message).ConfigureAwait(false)) + { + await ProcessNextAsync(message, pipeline).ConfigureAwait(false); + } + } + else + { + if (AuthorizeRequestOnChallenge(message)) + { + ProcessNext(message, pipeline); + } + } + } + + // Handle the scenario in which we get a CAE challenge back. + if (message.Response.Status == (int)HttpStatusCode.Unauthorized + && message.Response.Headers.Contains(HttpHeader.Names.WwwAuthenticate) + && AuthorizationChallengeParser.GetChallengeParameterFromResponse(message.Response, "Bearer", "claims") != null) + { + if (async) + { + if (await AuthorizeRequestOnChallengeAsync(message).ConfigureAwait(false)) + { + await ProcessNextAsync(message, pipeline).ConfigureAwait(false); + } + } + else + { + if (AuthorizeRequestOnChallenge(message)) + { + ProcessNext(message, pipeline); + } + } + } + // If we get a second CAE challenge, an unlikely scenario, we do not attempt to re-authenticate. + } + internal class ChallengeParameters { internal ChallengeParameters(Uri authorizationUri, string[] scopes) From 1f9a73c1eeec040c8c5c4f22a8391c9645e4b487 Mon Sep 17 00:00:00 2001 From: Jonathan Cardenas Date: Wed, 25 Sep 2024 13:51:26 -0700 Subject: [PATCH 13/27] Add tests --- ...ChallengeBasedAuthenticationPolicyTests.cs | 117 +++++++++++++++++- 1 file changed, 116 insertions(+), 1 deletion(-) diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs b/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs index 077e811e2b76..ac9437fb4cc9 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs @@ -200,7 +200,7 @@ public async Task ReauthenticatesWhenTenantChanged() } [Test] - public void GetClaims() + public void GetClaimsFromChallengeHeaders() { MockResponse response401WithClaims = new MockResponse(401) .WithHeader("WWW-Authenticate", @"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsiYWNycyI6eyJlc3NlbnRpYWwiOnRydWUsInZhbHVlIjoiY3AxIn19fQ=="""); @@ -211,6 +211,121 @@ public void GetClaims() Assert.IsNull(ChallengeBasedAuthenticationPolicy.getDecodedClaimsParameter(null, response401)); } + [Test] + public void HandlesCaeChallenges(){ + MockTransport transport = new(new[] + { + // Initial scope challlenge + new MockResponse(401) + .WithHeader("WWW-Authenticate", @"Bearer authorization=""https://login.windows.net/de763a21-49f7-4b08-a8e1-52c8fbc103b4"", resource=""https://vault.azure.net"""), + + new MockResponse(200) + .WithJson(""" + { + "token_type": "Bearer", + "expires_in": 3599, + "resource": "https://vault.azure.net", + "access_token": "ZGU3NjNhMjEtNDlmNy00YjA4LWE4ZTEtNTJjOGZiYzEwM2I0" + } + """), + + // CAE Challenge + new MockResponse(401) + .WithHeader("WWW-Authenticate", @"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ=="""), + + new MockResponse(200) + .WithJson(""" + { + "token_type": "Bearer", + "expires_in": 3599, + "resource": "https://vault.azure.net", + "access_token": "NzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3" + } + """), + + new MockResponse(200) + { + ContentStream = new KeyVaultSecret("test-secret", "secret-value").ToStream(), + }, + }); + + SecretClient client = new( + VaultUri, + new MockCredential(transport), + new SecretClientOptions() + { + Transport = transport, + }); + + Response response = client.GetSecret("test-secret"); + Assert.AreEqual(200, response.GetRawResponse().Status); + Assert.AreEqual("secret-value", response.Value.Value); + } + + [Test] + public void ThrowsWithTwoConsecutiveCaeChallenges() + { + MockTransport transport = new(new[] + { + // Initial scope challlenge + new MockResponse(401) + .WithHeader("WWW-Authenticate", @"Bearer authorization=""https://login.windows.net/de763a21-49f7-4b08-a8e1-52c8fbc103b4"", resource=""https://vault.azure.net"""), + + new MockResponse(200) + .WithJson(""" + { + "token_type": "Bearer", + "expires_in": 3599, + "resource": "https://vault.azure.net", + "access_token": "ZGU3NjNhMjEtNDlmNy00YjA4LWE4ZTEtNTJjOGZiYzEwM2I0" + } + """), + + // CAE Challenge + new MockResponse(401) + .WithHeader("WWW-Authenticate", @"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ=="""), + + new MockResponse(200) + .WithJson(""" + { + "token_type": "Bearer", + "expires_in": 3599, + "resource": "https://vault.azure.net", + "access_token": "NzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3" + } + """), + + // Second CAE Challenge + new MockResponse(401) + .WithHeader("WWW-Authenticate", @"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ=="""), + + new MockResponse(200) + .WithJson(""" + { + "token_type": "Bearer", + "expires_in": 3599, + "resource": "https://vault.azure.net", + "access_token": GUID.NewGuid().ToString() + } + """), + + new MockResponse(200) + { + ContentStream = new KeyVaultSecret("test-secret", "secret-value").ToStream(), + }, + }); + + SecretClient client = new( + VaultUri, + new MockCredential(transport), + new SecretClientOptions() + { + Transport = transport, + }); + + Assert.Throws(() => client.GetSecret("test-secret")); + } + private class MockTransportBuilder { private const string AuthorizationHeader = "Authorization"; From daa03efe9b8233bf2d605e62da57cffce1d7fb32 Mon Sep 17 00:00:00 2001 From: Jonathan Cardenas Date: Wed, 25 Sep 2024 16:39:06 -0700 Subject: [PATCH 14/27] Separate credential and client transports and assert for a 401. --- ...ChallengeBasedAuthenticationPolicyTests.cs | 75 ++++++++++++------- 1 file changed, 46 insertions(+), 29 deletions(-) diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs b/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs index ac9437fb4cc9..38494211309c 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs @@ -213,12 +213,24 @@ public void GetClaimsFromChallengeHeaders() [Test] public void HandlesCaeChallenges(){ - MockTransport transport = new(new[] + MockTransport keyVaultTransport = new(new[] { // Initial scope challlenge new MockResponse(401) .WithHeader("WWW-Authenticate", @"Bearer authorization=""https://login.windows.net/de763a21-49f7-4b08-a8e1-52c8fbc103b4"", resource=""https://vault.azure.net"""), + // CAE Challenge + new MockResponse(401) + .WithHeader("WWW-Authenticate", @"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ=="""), + + new MockResponse(200) + { + ContentStream = new KeyVaultSecret("test-secret", "secret-value").ToStream(), + }, + }); + + MockTransport credentialTransport = new(new[] + { new MockResponse(200) .WithJson(""" { @@ -229,10 +241,6 @@ public void HandlesCaeChallenges(){ } """), - // CAE Challenge - new MockResponse(401) - .WithHeader("WWW-Authenticate", @"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ=="""), - new MockResponse(200) .WithJson(""" { @@ -242,19 +250,14 @@ public void HandlesCaeChallenges(){ "access_token": "NzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3" } """), - - new MockResponse(200) - { - ContentStream = new KeyVaultSecret("test-secret", "secret-value").ToStream(), - }, }); SecretClient client = new( VaultUri, - new MockCredential(transport), + new MockCredential(credentialTransport), new SecretClientOptions() { - Transport = transport, + Transport = keyVaultTransport, }); Response response = client.GetSecret("test-secret"); @@ -265,12 +268,28 @@ public void HandlesCaeChallenges(){ [Test] public void ThrowsWithTwoConsecutiveCaeChallenges() { - MockTransport transport = new(new[] + MockTransport keyVaultTransport = new(new[] { // Initial scope challlenge new MockResponse(401) .WithHeader("WWW-Authenticate", @"Bearer authorization=""https://login.windows.net/de763a21-49f7-4b08-a8e1-52c8fbc103b4"", resource=""https://vault.azure.net"""), + // CAE Challenge + new MockResponse(401) + .WithHeader("WWW-Authenticate", @"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ=="""), + + // Second CAE Challenge + new MockResponse(401) + .WithHeader("WWW-Authenticate", @"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ=="""), + + new MockResponse(200) + { + ContentStream = new KeyVaultSecret("test-secret", "secret-value").ToStream(), + }, + }); + + MockTransport credentialTransport = new(new[] + { new MockResponse(200) .WithJson(""" { @@ -281,10 +300,6 @@ public void ThrowsWithTwoConsecutiveCaeChallenges() } """), - // CAE Challenge - new MockResponse(401) - .WithHeader("WWW-Authenticate", @"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ=="""), - new MockResponse(200) .WithJson(""" { @@ -295,10 +310,6 @@ public void ThrowsWithTwoConsecutiveCaeChallenges() } """), - // Second CAE Challenge - new MockResponse(401) - .WithHeader("WWW-Authenticate", @"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ=="""), - new MockResponse(200) .WithJson(""" { @@ -308,22 +319,28 @@ public void ThrowsWithTwoConsecutiveCaeChallenges() "access_token": GUID.NewGuid().ToString() } """), - - new MockResponse(200) - { - ContentStream = new KeyVaultSecret("test-secret", "secret-value").ToStream(), - }, }); SecretClient client = new( VaultUri, - new MockCredential(transport), + new MockCredential(credentialTransport), new SecretClientOptions() { - Transport = transport, + Transport = keyVaultTransport, }); - Assert.Throws(() => client.GetSecret("test-secret")); + try + { + client.GetSecret("test-secret"); + } + catch (RequestFailedException ex) + { + Assert.AreEqual(401, ex.Status); + } + catch (Exception ex) + { + Assert.Fail($"Expected RequestFailedException, but got {ex.GetType()}"); + } } private class MockTransportBuilder From f454e4d58700cfbb18f56f15db5637ad1b62a5f8 Mon Sep 17 00:00:00 2001 From: Jonathan Cardenas Date: Fri, 27 Sep 2024 14:57:39 -0700 Subject: [PATCH 15/27] Nest rety inside challenge if block --- .../src/ChallengeBasedAuthenticationPolicy.cs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs b/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs index 2f52721582b5..0c52c7ba688f 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Shared/src/ChallengeBasedAuthenticationPolicy.cs @@ -238,29 +238,29 @@ private async ValueTask ProcessAsyncInternal(HttpMessage message, ReadOnlyMemory ProcessNext(message, pipeline); } } - } - // Handle the scenario in which we get a CAE challenge back. - if (message.Response.Status == (int)HttpStatusCode.Unauthorized - && message.Response.Headers.Contains(HttpHeader.Names.WwwAuthenticate) - && AuthorizationChallengeParser.GetChallengeParameterFromResponse(message.Response, "Bearer", "claims") != null) - { - if (async) + // Handle the scenario in which we get a CAE challenge back. + if (message.Response.Status == (int)HttpStatusCode.Unauthorized + && message.Response.Headers.Contains(HttpHeader.Names.WwwAuthenticate) + && AuthorizationChallengeParser.GetChallengeParameterFromResponse(message.Response, "Bearer", "claims") != null) { - if (await AuthorizeRequestOnChallengeAsync(message).ConfigureAwait(false)) + if (async) { - await ProcessNextAsync(message, pipeline).ConfigureAwait(false); + if (await AuthorizeRequestOnChallengeAsync(message).ConfigureAwait(false)) + { + await ProcessNextAsync(message, pipeline).ConfigureAwait(false); + } } - } - else - { - if (AuthorizeRequestOnChallenge(message)) + else { - ProcessNext(message, pipeline); + if (AuthorizeRequestOnChallenge(message)) + { + ProcessNext(message, pipeline); + } } } + // If we get a second CAE challenge, an unlikely scenario, we do not attempt to re-authenticate. } - // If we get a second CAE challenge, an unlikely scenario, we do not attempt to re-authenticate. } internal class ChallengeParameters From de6d54d38ff9970331b5cd0018d7d66f12fa6af2 Mon Sep 17 00:00:00 2001 From: Jonathan Cardenas Date: Mon, 30 Sep 2024 18:44:35 -0700 Subject: [PATCH 16/27] Add test for claims in token --- ...ChallengeBasedAuthenticationPolicyTests.cs | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs b/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs index 38494211309c..e98bfa016083 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs @@ -343,6 +343,77 @@ public void ThrowsWithTwoConsecutiveCaeChallenges() } } + [Test] + [TestCase("standard", """Bearer realm="", authorization_uri="https://login.microsoftonline.com/common/oauth2/authorize", error="insufficient_claims", claims="eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ==" """, """{"access_token":{"nbf":{"essential":true,"value":"1726077595"},"xms_caeerror":{"value":"10012"}}}""")] + public async Task VerifyClaimsInToken(string challenge, string expectedClaims) + { + string claims = null; + int callCount = 0; + + MockTransport keyVaultTransport = new(new[] + { + // Initial challlenge + new MockResponse(401) + .WithHeader("WWW-Authenticate", @"Bearer authorization=""https://login.windows.net/de763a21-49f7-4b08-a8e1-52c8fbc103b4"", resource=""https://vault.azure.net"""), + + // CAE Challenge + new MockResponse(401) + .WithHeader("WWW-Authenticate", challenge), + + new MockResponse(200) + { + ContentStream = new KeyVaultSecret("test-secret", "secret-value").ToStream(), + }, + }); + + var credential = new TokenCredentialStub((r, c) => + { + claims = r.Claims; + Interlocked.Increment(ref callCount); + Assert.AreEqual(true, r.IsCaeEnabled); + + return new(callCount.ToString(), DateTimeOffset.Now.AddHours(2)); + }, true); + var policy = new BearerTokenAuthenticationPolicy(credential, "scope"); + + SecretClient client = new( + VaultUri, + credential, + new SecretClientOptions() + { + Transport = keyVaultTransport, + }); + + var FooResponse = await client.GetSecretAsync("test-secret"); + Assert.AreEqual(expectedClaims, claims); + } + + private class TokenCredentialStub : TokenCredential + { + public TokenCredentialStub(Func handler, bool isAsync) + { + if (isAsync) + { +#pragma warning disable 1998 + _getTokenAsyncHandler = async (r, c) => handler(r, c); +#pragma warning restore 1998 + } + else + { + _getTokenHandler = handler; + } + } + + private readonly Func> _getTokenAsyncHandler; + private readonly Func _getTokenHandler; + + public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) + => _getTokenAsyncHandler(requestContext, cancellationToken); + + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) + => _getTokenHandler(requestContext, cancellationToken); + } + private class MockTransportBuilder { private const string AuthorizationHeader = "Authorization"; From 72d98ef7f520ba476f382914e5a832779fed5710 Mon Sep 17 00:00:00 2001 From: Jonathan Cardenas Date: Tue, 1 Oct 2024 13:23:45 -0700 Subject: [PATCH 17/27] Fix CI by removing extra test case parameter --- .../tests/ChallengeBasedAuthenticationPolicyTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs b/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs index e98bfa016083..65238c5057a2 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs @@ -344,7 +344,7 @@ public void ThrowsWithTwoConsecutiveCaeChallenges() } [Test] - [TestCase("standard", """Bearer realm="", authorization_uri="https://login.microsoftonline.com/common/oauth2/authorize", error="insufficient_claims", claims="eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ==" """, """{"access_token":{"nbf":{"essential":true,"value":"1726077595"},"xms_caeerror":{"value":"10012"}}}""")] + [TestCase("""Bearer realm="", authorization_uri="https://login.microsoftonline.com/common/oauth2/authorize", error="insufficient_claims", claims="eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ==" """, """{"access_token":{"nbf":{"essential":true,"value":"1726077595"},"xms_caeerror":{"value":"10012"}}}""")] public async Task VerifyClaimsInToken(string challenge, string expectedClaims) { string claims = null; From 46909fe20381e17ef431e5bce190439f969741ab Mon Sep 17 00:00:00 2001 From: Jonathan Cardenas Date: Wed, 2 Oct 2024 23:39:49 -0700 Subject: [PATCH 18/27] Nit changes to tests --- ...ChallengeBasedAuthenticationPolicyTests.cs | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs b/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs index 65238c5057a2..473ae0c109c2 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs @@ -268,6 +268,9 @@ public void HandlesCaeChallenges(){ [Test] public void ThrowsWithTwoConsecutiveCaeChallenges() { + MockResponse caeChallenge = new MockResponse(401) + .WithHeader("WWW-Authenticate", @"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ=="""); + MockTransport keyVaultTransport = new(new[] { // Initial scope challlenge @@ -275,12 +278,10 @@ public void ThrowsWithTwoConsecutiveCaeChallenges() .WithHeader("WWW-Authenticate", @"Bearer authorization=""https://login.windows.net/de763a21-49f7-4b08-a8e1-52c8fbc103b4"", resource=""https://vault.azure.net"""), // CAE Challenge - new MockResponse(401) - .WithHeader("WWW-Authenticate", @"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ=="""), + caeChallenge, // Second CAE Challenge - new MockResponse(401) - .WithHeader("WWW-Authenticate", @"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ=="""), + caeChallenge, new MockResponse(200) { @@ -296,7 +297,7 @@ public void ThrowsWithTwoConsecutiveCaeChallenges() "token_type": "Bearer", "expires_in": 3599, "resource": "https://vault.azure.net", - "access_token": "ZGU3NjNhMjEtNDlmNy00YjA4LWE4ZTEtNTJjOGZiYzEwM2I0" + "access_token": "foo" } """), @@ -306,7 +307,7 @@ public void ThrowsWithTwoConsecutiveCaeChallenges() "token_type": "Bearer", "expires_in": 3599, "resource": "https://vault.azure.net", - "access_token": "NzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3" + "access_token": "foo" } """), @@ -316,7 +317,7 @@ public void ThrowsWithTwoConsecutiveCaeChallenges() "token_type": "Bearer", "expires_in": 3599, "resource": "https://vault.azure.net", - "access_token": GUID.NewGuid().ToString() + "access_token": "foo" } """), }); @@ -336,19 +337,23 @@ public void ThrowsWithTwoConsecutiveCaeChallenges() catch (RequestFailedException ex) { Assert.AreEqual(401, ex.Status); + return; } catch (Exception ex) { Assert.Fail($"Expected RequestFailedException, but got {ex.GetType()}"); + return; } + Assert.Fail("Expected RequestFailedException, but no exception was thrown."); } [Test] - [TestCase("""Bearer realm="", authorization_uri="https://login.microsoftonline.com/common/oauth2/authorize", error="insufficient_claims", claims="eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ==" """, """{"access_token":{"nbf":{"essential":true,"value":"1726077595"},"xms_caeerror":{"value":"10012"}}}""")] + [TestCase(@"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ==""", """{"access_token":{"nbf":{"essential":true,"value":"1726077595"},"xms_caeerror":{"value":"10012"}}}""")] public async Task VerifyClaimsInToken(string challenge, string expectedClaims) { string claims = null; int callCount = 0; + Stream content = new KeyVaultSecret("test-secret", "secret-value").ToStream(); MockTransport keyVaultTransport = new(new[] { @@ -369,6 +374,15 @@ public async Task VerifyClaimsInToken(string challenge, string expectedClaims) var credential = new TokenCredentialStub((r, c) => { claims = r.Claims; + if (callCount == 0) + { + // The first challenge should not have any claims. + Assert.IsNull(claims); + } + else if (callCount == 1) + { + Assert.AreEqual(expectedClaims, claims); + } Interlocked.Increment(ref callCount); Assert.AreEqual(true, r.IsCaeEnabled); From 15a5ab765eb88122fea991b282a3e04dfdfc7075 Mon Sep 17 00:00:00 2001 From: Jonathan Cardenas Date: Thu, 3 Oct 2024 02:45:54 -0700 Subject: [PATCH 19/27] Simplify tests --- ...ChallengeBasedAuthenticationPolicyTests.cs | 66 ++----------------- 1 file changed, 6 insertions(+), 60 deletions(-) diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs b/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs index 473ae0c109c2..3b3f27c9813b 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs @@ -211,60 +211,6 @@ public void GetClaimsFromChallengeHeaders() Assert.IsNull(ChallengeBasedAuthenticationPolicy.getDecodedClaimsParameter(null, response401)); } - [Test] - public void HandlesCaeChallenges(){ - MockTransport keyVaultTransport = new(new[] - { - // Initial scope challlenge - new MockResponse(401) - .WithHeader("WWW-Authenticate", @"Bearer authorization=""https://login.windows.net/de763a21-49f7-4b08-a8e1-52c8fbc103b4"", resource=""https://vault.azure.net"""), - - // CAE Challenge - new MockResponse(401) - .WithHeader("WWW-Authenticate", @"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ=="""), - - new MockResponse(200) - { - ContentStream = new KeyVaultSecret("test-secret", "secret-value").ToStream(), - }, - }); - - MockTransport credentialTransport = new(new[] - { - new MockResponse(200) - .WithJson(""" - { - "token_type": "Bearer", - "expires_in": 3599, - "resource": "https://vault.azure.net", - "access_token": "ZGU3NjNhMjEtNDlmNy00YjA4LWE4ZTEtNTJjOGZiYzEwM2I0" - } - """), - - new MockResponse(200) - .WithJson(""" - { - "token_type": "Bearer", - "expires_in": 3599, - "resource": "https://vault.azure.net", - "access_token": "NzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3" - } - """), - }); - - SecretClient client = new( - VaultUri, - new MockCredential(credentialTransport), - new SecretClientOptions() - { - Transport = keyVaultTransport, - }); - - Response response = client.GetSecret("test-secret"); - Assert.AreEqual(200, response.GetRawResponse().Status); - Assert.AreEqual("secret-value", response.Value.Value); - } - [Test] public void ThrowsWithTwoConsecutiveCaeChallenges() { @@ -349,11 +295,11 @@ public void ThrowsWithTwoConsecutiveCaeChallenges() [Test] [TestCase(@"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ==""", """{"access_token":{"nbf":{"essential":true,"value":"1726077595"},"xms_caeerror":{"value":"10012"}}}""")] - public async Task VerifyClaimsInToken(string challenge, string expectedClaims) + public async Task VerifyCaeClaims(string challenge, string expectedClaims) { string claims = null; int callCount = 0; - Stream content = new KeyVaultSecret("test-secret", "secret-value").ToStream(); + string content = "{\"value\":\"secret-value\"}"; MockTransport keyVaultTransport = new(new[] { @@ -366,9 +312,7 @@ public async Task VerifyClaimsInToken(string challenge, string expectedClaims) .WithHeader("WWW-Authenticate", challenge), new MockResponse(200) - { - ContentStream = new KeyVaultSecret("test-secret", "secret-value").ToStream(), - }, + .WithContent(content) }); var credential = new TokenCredentialStub((r, c) => @@ -398,8 +342,10 @@ public async Task VerifyClaimsInToken(string challenge, string expectedClaims) Transport = keyVaultTransport, }); - var FooResponse = await client.GetSecretAsync("test-secret"); + Response response = await client.GetSecretAsync("test-secret"); Assert.AreEqual(expectedClaims, claims); + Assert.AreEqual(200, response.GetRawResponse().Status); + Assert.AreEqual("secret-value", response.Value.Value); } private class TokenCredentialStub : TokenCredential From ee196ec071b9b92fbbe36be6e6543a6db26462b7 Mon Sep 17 00:00:00 2001 From: Jonathan Cardenas Date: Thu, 3 Oct 2024 12:38:58 -0700 Subject: [PATCH 20/27] removing unnecessary mock responses --- ...ChallengeBasedAuthenticationPolicyTests.cs | 21 +++---------------- 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs b/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs index 3b3f27c9813b..8dfa1952dc8f 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; @@ -227,12 +227,7 @@ public void ThrowsWithTwoConsecutiveCaeChallenges() caeChallenge, // Second CAE Challenge - caeChallenge, - - new MockResponse(200) - { - ContentStream = new KeyVaultSecret("test-secret", "secret-value").ToStream(), - }, + caeChallenge }); MockTransport credentialTransport = new(new[] @@ -255,17 +250,7 @@ public void ThrowsWithTwoConsecutiveCaeChallenges() "resource": "https://vault.azure.net", "access_token": "foo" } - """), - - new MockResponse(200) - .WithJson(""" - { - "token_type": "Bearer", - "expires_in": 3599, - "resource": "https://vault.azure.net", - "access_token": "foo" - } - """), + """) }); SecretClient client = new( From 0c33973a12ff8fb68663afa0984545267595c87c Mon Sep 17 00:00:00 2001 From: Jonathan Cardenas Date: Mon, 7 Oct 2024 12:04:56 -0700 Subject: [PATCH 21/27] Refactor tests to test CAE in all projects --- .../tests/ContinuousAccessEvaluationTests.cs | 98 ++++++++++ .../tests/ContinuousAccessEvaluationTests.cs | 95 ++++++++++ .../tests/ContinuousAccessEvaluationTests.cs | 109 +++++++++++ ...ChallengeBasedAuthenticationPolicyTests.cs | 150 +-------------- .../tests/ContinousAccessEvaluationTests.cs | 90 +++++++++ ...e.Security.KeyVault.Shared.Tests.projitems | 1 + .../ContinuousAccessEvaluationTestsBase.cs | 171 ++++++++++++++++++ 7 files changed, 565 insertions(+), 149 deletions(-) create mode 100644 sdk/keyvault/Azure.Security.KeyVault.Administration/tests/ContinuousAccessEvaluationTests.cs create mode 100644 sdk/keyvault/Azure.Security.KeyVault.Certificates/tests/ContinuousAccessEvaluationTests.cs create mode 100644 sdk/keyvault/Azure.Security.KeyVault.Keys/tests/ContinuousAccessEvaluationTests.cs create mode 100644 sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ContinousAccessEvaluationTests.cs create mode 100644 sdk/keyvault/Azure.Security.KeyVault.Shared/tests/ContinuousAccessEvaluationTestsBase.cs diff --git a/sdk/keyvault/Azure.Security.KeyVault.Administration/tests/ContinuousAccessEvaluationTests.cs b/sdk/keyvault/Azure.Security.KeyVault.Administration/tests/ContinuousAccessEvaluationTests.cs new file mode 100644 index 000000000000..c3418a35cd07 --- /dev/null +++ b/sdk/keyvault/Azure.Security.KeyVault.Administration/tests/ContinuousAccessEvaluationTests.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Azure.Core.TestFramework; +using Azure.Security.KeyVault.Tests; +using NUnit.Framework; + +namespace Azure.Security.KeyVault.Administration.Tests +{ + internal class ContinuousAccessEvaluationTests : ContinuousAccessEvaluationTestsBase + { + [Test] + [TestCase(@"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ==""", """{"access_token":{"nbf":{"essential":true,"value":"1726077595"},"xms_caeerror":{"value":"10012"}}}""")] + public async Task VerifyCaeClaims(string challenge, string expectedClaims) + { + int callCount = 0; + + MockResponse response = new MockResponse(200); + + MockTransport transport = GetMockTransportWithCaeChallenges(numberOfCaeChallenges: 1, final200response: response); + + var credential = new TokenCredentialStub((r, c) => + { + if (callCount == 0) + { + // The first challenge should not have any claims. + Assert.IsNull(r.Claims); + } + else if (callCount == 1) + { + Assert.AreEqual(expectedClaims, r.Claims); + } + Interlocked.Increment(ref callCount); + Assert.AreEqual(true, r.IsCaeEnabled); + + return new(callCount.ToString(), DateTimeOffset.Now.AddHours(2)); + }, true); + + KeyVaultBackupClient client = new( + VaultUri, + credential, + new KeyVaultAdministrationClientOptions() + { + Transport = transport, + }); + + try + { + KeyVaultBackupOperation operation = await client.StartBackupAsync(VaultUri); + } + catch (RequestFailedException ex) + { + Assert.AreEqual(200, ex.Status); + return; + } + catch (Exception ex) + { + Assert.Fail($"Expected RequestFailedException, but got {ex.GetType()}"); + return; + } + } + + [Test] + public void ThrowsWithTwoConsecutiveCaeChallenges() + { + MockTransport keyVaultTransport = GetMockTransportWithCaeChallenges(numberOfCaeChallenges: 2); + + MockTransport credentialTransport = GetMockCredentialTransport(2); + + KeyVaultBackupClient client = new( + VaultUri, + new MockCredential(credentialTransport), + new KeyVaultAdministrationClientOptions() + { + Transport = keyVaultTransport, + }); + + try + { + var operation = client.StartBackup(VaultUri); + } + catch (RequestFailedException ex) + { + Assert.AreEqual(401, ex.Status); + return; + } + catch (Exception ex) + { + Assert.Fail($"Expected RequestFailedException, but got {ex.GetType()}"); + return; + } + Assert.Fail("Expected RequestFailedException, but no exception was thrown."); + } + } +} diff --git a/sdk/keyvault/Azure.Security.KeyVault.Certificates/tests/ContinuousAccessEvaluationTests.cs b/sdk/keyvault/Azure.Security.KeyVault.Certificates/tests/ContinuousAccessEvaluationTests.cs new file mode 100644 index 000000000000..685242897854 --- /dev/null +++ b/sdk/keyvault/Azure.Security.KeyVault.Certificates/tests/ContinuousAccessEvaluationTests.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Azure.Core.TestFramework; +using Azure.Security.KeyVault.Tests; +using NUnit.Framework; + +namespace Azure.Security.KeyVault.Certificates.Tests +{ + internal class ContinuousAccessEvaluationTests : ContinuousAccessEvaluationTestsBase + { + [Test] + [TestCase(@"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ==""", """{"access_token":{"nbf":{"essential":true,"value":"1726077595"},"xms_caeerror":{"value":"10012"}}}""")] + public async Task VerifyCaeClaims(string challenge, string expectedClaims) + { + int callCount = 0; + + MockResponse responseWithSecret = new MockResponse(200) + .WithContent(@"{ + ""id"": ""https://foo.vault.azure.net/certificates/1/foo"", + ""cer"": ""Zm9v"", + ""attributes"": { + }, + ""pending"": { + ""id"": ""foo"" + } + }"); + + MockTransport transport = GetMockTransportWithCaeChallenges(numberOfCaeChallenges: 1, final200response: responseWithSecret); + + var credential = new TokenCredentialStub((r, c) => + { + if (callCount == 0) + { + // The first challenge should not have any claims. + Assert.IsNull(r.Claims); + } + else if (callCount == 1) + { + Assert.AreEqual(expectedClaims, r.Claims); + } + Interlocked.Increment(ref callCount); + Assert.AreEqual(true, r.IsCaeEnabled); + + return new(callCount.ToString(), DateTimeOffset.Now.AddHours(2)); + }, true); + + CertificateClient client = new( + VaultUri, + credential, + new CertificateClientOptions() + { + Transport = transport, + }); + + Response response = await client.GetCertificateAsync("certificate"); + Assert.AreEqual(200, response.GetRawResponse().Status); + } + + [Test] + public void ThrowsWithTwoConsecutiveCaeChallenges() + { + MockTransport keyVaultTransport = GetMockTransportWithCaeChallenges(numberOfCaeChallenges: 2); + + MockTransport credentialTransport = GetMockCredentialTransport(2); + + CertificateClient client = new( + VaultUri, + new MockCredential(credentialTransport), + new CertificateClientOptions() + { + Transport = keyVaultTransport, + }); + + try + { + client.GetCertificate("certificate"); + } + catch (RequestFailedException ex) + { + Assert.AreEqual(401, ex.Status); + return; + } + catch (Exception ex) + { + Assert.Fail($"Expected RequestFailedException, but got {ex.GetType()}"); + return; + } + Assert.Fail("Expected RequestFailedException, but no exception was thrown."); + } + } +} diff --git a/sdk/keyvault/Azure.Security.KeyVault.Keys/tests/ContinuousAccessEvaluationTests.cs b/sdk/keyvault/Azure.Security.KeyVault.Keys/tests/ContinuousAccessEvaluationTests.cs new file mode 100644 index 000000000000..34e8a4909b85 --- /dev/null +++ b/sdk/keyvault/Azure.Security.KeyVault.Keys/tests/ContinuousAccessEvaluationTests.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Azure.Core.TestFramework; +using Azure.Security.KeyVault.Tests; +using NUnit.Framework; + +namespace Azure.Security.KeyVault.Keys.Tests +{ + internal class ContinuousAccessEvaluationTests : ContinuousAccessEvaluationTestsBase + { + [Test] + [TestCase(@"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ==""", """{"access_token":{"nbf":{"essential":true,"value":"1726077595"},"xms_caeerror":{"value":"10012"}}}""")] + public async Task VerifyCaeClaims(string challenge, string expectedClaims) + { + int callCount = 0; + + MockResponse responseWithKey = new MockResponse(200) + .WithContent(@"{ + ""key"": { + ""kid"": ""https://heathskeyvault.vault.azure.net/keys/625710934/ef3685592e1c4e839206aaa10f0f058e"", + ""kty"": ""RSA"", + ""key_ops"": [ + ""encrypt"", + ""decrypt"", + ""sign"", + ""verify"", + ""wrapKey"", + ""unwrapKey"" + ], + ""n"": ""foo"", + ""e"": ""AQAB"" + }, + ""attributes"": { + ""enabled"": true, + ""created"": 1613807137, + ""updated"": 1613807137, + ""recoveryLevel"": ""Recoverable\u002BPurgeable"", + ""recoverableDays"": 90 + } + }"); + + MockTransport transport = GetMockTransportWithCaeChallenges(numberOfCaeChallenges: 1, final200response: responseWithKey); + + var credential = new TokenCredentialStub((r, c) => + { + if (callCount == 0) + { + // The first challenge should not have any claims. + Assert.IsNull(r.Claims); + } + else if (callCount == 1) + { + Assert.AreEqual(expectedClaims, r.Claims); + } + Interlocked.Increment(ref callCount); + Assert.AreEqual(true, r.IsCaeEnabled); + + return new(callCount.ToString(), DateTimeOffset.Now.AddHours(2)); + }, true); + + KeyClient client = new( + VaultUri, + credential, + new KeyClientOptions() + { + Transport = transport, + }); + + Response response = await client.GetKeyAsync("key"); + Assert.AreEqual(200, response.GetRawResponse().Status); + } + + [Test] + public void ThrowsWithTwoConsecutiveCaeChallenges() + { + MockTransport keyVaultTransport = GetMockTransportWithCaeChallenges(numberOfCaeChallenges: 2); + + MockTransport credentialTransport = GetMockCredentialTransport(2); + + KeyClient client = new( + VaultUri, + new MockCredential(credentialTransport), + new KeyClientOptions() + { + Transport = keyVaultTransport, + }); + + try + { + client.GetKey("key"); + } + catch (RequestFailedException ex) + { + Assert.AreEqual(401, ex.Status); + return; + } + catch (Exception ex) + { + Assert.Fail($"Expected RequestFailedException, but got {ex.GetType()}"); + return; + } + Assert.Fail("Expected RequestFailedException, but no exception was thrown."); + } + } +} diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs b/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs index 8dfa1952dc8f..7330a6ad028a 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ChallengeBasedAuthenticationPolicyTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; @@ -211,154 +211,6 @@ public void GetClaimsFromChallengeHeaders() Assert.IsNull(ChallengeBasedAuthenticationPolicy.getDecodedClaimsParameter(null, response401)); } - [Test] - public void ThrowsWithTwoConsecutiveCaeChallenges() - { - MockResponse caeChallenge = new MockResponse(401) - .WithHeader("WWW-Authenticate", @"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ=="""); - - MockTransport keyVaultTransport = new(new[] - { - // Initial scope challlenge - new MockResponse(401) - .WithHeader("WWW-Authenticate", @"Bearer authorization=""https://login.windows.net/de763a21-49f7-4b08-a8e1-52c8fbc103b4"", resource=""https://vault.azure.net"""), - - // CAE Challenge - caeChallenge, - - // Second CAE Challenge - caeChallenge - }); - - MockTransport credentialTransport = new(new[] - { - new MockResponse(200) - .WithJson(""" - { - "token_type": "Bearer", - "expires_in": 3599, - "resource": "https://vault.azure.net", - "access_token": "foo" - } - """), - - new MockResponse(200) - .WithJson(""" - { - "token_type": "Bearer", - "expires_in": 3599, - "resource": "https://vault.azure.net", - "access_token": "foo" - } - """) - }); - - SecretClient client = new( - VaultUri, - new MockCredential(credentialTransport), - new SecretClientOptions() - { - Transport = keyVaultTransport, - }); - - try - { - client.GetSecret("test-secret"); - } - catch (RequestFailedException ex) - { - Assert.AreEqual(401, ex.Status); - return; - } - catch (Exception ex) - { - Assert.Fail($"Expected RequestFailedException, but got {ex.GetType()}"); - return; - } - Assert.Fail("Expected RequestFailedException, but no exception was thrown."); - } - - [Test] - [TestCase(@"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ==""", """{"access_token":{"nbf":{"essential":true,"value":"1726077595"},"xms_caeerror":{"value":"10012"}}}""")] - public async Task VerifyCaeClaims(string challenge, string expectedClaims) - { - string claims = null; - int callCount = 0; - string content = "{\"value\":\"secret-value\"}"; - - MockTransport keyVaultTransport = new(new[] - { - // Initial challlenge - new MockResponse(401) - .WithHeader("WWW-Authenticate", @"Bearer authorization=""https://login.windows.net/de763a21-49f7-4b08-a8e1-52c8fbc103b4"", resource=""https://vault.azure.net"""), - - // CAE Challenge - new MockResponse(401) - .WithHeader("WWW-Authenticate", challenge), - - new MockResponse(200) - .WithContent(content) - }); - - var credential = new TokenCredentialStub((r, c) => - { - claims = r.Claims; - if (callCount == 0) - { - // The first challenge should not have any claims. - Assert.IsNull(claims); - } - else if (callCount == 1) - { - Assert.AreEqual(expectedClaims, claims); - } - Interlocked.Increment(ref callCount); - Assert.AreEqual(true, r.IsCaeEnabled); - - return new(callCount.ToString(), DateTimeOffset.Now.AddHours(2)); - }, true); - var policy = new BearerTokenAuthenticationPolicy(credential, "scope"); - - SecretClient client = new( - VaultUri, - credential, - new SecretClientOptions() - { - Transport = keyVaultTransport, - }); - - Response response = await client.GetSecretAsync("test-secret"); - Assert.AreEqual(expectedClaims, claims); - Assert.AreEqual(200, response.GetRawResponse().Status); - Assert.AreEqual("secret-value", response.Value.Value); - } - - private class TokenCredentialStub : TokenCredential - { - public TokenCredentialStub(Func handler, bool isAsync) - { - if (isAsync) - { -#pragma warning disable 1998 - _getTokenAsyncHandler = async (r, c) => handler(r, c); -#pragma warning restore 1998 - } - else - { - _getTokenHandler = handler; - } - } - - private readonly Func> _getTokenAsyncHandler; - private readonly Func _getTokenHandler; - - public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) - => _getTokenAsyncHandler(requestContext, cancellationToken); - - public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) - => _getTokenHandler(requestContext, cancellationToken); - } - private class MockTransportBuilder { private const string AuthorizationHeader = "Authorization"; diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ContinousAccessEvaluationTests.cs b/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ContinousAccessEvaluationTests.cs new file mode 100644 index 000000000000..8b795f23286b --- /dev/null +++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ContinousAccessEvaluationTests.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Azure.Core.TestFramework; +using Azure.Security.KeyVault.Tests; +using NUnit.Framework; + +namespace Azure.Security.KeyVault.Secrets.Tests +{ + internal class ContinousAccessEvaluationTests : ContinuousAccessEvaluationTestsBase + { + [Test] + [TestCase(@"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ==""", """{"access_token":{"nbf":{"essential":true,"value":"1726077595"},"xms_caeerror":{"value":"10012"}}}""")] + public async Task VerifyCaeClaims(string challenge, string expectedClaims) + { + int callCount = 0; + + MockResponse responseWithSecret = new MockResponse(200) + { + ContentStream = new KeyVaultSecret("test-secret", "secret-value").ToStream(), + }; + + MockTransport transport = GetMockTransportWithCaeChallenges(numberOfCaeChallenges: 1, final200response: responseWithSecret); + + var credential = new TokenCredentialStub((r, c) => + { + if (callCount == 0) + { + // The first challenge should not have any claims. + Assert.IsNull(r.Claims); + } + else if (callCount == 1) + { + Assert.AreEqual(expectedClaims, r.Claims); + } + Interlocked.Increment(ref callCount); + Assert.AreEqual(true, r.IsCaeEnabled); + + return new(callCount.ToString(), DateTimeOffset.Now.AddHours(2)); + }, true); + + SecretClient client = new( + VaultUri, + credential, + new SecretClientOptions() + { + Transport = transport, + }); + + Response response = await client.GetSecretAsync("test-secret"); + Assert.AreEqual(200, response.GetRawResponse().Status); + Assert.AreEqual("secret-value", response.Value.Value); + } + + [Test] + public void ThrowsWithTwoConsecutiveCaeChallenges() + { + MockTransport keyVaultTransport = GetMockTransportWithCaeChallenges(numberOfCaeChallenges: 2); + + MockTransport credentialTransport = GetMockCredentialTransport(2); + + SecretClient client = new( + VaultUri, + new MockCredential(credentialTransport), + new SecretClientOptions() + { + Transport = keyVaultTransport, + }); + + try + { + client.GetSecret("test-secret"); + } + catch (RequestFailedException ex) + { + Assert.AreEqual(401, ex.Status); + return; + } + catch (Exception ex) + { + Assert.Fail($"Expected RequestFailedException, but got {ex.GetType()}"); + return; + } + Assert.Fail("Expected RequestFailedException, but no exception was thrown."); + } + } +} diff --git a/sdk/keyvault/Azure.Security.KeyVault.Shared/tests/Azure.Security.KeyVault.Shared.Tests.projitems b/sdk/keyvault/Azure.Security.KeyVault.Shared/tests/Azure.Security.KeyVault.Shared.Tests.projitems index 9f553875f304..55413c17628b 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Shared/tests/Azure.Security.KeyVault.Shared.Tests.projitems +++ b/sdk/keyvault/Azure.Security.KeyVault.Shared/tests/Azure.Security.KeyVault.Shared.Tests.projitems @@ -9,6 +9,7 @@ Azure.Security.KeyVault.Tests + diff --git a/sdk/keyvault/Azure.Security.KeyVault.Shared/tests/ContinuousAccessEvaluationTestsBase.cs b/sdk/keyvault/Azure.Security.KeyVault.Shared/tests/ContinuousAccessEvaluationTestsBase.cs new file mode 100644 index 000000000000..a655738d3256 --- /dev/null +++ b/sdk/keyvault/Azure.Security.KeyVault.Shared/tests/ContinuousAccessEvaluationTestsBase.cs @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.Threading; +using Azure.Core.TestFramework; +using Azure.Core; +using System.IO; +using System.Text.Json; +using Azure.Core.Pipeline; + +namespace Azure.Security.KeyVault.Tests +{ + internal class ContinuousAccessEvaluationTestsBase + { + private MockResponse defaultCaeChallenge = new MockResponse(401).WithHeader("WWW-Authenticate", @"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ=="""); + + private MockResponse defaultInitialChallenge = new MockResponse(401).WithHeader("WWW-Authenticate", @"Bearer authorization=""https://login.windows.net/de763a21-49f7-4b08-a8e1-52c8fbc103b4"", resource=""https://vault.azure.net"""); + + private const string VaultHost = "test.vault.azure.net"; + protected Uri VaultUri => new Uri("https://" + VaultHost); + + protected MockTransport GetMockTransportWithCaeChallenges(int numberOfCaeChallenges = 1, MockResponse final200response = null ) + { + if (numberOfCaeChallenges < 1) + { + throw new ArgumentOutOfRangeException(nameof(numberOfCaeChallenges), "Number of CAE challenges must be greater than or equal to 1."); + } + + var responses = new List { defaultInitialChallenge }; + for (int i = 0; i < numberOfCaeChallenges; i++) + { + responses.Add(defaultCaeChallenge); + } + if (final200response != null) + { + responses.Add(final200response); + } + return new MockTransport(responses.ToArray()); + } + + protected MockTransport GetMockCredentialTransport(int numberOfTokenResponses = 1) + { + if (numberOfTokenResponses < 1) + { + throw new ArgumentOutOfRangeException(nameof(numberOfTokenResponses), "Number of token responses must be greater than or equal to 1."); + } + + var responses = new List(); + for (int i = 0; i < numberOfTokenResponses; i++) + { + responses.Add(new MockResponse(200) + .WithJson(""" + { + "token_type": "Bearer", + "expires_in": 3599, + "resource": "https://vault.azure.net", + "access_token": "foo" + } + """)); + } + return new MockTransport(responses.ToArray()); + } + + protected class TokenCredentialStub : TokenCredential + { + public TokenCredentialStub() { } + + public TokenCredentialStub(Func handler, bool isAsync) + { + setCallBack(handler, isAsync); + } + + private Func> _getTokenAsyncHandler; + private Func _getTokenHandler; + + public void setCallBack(Func handler, bool isAsync) + { + if (isAsync) + { +#pragma warning disable 1998 + _getTokenAsyncHandler = async (r, c) => handler(r, c); +#pragma warning restore 1998 + } + else + { + _getTokenHandler = handler; + } + } + + public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) + => _getTokenAsyncHandler(requestContext, cancellationToken); + + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) + => _getTokenHandler(requestContext, cancellationToken); + } + + protected class MockCredential : TokenCredential + { + private readonly HttpPipeline _pipeline; + private readonly string _tenantId; + private readonly string _clientId; + private readonly string _clientSecret; + private const string TenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47"; + + public MockCredential(MockTransport transport, string tenantId = TenantId, string clientId = "test_id", string clientSecret = "test_secret") + { + _pipeline = new HttpPipeline(transport); + _tenantId = tenantId ?? throw new ArgumentNullException(nameof(tenantId)); + _clientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); + _clientSecret = clientSecret ?? throw new ArgumentNullException(nameof(clientSecret)); + } + + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) => GetTokenAsync(requestContext, cancellationToken).EnsureCompleted(); + + public override async ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + Request request = _pipeline.CreateRequest(); + request.Method = RequestMethod.Post; + request.Headers.Add(HttpHeader.Common.FormUrlEncodedContentType); + + request.Uri.Reset(new Uri($"https://login.windows.net/{_tenantId}/oauth2/v2.0/token")); + + string body = $"response_type=token&grant_type=client_credentials&client_id={Uri.EscapeDataString(_clientId)}&client_secret={Uri.EscapeDataString(_clientSecret)}&scope={Uri.EscapeDataString(string.Join(" ", requestContext.Scopes))}"; + ReadOnlyMemory content = Encoding.UTF8.GetBytes(body).AsMemory(); + request.Content = RequestContent.Create(content); + + Response response = await _pipeline.SendRequestAsync(request, cancellationToken); + if (response.Status == 200 || response.Status == 201) + { + return await DeserializeAsync(response.ContentStream, cancellationToken); + } + + throw new RequestFailedException(response.Status, response.ReasonPhrase); + } + + private static async Task DeserializeAsync(Stream content, CancellationToken cancellationToken) + { + using (JsonDocument json = await JsonDocument.ParseAsync(content, default, cancellationToken).ConfigureAwait(false)) + { + return Deserialize(json.RootElement); + } + } + + private static AccessToken Deserialize(JsonElement json) + { + string accessToken = null; + DateTimeOffset expiresOn = DateTimeOffset.MaxValue; + + foreach (JsonProperty prop in json.EnumerateObject()) + { + switch (prop.Name) + { + case "access_token": + accessToken = prop.Value.GetString(); + break; + + case "expires_in": + expiresOn = DateTimeOffset.UtcNow + TimeSpan.FromSeconds(prop.Value.GetInt64()); + break; + } + } + + return new AccessToken(accessToken, expiresOn); + } + } + } +} From a0de67f7ec272ae7f4ba23b496da2361bd6d3c0b Mon Sep 17 00:00:00 2001 From: Jonathan Cardenas Date: Mon, 7 Oct 2024 15:12:19 -0700 Subject: [PATCH 22/27] Make tests non parallelizable --- .../tests/ContinuousAccessEvaluationTests.cs | 1 + .../tests/ContinuousAccessEvaluationTests.cs | 1 + .../tests/ContinuousAccessEvaluationTests.cs | 1 + .../tests/ContinousAccessEvaluationTests.cs | 1 + 4 files changed, 4 insertions(+) diff --git a/sdk/keyvault/Azure.Security.KeyVault.Administration/tests/ContinuousAccessEvaluationTests.cs b/sdk/keyvault/Azure.Security.KeyVault.Administration/tests/ContinuousAccessEvaluationTests.cs index c3418a35cd07..276e9433c3e7 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Administration/tests/ContinuousAccessEvaluationTests.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Administration/tests/ContinuousAccessEvaluationTests.cs @@ -10,6 +10,7 @@ namespace Azure.Security.KeyVault.Administration.Tests { + [NonParallelizable] internal class ContinuousAccessEvaluationTests : ContinuousAccessEvaluationTestsBase { [Test] diff --git a/sdk/keyvault/Azure.Security.KeyVault.Certificates/tests/ContinuousAccessEvaluationTests.cs b/sdk/keyvault/Azure.Security.KeyVault.Certificates/tests/ContinuousAccessEvaluationTests.cs index 685242897854..823a8519866b 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Certificates/tests/ContinuousAccessEvaluationTests.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Certificates/tests/ContinuousAccessEvaluationTests.cs @@ -10,6 +10,7 @@ namespace Azure.Security.KeyVault.Certificates.Tests { + [NonParallelizable] internal class ContinuousAccessEvaluationTests : ContinuousAccessEvaluationTestsBase { [Test] diff --git a/sdk/keyvault/Azure.Security.KeyVault.Keys/tests/ContinuousAccessEvaluationTests.cs b/sdk/keyvault/Azure.Security.KeyVault.Keys/tests/ContinuousAccessEvaluationTests.cs index 34e8a4909b85..256c362e50f0 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Keys/tests/ContinuousAccessEvaluationTests.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Keys/tests/ContinuousAccessEvaluationTests.cs @@ -10,6 +10,7 @@ namespace Azure.Security.KeyVault.Keys.Tests { + [NonParallelizable] internal class ContinuousAccessEvaluationTests : ContinuousAccessEvaluationTestsBase { [Test] diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ContinousAccessEvaluationTests.cs b/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ContinousAccessEvaluationTests.cs index 8b795f23286b..39ba30ca0c73 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ContinousAccessEvaluationTests.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ContinousAccessEvaluationTests.cs @@ -10,6 +10,7 @@ namespace Azure.Security.KeyVault.Secrets.Tests { + [NonParallelizable] internal class ContinousAccessEvaluationTests : ContinuousAccessEvaluationTestsBase { [Test] From 0ffee52d84412e26fa1060a07fe7e57160d730ad Mon Sep 17 00:00:00 2001 From: Jonathan Cardenas Date: Mon, 7 Oct 2024 23:47:25 -0700 Subject: [PATCH 23/27] Add setup method to CAE tests --- .../tests/ContinuousAccessEvaluationTests.cs | 6 ++++++ .../tests/ContinuousAccessEvaluationTests.cs | 6 ++++++ .../tests/ContinuousAccessEvaluationTests.cs | 6 ++++++ .../tests/ContinousAccessEvaluationTests.cs | 6 ++++++ 4 files changed, 24 insertions(+) diff --git a/sdk/keyvault/Azure.Security.KeyVault.Administration/tests/ContinuousAccessEvaluationTests.cs b/sdk/keyvault/Azure.Security.KeyVault.Administration/tests/ContinuousAccessEvaluationTests.cs index 276e9433c3e7..590240edc7dc 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Administration/tests/ContinuousAccessEvaluationTests.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Administration/tests/ContinuousAccessEvaluationTests.cs @@ -13,6 +13,12 @@ namespace Azure.Security.KeyVault.Administration.Tests [NonParallelizable] internal class ContinuousAccessEvaluationTests : ContinuousAccessEvaluationTestsBase { + [SetUp] + public void Setup() + { + ChallengeBasedAuthenticationPolicy.ClearCache(); + } + [Test] [TestCase(@"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ==""", """{"access_token":{"nbf":{"essential":true,"value":"1726077595"},"xms_caeerror":{"value":"10012"}}}""")] public async Task VerifyCaeClaims(string challenge, string expectedClaims) diff --git a/sdk/keyvault/Azure.Security.KeyVault.Certificates/tests/ContinuousAccessEvaluationTests.cs b/sdk/keyvault/Azure.Security.KeyVault.Certificates/tests/ContinuousAccessEvaluationTests.cs index 823a8519866b..eef6a425ffc0 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Certificates/tests/ContinuousAccessEvaluationTests.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Certificates/tests/ContinuousAccessEvaluationTests.cs @@ -13,6 +13,12 @@ namespace Azure.Security.KeyVault.Certificates.Tests [NonParallelizable] internal class ContinuousAccessEvaluationTests : ContinuousAccessEvaluationTestsBase { + [SetUp] + public void Setup() + { + ChallengeBasedAuthenticationPolicy.ClearCache(); + } + [Test] [TestCase(@"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ==""", """{"access_token":{"nbf":{"essential":true,"value":"1726077595"},"xms_caeerror":{"value":"10012"}}}""")] public async Task VerifyCaeClaims(string challenge, string expectedClaims) diff --git a/sdk/keyvault/Azure.Security.KeyVault.Keys/tests/ContinuousAccessEvaluationTests.cs b/sdk/keyvault/Azure.Security.KeyVault.Keys/tests/ContinuousAccessEvaluationTests.cs index 256c362e50f0..6e34e78f7dfd 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Keys/tests/ContinuousAccessEvaluationTests.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Keys/tests/ContinuousAccessEvaluationTests.cs @@ -13,6 +13,12 @@ namespace Azure.Security.KeyVault.Keys.Tests [NonParallelizable] internal class ContinuousAccessEvaluationTests : ContinuousAccessEvaluationTestsBase { + [SetUp] + public void Setup() + { + ChallengeBasedAuthenticationPolicy.ClearCache(); + } + [Test] [TestCase(@"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ==""", """{"access_token":{"nbf":{"essential":true,"value":"1726077595"},"xms_caeerror":{"value":"10012"}}}""")] public async Task VerifyCaeClaims(string challenge, string expectedClaims) diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ContinousAccessEvaluationTests.cs b/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ContinousAccessEvaluationTests.cs index 39ba30ca0c73..6050a15c89e0 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ContinousAccessEvaluationTests.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ContinousAccessEvaluationTests.cs @@ -13,6 +13,12 @@ namespace Azure.Security.KeyVault.Secrets.Tests [NonParallelizable] internal class ContinousAccessEvaluationTests : ContinuousAccessEvaluationTestsBase { + [SetUp] + public void Setup() + { + ChallengeBasedAuthenticationPolicy.ClearCache(); + } + [Test] [TestCase(@"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ==""", """{"access_token":{"nbf":{"essential":true,"value":"1726077595"},"xms_caeerror":{"value":"10012"}}}""")] public async Task VerifyCaeClaims(string challenge, string expectedClaims) From f03fe3bec00d168e2a9aded567e9072dbd7dfc04 Mon Sep 17 00:00:00 2001 From: Jonathan Cardenas Date: Thu, 10 Oct 2024 08:47:58 -0700 Subject: [PATCH 24/27] Test for tokens obtained from cae challenges --- .../tests/ContinousAccessEvaluationTests.cs | 85 +++++++++++++++++++ .../ContinuousAccessEvaluationTestsBase.cs | 4 +- 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ContinousAccessEvaluationTests.cs b/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ContinousAccessEvaluationTests.cs index 6050a15c89e0..b9e22660cba1 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ContinousAccessEvaluationTests.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ContinousAccessEvaluationTests.cs @@ -93,5 +93,90 @@ public void ThrowsWithTwoConsecutiveCaeChallenges() } Assert.Fail("Expected RequestFailedException, but no exception was thrown."); } + + [Test] + public void ensureTokenFromClaimsChallengeGetsUsed() + { + MockTransport transport = new(new[] + { + defaultInitialChallenge, + + new MockResponse(200) + .WithJson(""" + { + "token_type": "Bearer", + "expires_in": 3599, + "resource": "https://vault.azure.net", + "access_token": "TOKEN_1" + } + """), + + defaultCaeChallenge, + + new MockResponse(200) + .WithJson(""" + { + "token_type": "Bearer", + "expires_in": 3599, + "resource": "https://vault.azure.net", + "access_token": "TOKEN_2" + } + """), + + new MockResponse(200) + { + ContentStream = new KeyVaultSecret("test-secret", "secret-value").ToStream(), + }, + + new MockResponse(200) + { + ContentStream = new KeyVaultSecret("test-secret", "secret-value").ToStream(), + }, + + new MockResponse(401) + .WithHeader("WWW-Authenticate", @"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk5In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjIyMjEyIn19fQ=="""), + + new MockResponse(200) + .WithJson(""" + { + "token_type": "Bearer", + "expires_in": 3599, + "resource": "https://vault.azure.net", + "access_token": "TOKEN_3" + } + """), + + new MockResponse(200) + { + ContentStream = new KeyVaultSecret("test-secret", "secret-value").ToStream(), + }, + }); + + SecretClient client = new( + VaultUri, + new MockCredential(transport), + new SecretClientOptions() + { + Transport = transport, + }); + + client.GetSecret("test-secret"); + client.GetSecret("test-secret"); + client.GetSecret("test-secret"); + + var requests = transport.Requests; + + // Token 1 gets revoked immidiately after the first request, so it's never used in a GET request to KeyVault + Assert.IsTrue(transport.Requests[4].Headers.TryGetValue("Authorization", out string authorizationValue)); + Assert.AreEqual("Bearer TOKEN_2", authorizationValue); + + // Token 2 is still valid for the second GET request + Assert.IsTrue(transport.Requests[5].Headers.TryGetValue("Authorization", out string authorizationValue2)); + Assert.AreEqual("Bearer TOKEN_2", authorizationValue2); + + // Token 2 becomes invalid and now Token 3 is used for the third GET request + Assert.IsTrue(transport.Requests[8].Headers.TryGetValue("Authorization", out string authorizationValue3)); + Assert.AreEqual("Bearer TOKEN_3", authorizationValue3); + } } } diff --git a/sdk/keyvault/Azure.Security.KeyVault.Shared/tests/ContinuousAccessEvaluationTestsBase.cs b/sdk/keyvault/Azure.Security.KeyVault.Shared/tests/ContinuousAccessEvaluationTestsBase.cs index a655738d3256..af2ee53ff913 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Shared/tests/ContinuousAccessEvaluationTestsBase.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Shared/tests/ContinuousAccessEvaluationTestsBase.cs @@ -16,9 +16,9 @@ namespace Azure.Security.KeyVault.Tests { internal class ContinuousAccessEvaluationTestsBase { - private MockResponse defaultCaeChallenge = new MockResponse(401).WithHeader("WWW-Authenticate", @"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ=="""); + protected MockResponse defaultCaeChallenge = new MockResponse(401).WithHeader("WWW-Authenticate", @"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ=="""); - private MockResponse defaultInitialChallenge = new MockResponse(401).WithHeader("WWW-Authenticate", @"Bearer authorization=""https://login.windows.net/de763a21-49f7-4b08-a8e1-52c8fbc103b4"", resource=""https://vault.azure.net"""); + protected MockResponse defaultInitialChallenge = new MockResponse(401).WithHeader("WWW-Authenticate", @"Bearer authorization=""https://login.windows.net/de763a21-49f7-4b08-a8e1-52c8fbc103b4"", resource=""https://vault.azure.net"""); private const string VaultHost = "test.vault.azure.net"; protected Uri VaultUri => new Uri("https://" + VaultHost); From 8bd6cfc4e2a07235bcc70584880810a1dc039749 Mon Sep 17 00:00:00 2001 From: Jonathan Cardenas Date: Thu, 10 Oct 2024 14:09:00 -0700 Subject: [PATCH 25/27] Fix test / CI --- .../tests/ContinousAccessEvaluationTests.cs | 47 ++++--------------- 1 file changed, 10 insertions(+), 37 deletions(-) diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ContinousAccessEvaluationTests.cs b/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ContinousAccessEvaluationTests.cs index b9e22660cba1..c7a90916dd60 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ContinousAccessEvaluationTests.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ContinousAccessEvaluationTests.cs @@ -111,6 +111,11 @@ public void ensureTokenFromClaimsChallengeGetsUsed() } """), + new MockResponse(200) + { + ContentStream = new KeyVaultSecret("test-secret", "secret-value").ToStream(), + }, + defaultCaeChallenge, new MockResponse(200) @@ -124,31 +129,8 @@ public void ensureTokenFromClaimsChallengeGetsUsed() """), new MockResponse(200) - { - ContentStream = new KeyVaultSecret("test-secret", "secret-value").ToStream(), - }, - - new MockResponse(200) - { - ContentStream = new KeyVaultSecret("test-secret", "secret-value").ToStream(), - }, - - new MockResponse(401) - .WithHeader("WWW-Authenticate", @"Bearer realm="""", authorization_uri=""https://login.microsoftonline.com/common/oauth2/authorize"", error=""insufficient_claims"", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk5In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjIyMjEyIn19fQ=="""), - - new MockResponse(200) - .WithJson(""" { - "token_type": "Bearer", - "expires_in": 3599, - "resource": "https://vault.azure.net", - "access_token": "TOKEN_3" - } - """), - - new MockResponse(200) - { - ContentStream = new KeyVaultSecret("test-secret", "secret-value").ToStream(), + ContentStream = new KeyVaultSecret("test-secret2", "secret-value").ToStream(), }, }); @@ -160,23 +142,14 @@ public void ensureTokenFromClaimsChallengeGetsUsed() Transport = transport, }); - client.GetSecret("test-secret"); - client.GetSecret("test-secret"); - client.GetSecret("test-secret"); + _ = client.GetSecret("test-secret"); + _ = client.GetSecret("test-secret2"); - var requests = transport.Requests; + Assert.IsTrue(transport.Requests[2].Headers.TryGetValue("Authorization", out string authorizationValue)); + Assert.AreEqual("Bearer TOKEN_1", authorizationValue); - // Token 1 gets revoked immidiately after the first request, so it's never used in a GET request to KeyVault - Assert.IsTrue(transport.Requests[4].Headers.TryGetValue("Authorization", out string authorizationValue)); - Assert.AreEqual("Bearer TOKEN_2", authorizationValue); - - // Token 2 is still valid for the second GET request Assert.IsTrue(transport.Requests[5].Headers.TryGetValue("Authorization", out string authorizationValue2)); Assert.AreEqual("Bearer TOKEN_2", authorizationValue2); - - // Token 2 becomes invalid and now Token 3 is used for the third GET request - Assert.IsTrue(transport.Requests[8].Headers.TryGetValue("Authorization", out string authorizationValue3)); - Assert.AreEqual("Bearer TOKEN_3", authorizationValue3); } } } From 8544ff60e33f873cfd1610d35e831e468524f41b Mon Sep 17 00:00:00 2001 From: Jonathan Cardenas Date: Thu, 10 Oct 2024 14:58:45 -0700 Subject: [PATCH 26/27] Update dependency for System.ClientModel --- eng/Packages.Data.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/Packages.Data.props b/eng/Packages.Data.props index 90250c5d9ca9..c69c48779f5b 100644 --- a/eng/Packages.Data.props +++ b/eng/Packages.Data.props @@ -354,7 +354,7 @@ - + From 6ac9c91e1a39d48c7d29844e70cb977e9ccebfaa Mon Sep 17 00:00:00 2001 From: Jonathan Cardenas Date: Thu, 10 Oct 2024 16:15:00 -0700 Subject: [PATCH 27/27] Apply suggestions --- .../tests/ContinuousAccessEvaluationTests.cs | 4 ++++ .../tests/ContinuousAccessEvaluationTests.cs | 4 ++++ .../tests/ContinuousAccessEvaluationTests.cs | 4 ++++ .../tests/ContinousAccessEvaluationTests.cs | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/sdk/keyvault/Azure.Security.KeyVault.Administration/tests/ContinuousAccessEvaluationTests.cs b/sdk/keyvault/Azure.Security.KeyVault.Administration/tests/ContinuousAccessEvaluationTests.cs index 590240edc7dc..e086160b485d 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Administration/tests/ContinuousAccessEvaluationTests.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Administration/tests/ContinuousAccessEvaluationTests.cs @@ -40,6 +40,10 @@ public async Task VerifyCaeClaims(string challenge, string expectedClaims) { Assert.AreEqual(expectedClaims, r.Claims); } + else + { + Assert.Fail("unexpected token request"); + } Interlocked.Increment(ref callCount); Assert.AreEqual(true, r.IsCaeEnabled); diff --git a/sdk/keyvault/Azure.Security.KeyVault.Certificates/tests/ContinuousAccessEvaluationTests.cs b/sdk/keyvault/Azure.Security.KeyVault.Certificates/tests/ContinuousAccessEvaluationTests.cs index eef6a425ffc0..27fca350ee8d 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Certificates/tests/ContinuousAccessEvaluationTests.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Certificates/tests/ContinuousAccessEvaluationTests.cs @@ -49,6 +49,10 @@ public async Task VerifyCaeClaims(string challenge, string expectedClaims) { Assert.AreEqual(expectedClaims, r.Claims); } + else + { + Assert.Fail("unexpected token request"); + } Interlocked.Increment(ref callCount); Assert.AreEqual(true, r.IsCaeEnabled); diff --git a/sdk/keyvault/Azure.Security.KeyVault.Keys/tests/ContinuousAccessEvaluationTests.cs b/sdk/keyvault/Azure.Security.KeyVault.Keys/tests/ContinuousAccessEvaluationTests.cs index 6e34e78f7dfd..6f227b954781 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Keys/tests/ContinuousAccessEvaluationTests.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Keys/tests/ContinuousAccessEvaluationTests.cs @@ -63,6 +63,10 @@ public async Task VerifyCaeClaims(string challenge, string expectedClaims) { Assert.AreEqual(expectedClaims, r.Claims); } + else + { + Assert.Fail("unexpected token request"); + } Interlocked.Increment(ref callCount); Assert.AreEqual(true, r.IsCaeEnabled); diff --git a/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ContinousAccessEvaluationTests.cs b/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ContinousAccessEvaluationTests.cs index c7a90916dd60..dad44a925d3e 100644 --- a/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ContinousAccessEvaluationTests.cs +++ b/sdk/keyvault/Azure.Security.KeyVault.Secrets/tests/ContinousAccessEvaluationTests.cs @@ -43,6 +43,10 @@ public async Task VerifyCaeClaims(string challenge, string expectedClaims) { Assert.AreEqual(expectedClaims, r.Claims); } + else + { + Assert.Fail("unexpected token request"); + } Interlocked.Increment(ref callCount); Assert.AreEqual(true, r.IsCaeEnabled);