diff --git a/src/Auth0.ManagementApi/Clients/IKeysClient.cs b/src/Auth0.ManagementApi/Clients/IKeysClient.cs index 772c62d1d..961b718ab 100644 --- a/src/Auth0.ManagementApi/Clients/IKeysClient.cs +++ b/src/Auth0.ManagementApi/Clients/IKeysClient.cs @@ -4,6 +4,7 @@ namespace Auth0.ManagementApi.Clients using System.Threading; using System.Threading.Tasks; using Models.Keys; + using Paging; public interface IKeysClient { @@ -36,5 +37,50 @@ public interface IKeysClient /// The cancellation token to cancel operation. /// The revoked key's cert and kid. Task RevokeSigningKeyAsync(string kid, CancellationToken cancellationToken = default); + + /// + /// Retrieve details of all the encryption keys associated with your tenant. + /// + /// + /// The cancellation token to cancel operation. + /// Retrieve details of all the encryption keys associated with your tenant. . + Task> GetAllEncryptionKeysAsync(PaginationInfo pagination, CancellationToken cancellationToken = default); + + /// + /// Create the new, pre-activated encryption key, without the key material. + /// + /// + /// The cancellation token to cancel operation. + /// Newly created pre-activated encryption key . + Task CreateEncryptionKeyAsync(EncryptionKeyCreateRequest request, CancellationToken cancellationToken = default); + + /// + /// Retrieve details of the encryption key with the given ID. + /// + /// + /// The cancellation token to cancel operation. + /// Retrieve details of the encryption key associated with the id. . + Task GetEncryptionKeyAsync(EncryptionKeyGetRequest request, CancellationToken cancellationToken = default); + + /// + /// Delete the custom provided encryption key with the given ID and move back to using native encryption key. + /// + /// Encryption key ID + /// The cancellation token to cancel operation. + Task DeleteEncryptionKeyAsync(string kid, CancellationToken cancellationToken = default); + + /// + /// Import wrapped key material and activate encryption key. + /// + /// + /// The cancellation token to cancel operation. + Task ImportEncryptionKeyAsync(EncryptionKeyImportRequest request, CancellationToken cancellationToken = default); + + /// + /// Create the public wrapping key to wrap your own encryption key material. + /// + /// + /// The cancellation token to cancel operation. + Task CreatePublicWrappingKeyAsync(WrappingKeyCreateRequest request, CancellationToken cancellationToken = default); } } diff --git a/src/Auth0.ManagementApi/Clients/KeysClient.cs b/src/Auth0.ManagementApi/Clients/KeysClient.cs index 965260a02..455c12160 100644 --- a/src/Auth0.ManagementApi/Clients/KeysClient.cs +++ b/src/Auth0.ManagementApi/Clients/KeysClient.cs @@ -5,6 +5,9 @@ using System.Threading; using System.Threading.Tasks; using Auth0.ManagementApi.Models.Keys; +using Auth0.ManagementApi.Paging; +using Newtonsoft.Json; +using EncryptionKey = Auth0.ManagementApi.Models.Keys.EncryptionKey; namespace Auth0.ManagementApi.Clients { @@ -13,6 +16,7 @@ namespace Auth0.ManagementApi.Clients /// public class KeysClient : BaseClient, IKeysClient { + readonly JsonConverter[] converters = new JsonConverter[] { new PagedListConverter("keys") }; /// /// Initializes a new instance of the class. /// @@ -65,5 +69,104 @@ public Task RevokeSigningKeyAsync(string kid, Cancella { return Connection.SendAsync(HttpMethod.Put, BuildUri($"keys/signing/{EncodePath(kid)}/revoke"), null, DefaultHeaders, cancellationToken: cancellationToken); } + + /// + public Task> GetAllEncryptionKeysAsync( + PaginationInfo pagination, CancellationToken cancellationToken = default) + { + var queryStrings = new Dictionary(); + + if (pagination != null) + { + queryStrings["page"] = pagination.PageNo.ToString(); + queryStrings["per_page"] = pagination.PerPage.ToString(); + queryStrings["include_totals"] = pagination.IncludeTotals.ToString().ToLower(); + } + + return Connection.GetAsync>( + BuildUri("keys/encryption", queryStrings), DefaultHeaders, converters, cancellationToken); + } + + /// + public Task CreateEncryptionKeyAsync( + EncryptionKeyCreateRequest request, CancellationToken cancellationToken = default) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + + if (string.IsNullOrEmpty(request.Type)) + throw new ArgumentNullException(nameof(request.Type)); + + return Connection.SendAsync( + HttpMethod.Post, + BuildUri("keys/encryption"), + request, + DefaultHeaders, + cancellationToken: cancellationToken); + } + + /// + public Task GetEncryptionKeyAsync( + EncryptionKeyGetRequest request, CancellationToken cancellationToken = default) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + + if (string.IsNullOrEmpty(request.Kid)) + throw new ArgumentNullException(nameof(request.Kid)); + + return Connection.GetAsync( + BuildUri($"keys/encryption/{EncodePath(request.Kid)}"), DefaultHeaders, null, cancellationToken); + } + + /// + public Task DeleteEncryptionKeyAsync(string kid, CancellationToken cancellationToken = default) + { + if (kid == null) + throw new ArgumentNullException(nameof(kid)); + + return Connection.SendAsync( + HttpMethod.Delete, + BuildUri($"keys/encryption/{EncodePath(kid)}"), + null, + DefaultHeaders, + cancellationToken: cancellationToken); + } + + /// + public Task ImportEncryptionKeyAsync( + EncryptionKeyImportRequest request, CancellationToken cancellationToken = default) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + + if (string.IsNullOrEmpty(request.Kid)) + throw new ArgumentNullException(nameof(request.Kid)); + + return Connection.SendAsync( + HttpMethod.Post, + BuildUri($"keys/encryption/{EncodePath(request.Kid)}"), + request, + DefaultHeaders, + cancellationToken: cancellationToken); + } + + /// + public Task CreatePublicWrappingKeyAsync( + WrappingKeyCreateRequest request, CancellationToken cancellationToken = default) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + + if (string.IsNullOrEmpty(request.Kid)) + throw new ArgumentNullException(nameof(request.Kid)); + + return Connection.SendAsync( + HttpMethod.Post, + BuildUri($"keys/encryption/{EncodePath(request.Kid)}/wrapping-key"), + body: null, + headers: DefaultHeaders, + cancellationToken: cancellationToken); + } } } diff --git a/src/Auth0.ManagementApi/Models/Keys/EncryptionKey.cs b/src/Auth0.ManagementApi/Models/Keys/EncryptionKey.cs new file mode 100644 index 000000000..a7f18a9c4 --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Keys/EncryptionKey.cs @@ -0,0 +1,53 @@ +using System; +using System.Net.Security; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace Auth0.ManagementApi.Models.Keys +{ + /// + /// Represents and Encryption Key + /// + public class EncryptionKey + { + /// + /// Key ID + /// + [JsonProperty("kid")] + public string Kid { get; set; } + + /// + [JsonProperty("type")] + [JsonConverter(typeof(StringEnumConverter))] + public EncryptionKeyType Type { get; set; } + + /// + [JsonProperty("state")] + [JsonConverter(typeof(StringEnumConverter))] + public EncryptionKeyState State { get; set; } + + /// + /// Key creation timestamp + /// + [JsonProperty("created_at")] + public DateTime CreatedAt { get; set; } + + /// + /// Key update timestamp + /// + [JsonProperty("updated_at")] + public DateTime UpdatedAt { get; set; } + + /// + /// ID of the parent wrapping key. + /// + [JsonProperty("parent_kid")] + public string ParentKid { get; set; } + + /// + /// Public key in PEM format + /// + [JsonProperty("public_key")] + public string PublicKey { get; set; } + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/Keys/EncryptionKeyCreateRequest.cs b/src/Auth0.ManagementApi/Models/Keys/EncryptionKeyCreateRequest.cs new file mode 100644 index 000000000..2757f3a02 --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Keys/EncryptionKeyCreateRequest.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; + +namespace Auth0.ManagementApi.Models.Keys +{ + /// + /// Contains information required for creating an encryption key. + /// + public class EncryptionKeyCreateRequest + { + /// + /// Type of the encryption key to be created. + /// Possible values: [customer-provided-root-key, tenant-encryption-key] + /// + [JsonProperty("type")] + public string Type { get; set; } + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/Keys/EncryptionKeyGetRequest.cs b/src/Auth0.ManagementApi/Models/Keys/EncryptionKeyGetRequest.cs new file mode 100644 index 000000000..08f2ce350 --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Keys/EncryptionKeyGetRequest.cs @@ -0,0 +1,13 @@ +namespace Auth0.ManagementApi.Models.Keys +{ + /// + /// Contains information required for getting an encryption key. + /// + public class EncryptionKeyGetRequest + { + /// + /// Encryption key ID. + /// + public string Kid { get; set; } + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/Keys/EncryptionKeyImportRequest.cs b/src/Auth0.ManagementApi/Models/Keys/EncryptionKeyImportRequest.cs new file mode 100644 index 000000000..b09f53862 --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Keys/EncryptionKeyImportRequest.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; + +namespace Auth0.ManagementApi.Models.Keys +{ + /// + /// Contains information required for importing an encryption key. + /// + public class EncryptionKeyImportRequest + { + /// + /// Encryption key ID + /// + public string Kid { get; set; } + + /// + /// Base64 encoded ciphertext of key material wrapped by public wrapping key. + /// + [JsonProperty("wrapped_key")] + public string WrappedKey { get; set; } + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/Keys/EncryptionKeyState.cs b/src/Auth0.ManagementApi/Models/Keys/EncryptionKeyState.cs new file mode 100644 index 000000000..a46468a89 --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Keys/EncryptionKeyState.cs @@ -0,0 +1,22 @@ +using System.Runtime.Serialization; + +namespace Auth0.ManagementApi.Models.Keys +{ + /// + /// Encryption Key State + /// + public enum EncryptionKeyState + { + [EnumMember(Value = "pre-activation")] + PreActivation, + + [EnumMember(Value = "active")] + Active, + + [EnumMember(Value = "deactivated")] + Deactivated, + + [EnumMember(Value = "destroyed")] + Destroyed, + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/Keys/EncryptionKeyType.cs b/src/Auth0.ManagementApi/Models/Keys/EncryptionKeyType.cs new file mode 100644 index 000000000..7fa78ee05 --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Keys/EncryptionKeyType.cs @@ -0,0 +1,22 @@ +using System.Runtime.Serialization; + +namespace Auth0.ManagementApi.Models.Keys +{ + /// + /// Encryption Key Type + /// + public enum EncryptionKeyType + { + [EnumMember(Value = "customer-provided-root-key")] + CustomerProvidedRootKey, + + [EnumMember(Value = "environment-root-key")] + EnvironmentRootKey, + + [EnumMember(Value = "tenant-master-key")] + TenantMasterKey, + + [EnumMember(Value = "tenant-encryption-key")] + TenantEncryptionKey, + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/Keys/WrappingKey.cs b/src/Auth0.ManagementApi/Models/Keys/WrappingKey.cs new file mode 100644 index 000000000..f4a6f3869 --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Keys/WrappingKey.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace Auth0.ManagementApi.Models.Keys +{ + /// + /// Represents the WrappingKey + /// + public class WrappingKey + { + /// + /// Public wrapping key in PEM format + /// + [JsonProperty("public_key")] + public string PublicKey { get; set; } + + /// + /// Encryption Algorithm that shall be used to wrap your key material + /// + [JsonProperty("algorithm")] + public string Algorithm { get; set; } + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/Keys/WrappingKeyCreateRequest.cs b/src/Auth0.ManagementApi/Models/Keys/WrappingKeyCreateRequest.cs new file mode 100644 index 000000000..bd4ab049c --- /dev/null +++ b/src/Auth0.ManagementApi/Models/Keys/WrappingKeyCreateRequest.cs @@ -0,0 +1,13 @@ +namespace Auth0.ManagementApi.Models.Keys +{ + /// + /// Contains information required for creating a wrapping key. + /// + public class WrappingKeyCreateRequest + { + /// + /// Encryption key ID + /// + public string Kid { get; set; } + } +} \ No newline at end of file diff --git a/tests/Auth0.AuthenticationApi.IntegrationTests/Testing/ManagementTestBaseUtils.cs b/tests/Auth0.AuthenticationApi.IntegrationTests/Testing/ManagementTestBaseUtils.cs index ff2d26bf5..452f36797 100644 --- a/tests/Auth0.AuthenticationApi.IntegrationTests/Testing/ManagementTestBaseUtils.cs +++ b/tests/Auth0.AuthenticationApi.IntegrationTests/Testing/ManagementTestBaseUtils.cs @@ -22,7 +22,8 @@ public static async Task CleanupAsync(ManagementApiClient client, CleanUpType ty new UsersCleanUpStrategy(client), new RulesCleanUpStrategy(client), new LogStreamsCleanUpStrategy(client), - new RolesCleanUpStrategy(client) + new RolesCleanUpStrategy(client), + new EncryptionKeysCleanupStrategy(client) }; var cleanUpStrategy = strategies.Single(s => s.Type == type); diff --git a/tests/Auth0.IntegrationTests.Shared/CleanUp/CleanUpType.cs b/tests/Auth0.IntegrationTests.Shared/CleanUp/CleanUpType.cs index 5085301fc..a2b0ec835 100644 --- a/tests/Auth0.IntegrationTests.Shared/CleanUp/CleanUpType.cs +++ b/tests/Auth0.IntegrationTests.Shared/CleanUp/CleanUpType.cs @@ -12,6 +12,7 @@ public enum CleanUpType Users, Roles, Rules, - LogStreams + LogStreams, + EncryptionKeys } } \ No newline at end of file diff --git a/tests/Auth0.IntegrationTests.Shared/CleanUp/EncryptionKeysCleanUpStrategy.cs b/tests/Auth0.IntegrationTests.Shared/CleanUp/EncryptionKeysCleanUpStrategy.cs new file mode 100644 index 000000000..a30733648 --- /dev/null +++ b/tests/Auth0.IntegrationTests.Shared/CleanUp/EncryptionKeysCleanUpStrategy.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading.Tasks; +using Auth0.ManagementApi; +using Auth0.ManagementApi.Models.Actions; +using Auth0.ManagementApi.Paging; +using static System.Collections.Specialized.BitVector32; + +namespace Auth0.IntegrationTests.Shared.CleanUp +{ + public class EncryptionKeysCleanupStrategy : CleanUpStrategy + { + public EncryptionKeysCleanupStrategy(ManagementApiClient apiClient) : base(CleanUpType.EncryptionKeys, apiClient) + { + + } + + public override async Task Run(string id) + { + System.Diagnostics.Debug.WriteLine("Running EncryptionKeysCleanupStrategy"); + await ApiClient.Keys.DeleteEncryptionKeyAsync(id); + } + } +} \ No newline at end of file diff --git a/tests/Auth0.ManagementApi.IntegrationTests/Auth0.ManagementApi.IntegrationTests.csproj b/tests/Auth0.ManagementApi.IntegrationTests/Auth0.ManagementApi.IntegrationTests.csproj index eddfbb574..a3ab8b7eb 100644 --- a/tests/Auth0.ManagementApi.IntegrationTests/Auth0.ManagementApi.IntegrationTests.csproj +++ b/tests/Auth0.ManagementApi.IntegrationTests/Auth0.ManagementApi.IntegrationTests.csproj @@ -23,6 +23,9 @@ Always + + Always + diff --git a/tests/Auth0.ManagementApi.IntegrationTests/Data/ImportEncryptionKeyResponse.json b/tests/Auth0.ManagementApi.IntegrationTests/Data/ImportEncryptionKeyResponse.json new file mode 100644 index 000000000..dd4020298 --- /dev/null +++ b/tests/Auth0.ManagementApi.IntegrationTests/Data/ImportEncryptionKeyResponse.json @@ -0,0 +1,9 @@ +{ + "kid": "093e36a8-88a1-4c34-8202-e454553ee2dc", + "type": "customer-provided-root-key", + "state": "destroyed", + "created_at": "2024-11-04T15:33:27.535Z", + "updated_at": "2024-11-04T15:33:32.274Z", + "parent_kid": "a20128c5-9bf5-4209-8c43-b6dfcee60e9b", + "public_key": "Random-PUBLIC-KEY" +} \ No newline at end of file diff --git a/tests/Auth0.ManagementApi.IntegrationTests/KeysTests.cs b/tests/Auth0.ManagementApi.IntegrationTests/KeysTests.cs index 29d0803dd..8a5ecd291 100644 --- a/tests/Auth0.ManagementApi.IntegrationTests/KeysTests.cs +++ b/tests/Auth0.ManagementApi.IntegrationTests/KeysTests.cs @@ -1,11 +1,30 @@ +using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; +using Auth0.IntegrationTests.Shared.CleanUp; +using Auth0.ManagementApi.Clients; +using Auth0.ManagementApi.IntegrationTests.Testing; +using Auth0.ManagementApi.Models.Keys; +using Auth0.ManagementApi.Paging; using FluentAssertions; +using Newtonsoft.Json; using Xunit; namespace Auth0.ManagementApi.IntegrationTests { - public class KeysTestsFixture : TestBaseFixture {} + public class KeysTestsFixture : TestBaseFixture + { + public override async Task DisposeAsync() + { + foreach (KeyValuePair> entry in identifiers) + { + await ManagementTestBaseUtils.CleanupAsync(ApiClient, entry.Key, entry.Value); + } + + ApiClient.Dispose(); + } + } public class KeysTests : IClassFixture { @@ -77,5 +96,64 @@ public async Task Test_keys_can_be_revoked_by_kid() // Assert revoked.Kid.Should().Be(previousKeyId); } + + [Fact] + public async Task Test_encryption_keys_crud_sequence() + { + var encryptionKeysCreateRequest = new EncryptionKeyCreateRequest() + { + Type = "customer-provided-root-key" + }; + // Create a new Encryption Key for testing purpose + var encryptionKey = await fixture.ApiClient.Keys.CreateEncryptionKeyAsync(encryptionKeysCreateRequest); + fixture.TrackIdentifier(CleanUpType.EncryptionKeys, encryptionKey.Kid); + + encryptionKey.Type.Should().Be(EncryptionKeyType.CustomerProvidedRootKey); + encryptionKey.State.Should().NotBeNull(); + + // Get all the existing encryption keys + var allEncryptionKeys = + await fixture.ApiClient.Keys.GetAllEncryptionKeysAsync(new PaginationInfo()); + allEncryptionKeys.Count.Should().BeGreaterThan(0); + + // Get the newly created encryption key by its kid + var encryptionKeyById = await fixture.ApiClient.Keys.GetEncryptionKeyAsync(new EncryptionKeyGetRequest() + { + Kid = encryptionKey.Kid + }); + encryptionKeyById.Should().BeEquivalentTo(encryptionKey); + + // Create Public key wrapping + var wrapping = await fixture.ApiClient.Keys.CreatePublicWrappingKeyAsync(new WrappingKeyCreateRequest() + { + Kid = encryptionKey.Kid + }); + + wrapping.Should().NotBeNull(); + wrapping.Algorithm.Should().NotBeNull(); + wrapping.PublicKey.Should().NotBeNull(); + + // Delete the encryption key + await fixture.ApiClient.Keys.DeleteEncryptionKeyAsync(encryptionKey.Kid); + fixture.UnTrackIdentifier(CleanUpType.EncryptionKeys, encryptionKey.Kid); + var allEncryptionKeysAfterCleanup = + await fixture.ApiClient.Keys.GetAllEncryptionKeysAsync(new PaginationInfo()); + allEncryptionKeysAfterCleanup.Should().NotContain(encryptionKey); + } + + [Fact] + public async void Test_import_encrypted_keys() + { + var sampleImportEncryptionKeyResponse = + await File.ReadAllTextAsync("./Data/ImportEncryptionKeyResponse.json"); + var httpClientManagementConnection = new HttpClientManagementConnection(); + var importedKeys = + httpClientManagementConnection.DeserializeContent(sampleImportEncryptionKeyResponse, null); + importedKeys.PublicKey.Should().Be("Random-PUBLIC-KEY"); + importedKeys.Kid.Should().Be("093e36a8-88a1-4c34-8202-e454553ee2dc"); + importedKeys.State.Should().Be(EncryptionKeyState.Destroyed); + importedKeys.Type.Should().Be(EncryptionKeyType.CustomerProvidedRootKey); + importedKeys.ParentKid.Should().Be("a20128c5-9bf5-4209-8c43-b6dfcee60e9b"); + } } } diff --git a/tests/Auth0.ManagementApi.IntegrationTests/Testing/ManagementTestBaseUtils.cs b/tests/Auth0.ManagementApi.IntegrationTests/Testing/ManagementTestBaseUtils.cs index 16935c504..4e18d514f 100644 --- a/tests/Auth0.ManagementApi.IntegrationTests/Testing/ManagementTestBaseUtils.cs +++ b/tests/Auth0.ManagementApi.IntegrationTests/Testing/ManagementTestBaseUtils.cs @@ -21,7 +21,8 @@ public static async Task CleanupAsync(ManagementApiClient client, CleanUpType ty new UsersCleanUpStrategy(client), new RulesCleanUpStrategy(client), new LogStreamsCleanUpStrategy(client), - new RolesCleanUpStrategy(client) + new RolesCleanUpStrategy(client), + new EncryptionKeysCleanupStrategy(client) }; var cleanUpStrategy = strategies.Single(s => s.Type == type);