Skip to content

Commit

Permalink
[Key Vault] Add CAE support & update System.ClientModel dependency ve…
Browse files Browse the repository at this point in the history
…rsion (#46013)

* Add flag and enable CAE to AuthorizeRequestInternal

* Enable CAE for AuthorizeRequestOnChallenge

* Add flag in SecretClientOption and SecretClient

* Revert "Add flag in SecretClientOption and SecretClient"

This reverts commit 02a805d.

* Enable CAE by default

* Removing unused parameter

* Remove saving the claims in the cache

* Update Changelog

* Update changelogs

* Simplify error checking logic

Addressing comment in PR

* Add test for base64 claims

* Override Process function to handle the first CAE Challenge after a scope challenge

* Add tests

* Separate credential and client transports and assert for a 401.

* Nest rety inside challenge if block

* Add test for claims in token

* Fix CI by removing extra test case parameter

* Nit changes to tests

* Simplify tests

* removing unnecessary mock responses

* Refactor tests to test CAE in all projects

* Make tests non parallelizable

* Add setup method to CAE tests

* Test for tokens obtained from cae challenges

* Fix test / CI

* Update dependency for System.ClientModel

* Apply suggestions
  • Loading branch information
JonathanCrd authored Oct 11, 2024
1 parent 991f2e6 commit bb158f4
Show file tree
Hide file tree
Showing 13 changed files with 795 additions and 3 deletions.
2 changes: 1 addition & 1 deletion eng/Packages.Data.props
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@
<PackageReference Update="Polly.Contrib.WaitAndRetry" Version="1.1.1" />
<PackageReference Update="Portable.BouncyCastle" Version="1.9.0" />
<PackageReference Update="PublicApiGenerator" Version="10.0.1" />
<PackageReference Update="System.ClientModel" Version="1.2.0" />
<PackageReference Update="System.ClientModel" Version="1.2.1" />
<PackageReference Update="System.Diagnostics.TraceSource" Version="4.3.0" />
<PackageReference Update="System.IO.Compression" Version="4.3.0" />
<PackageReference Update="System.IO.Pipelines" Version="4.5.1" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
- Support for Continuous Access Evaluation (CAE).

### Breaking Changes

Expand Down
Original file line number Diff line number Diff line change
@@ -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.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)
{
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);
}
else
{
Assert.Fail("unexpected token request");
}
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.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 4.7.0-beta.1 (Unreleased)

### Features Added
- Support for Continuous Access Evaluation (CAE).

### Breaking Changes

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// 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
{
[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)
{
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);
}
else
{
Assert.Fail("unexpected token request");
}
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<KeyVaultCertificateWithPolicy> 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.");
}
}
}
1 change: 1 addition & 0 deletions sdk/keyvault/Azure.Security.KeyVault.Keys/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 4.7.0-beta.1 (Unreleased)

### Features Added
- Support for Continuous Access Evaluation (CAE).

### Breaking Changes

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// 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
{
[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)
{
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);
}
else
{
Assert.Fail("unexpected token request");
}
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<KeyVaultKey> 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.");
}
}
}
1 change: 1 addition & 0 deletions sdk/keyvault/Azure.Security.KeyVault.Secrets/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 4.7.0-beta.1 (Unreleased)

### Features Added
- Support for Continuous Access Evaluation (CAE).

### Breaking Changes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,18 @@ public async Task ReauthenticatesWhenTenantChanged()
Assert.AreEqual("secret-value", response.Value.Value);
}

[Test]
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==""");
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";
Expand Down
Loading

0 comments on commit bb158f4

Please sign in to comment.