Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Key Vault] Add CAE support #46013

Merged
merged 30 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
71190d0
Add flag and enable CAE to AuthorizeRequestInternal
JonathanCrd Sep 17, 2024
36643e2
Enable CAE for AuthorizeRequestOnChallenge
JonathanCrd Sep 17, 2024
02a805d
Add flag in SecretClientOption and SecretClient
JonathanCrd Sep 17, 2024
8bd442a
Revert "Add flag in SecretClientOption and SecretClient"
JonathanCrd Sep 17, 2024
c6a65da
Enable CAE by default
JonathanCrd Sep 17, 2024
8b4e44c
Removing unused parameter
JonathanCrd Sep 17, 2024
a8772d5
Remove saving the claims in the cache
JonathanCrd Sep 17, 2024
ee0b2c6
Update Changelog
JonathanCrd Sep 17, 2024
f645368
Update changelogs
JonathanCrd Sep 18, 2024
1a74490
Simplify error checking logic
JonathanCrd Sep 18, 2024
5df4002
Add test for base64 claims
JonathanCrd Sep 23, 2024
2451396
Override Process function to handle the first CAE Challenge after a s…
JonathanCrd Sep 25, 2024
1f9a73c
Add tests
JonathanCrd Sep 25, 2024
daa03ef
Separate credential and client transports and assert for a 401.
JonathanCrd Sep 25, 2024
f454e4d
Nest rety inside challenge if block
JonathanCrd Sep 27, 2024
5b09202
Merge remote-tracking branch 'upstream/main' into Enable-CAE-for-KeyV…
JonathanCrd Sep 30, 2024
de6d54d
Add test for claims in token
JonathanCrd Oct 1, 2024
72d98ef
Fix CI by removing extra test case parameter
JonathanCrd Oct 1, 2024
46909fe
Nit changes to tests
JonathanCrd Oct 3, 2024
15a5ab7
Simplify tests
JonathanCrd Oct 3, 2024
ee196ec
removing unnecessary mock responses
JonathanCrd Oct 3, 2024
0c33973
Refactor tests to test CAE in all projects
JonathanCrd Oct 7, 2024
a0de67f
Make tests non parallelizable
JonathanCrd Oct 7, 2024
0ffee52
Add setup method to CAE tests
JonathanCrd Oct 8, 2024
f03fe3b
Test for tokens obtained from cae challenges
JonathanCrd Oct 10, 2024
a9657ef
Merge remote-tracking branch 'upstream/main' into Enable-CAE-for-KeyV…
JonathanCrd Oct 10, 2024
8bd6cfc
Fix test / CI
JonathanCrd Oct 10, 2024
d3e535d
Merge remote-tracking branch 'upstream/main' into Enable-CAE-for-KeyV…
JonathanCrd Oct 10, 2024
8544ff6
Update dependency for System.ClientModel
JonathanCrd Oct 10, 2024
6ac9c91
Apply suggestions
JonathanCrd Oct 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Expand Up @@ -3,6 +3,7 @@
## 4.7.0-beta.1 (Unreleased)

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

### Breaking Changes

Expand Down
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
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,221 @@ public async Task ReauthenticatesWhenTenantChanged()
Assert.AreEqual("secret-value", response.Value.Value);
}

[Test]
JonathanCrd marked this conversation as resolved.
Show resolved Hide resolved
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));
}

[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),
JonathanCrd marked this conversation as resolved.
Show resolved Hide resolved
new SecretClientOptions()
{
Transport = keyVaultTransport,
});

Response<KeyVaultSecret> response = client.GetSecret("test-secret");
Assert.AreEqual(200, response.GetRawResponse().Status);
Assert.AreEqual("secret-value", response.Value.Value);
}

[Test]
public void ThrowsWithTwoConsecutiveCaeChallenges()
{
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)
JonathanCrd marked this conversation as resolved.
Show resolved Hide resolved
{
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"
JonathanCrd marked this conversation as resolved.
Show resolved Hide resolved
}
"""),

new MockResponse(200)
.WithJson("""
{
"token_type": "Bearer",
"expires_in": 3599,
"resource": "https://vault.azure.net",
"access_token": "NzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3"
}
"""),

new MockResponse(200)
.WithJson("""
{
"token_type": "Bearer",
"expires_in": 3599,
"resource": "https://vault.azure.net",
"access_token": GUID.NewGuid().ToString()
}
"""),
JonathanCrd marked this conversation as resolved.
Show resolved Hide resolved
});

SecretClient client = new(
VaultUri,
new MockCredential(credentialTransport),
new SecretClientOptions()
{
Transport = keyVaultTransport,
});

try
{
client.GetSecret("test-secret");
}
catch (RequestFailedException ex)
{
Assert.AreEqual(401, ex.Status);
}
catch (Exception ex)
{
Assert.Fail($"Expected RequestFailedException, but got {ex.GetType()}");
}
JonathanCrd marked this conversation as resolved.
Show resolved Hide resolved
}

[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)
JonathanCrd marked this conversation as resolved.
Show resolved Hide resolved
{
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;
chlowell marked this conversation as resolved.
Show resolved Hide resolved
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<TokenRequestContext, CancellationToken, AccessToken> 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<TokenRequestContext, CancellationToken, ValueTask<AccessToken>> _getTokenAsyncHandler;
private readonly Func<TokenRequestContext, CancellationToken, AccessToken> _getTokenHandler;

public override ValueTask<AccessToken> 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";
Expand Down
Loading