Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions src/Auth0.Core/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ internal static Uri BuildUri(string baseUrl, string resource, IDictionary<string

return new Uri(resource, UriKind.RelativeOrAbsolute);
}
internal static Uri BuildUri(string baseUrl, string resource, IDictionary<string, string>? urlSegments, IList<Tuple<string, string>> queryStringsTuple, bool includeEmptyParameters = false)
internal static Uri BuildUri(string baseUrl, string resource, IDictionary<string, string>? urlSegments, IList<Tuple<string, string?>> queryStringsTuple, bool includeEmptyParameters = false)
{
resource = ReplaceUrlSegments(resource, urlSegments);

Expand Down Expand Up @@ -87,7 +87,7 @@ internal static string CombineUriParts(params string[]? uriParts)
}
return uri;
}
internal static string AddQueryString(IList<Tuple<string, string>> queryStrings, bool includeEmptyParameters)
internal static string AddQueryString(IList<Tuple<string, string?>> queryStrings, bool includeEmptyParameters)
{
var sb = new StringBuilder();
// Add the query strings
Expand Down
2 changes: 1 addition & 1 deletion src/Auth0.ManagementApi/Clients/BaseClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ protected Uri BuildUri(string resource, IDictionary<string, string> queryStrings
return Utils.BuildUri(BaseUri.AbsoluteUri, resource, null, queryStrings);
}

protected Uri BuildUri(string resource, IList<Tuple<string, string>> queryStrings)
protected Uri BuildUri(string resource, IList<Tuple<string, string?>> queryStrings)
{
return Utils.BuildUri(BaseUri.AbsoluteUri, resource, null, queryStrings);
}
Expand Down
34 changes: 34 additions & 0 deletions src/Auth0.ManagementApi/Clients/ClientsClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public class ClientsClient : BaseClient, IClientsClient
{
private readonly JsonConverter[] converters = [new PagedListConverter<Client>("clients")];
private readonly JsonConverter[] checkpointConverters = [new CheckpointPagedListConverter<Client>("clients")];
private readonly JsonConverter[] enabledClientsConverters = [new CheckpointPagedListConverter<Connection>("connections")];

/// <summary>
/// Initializes a new instance of <see cref="ClientsClient"/>.
Expand Down Expand Up @@ -197,6 +198,39 @@ public Task DeleteCredentialAsync(string clientId, string credentialId, Cancella
return Connection.SendAsync<object>(HttpMethod.Delete, BuildUri($"clients/{EncodePath(clientId)}/credentials/{EncodePath(credentialId)}"), null, DefaultHeaders, cancellationToken: cancellationToken);
}

/// <inheritdoc cref="IClientsClient.GetEnabledConnectionsForClientAsync(string,Auth0.ManagementApi.Models.EnabledConnectionsForClientGetRequest,Auth0.ManagementApi.Paging.CheckpointPaginationInfo,System.Threading.CancellationToken)"/>
public Task<ICheckpointPagedList<Connection>> GetEnabledConnectionsForClientAsync(
string id,
EnabledConnectionsForClientGetRequest? request,
CheckpointPaginationInfo? pagination, CancellationToken cancellationToken = default)
{
id.ThrowIfNull();

var queryStrings = new List<Tuple<string, string?>>()
{
new("fields", request?.Fields),
new("include_fields", request?.IncludeFields?.ToString().ToLower())
};

if (request?.Strategy != null)
{
queryStrings.AddRange(
request.Strategy.Select(eachStrategy => new Tuple<string, string?>("strategy", eachStrategy)));
}

if (pagination != null)
{
queryStrings.Add(new Tuple<string, string?>("from", pagination.From));
queryStrings.Add(new Tuple<string, string?>("take", pagination.Take.ToString()));
}

return Connection.GetAsync<ICheckpointPagedList<Connection>>(
BuildUri($"clients/{EncodePath(id)}/connections", queryStrings),
DefaultHeaders,
enabledClientsConverters,
cancellationToken);
}

/// <summary>
/// Builds common query strings for client requests.
/// </summary>
Expand Down
10 changes: 10 additions & 0 deletions src/Auth0.ManagementApi/Clients/IClientsClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,14 @@ public interface IClientsClient
/// <param name="cancellationToken">The cancellation token to cancel operation.</param>
/// <returns>A <see cref="Task"/> that represents the asynchronous delete operation.</returns>
Task DeleteCredentialAsync(string clientId, string credentialId, CancellationToken cancellationToken = default);

/// <summary>
/// Retrieve all connections that are enabled for the specified client.
/// </summary>
/// <param name="id">ID of the client for which to retrieve enabled connections.</param>
/// <param name="request">Specifies criteria to use when querying clients.</param>
/// <param name="pagination">Specifies <see cref="CheckpointPaginationInfo"/> to use in requesting checkpoint-paginated results.</param>
/// <param name="cancellationToken">The cancellation token to cancel operation.</param>
/// <returns>An <see cref="ICheckpointPagedList{Connection}"/> containing the enabled connections.</returns>
public Task<ICheckpointPagedList<Connection>> GetEnabledConnectionsForClientAsync(string id, EnabledConnectionsForClientGetRequest? request, CheckpointPaginationInfo? pagination, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Newtonsoft.Json;

namespace Auth0.ManagementApi.Models;

/// <summary>
/// Request to get enabled connections for a client
/// </summary>
public class EnabledConnectionsForClientGetRequest
{
/// <summary>
/// Provide strategies to only retrieve connections with such strategies
/// </summary>
[JsonProperty("strategy")]
public string[]? Strategy { get; set; }

/// <summary>
/// A comma separated list of fields to include or exclude (depending on include_fields) from the result, empty to retrieve all fields
/// </summary>
[JsonProperty("fields")]
public string? Fields { get; set; }

/// <summary>
/// True if the fields specified are to be included in the result, false otherwise (defaults to true)
/// </summary>
[JsonProperty("include_fields")]
public bool? IncludeFields { get; set; }
}
130 changes: 110 additions & 20 deletions tests/Auth0.ManagementApi.IntegrationTests/ClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,24 @@
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using Auth0.Core.Exceptions;
using Auth0.IntegrationTests.Shared.CleanUp;
using Auth0.ManagementApi.Clients;
using Auth0.ManagementApi.IntegrationTests.Testing;
using Auth0.ManagementApi.Models;
using Auth0.ManagementApi.Paging;
using Auth0.Tests.Shared;
using FluentAssertions;
using Moq;
using Newtonsoft.Json;
using Xunit;

namespace Auth0.ManagementApi.IntegrationTests;

public class ClientTestsFixture : TestBaseFixture
{

public override async Task DisposeAsync()
{
foreach (KeyValuePair<CleanUpType, IList<string>> entry in identifiers)
Expand Down Expand Up @@ -50,7 +53,7 @@ public async Task Test_client_crud_sequence()
LogoutInitiators.AccountDeleted,
LogoutInitiators.EmailIdentifierChanged
};

// Add a new client
var newClientRequest = new ClientCreateRequest
{
Expand Down Expand Up @@ -79,7 +82,7 @@ public async Task Test_client_crud_sequence()
Mode = LogoutInitiatorModes.Custom,
SelectedInitiators = selectedInitiators
},
BackchannelLogoutUrls = new [] { "https://create.com/logout" }
BackchannelLogoutUrls = new[] { "https://create.com/logout" }
},
DefaultOrganization = new DefaultOrganization()
{
Expand Down Expand Up @@ -127,7 +130,8 @@ public async Task Test_client_crud_sequence()
newClientResponse.OrganizationRequireBehavior.Should().Be(OrganizationRequireBehavior.PreLoginPrompt);
newClientResponse.OidcLogout.BackchannelLogoutUrls[0].Should().Be("https://create.com/logout");
newClientResponse.OidcLogout.BackchannelLogoutInitiators.Mode.Should().Be(LogoutInitiatorModes.Custom);
newClientResponse.OidcLogout.BackchannelLogoutInitiators.SelectedInitiators.Should().BeEquivalentTo(selectedInitiators);
newClientResponse.OidcLogout.BackchannelLogoutInitiators.SelectedInitiators.Should()
.BeEquivalentTo(selectedInitiators);
newClientResponse.DefaultOrganization.OrganizationId.Should().Be(existingOrganizationId);
newClientResponse.RequirePushedAuthorizationRequests.Should().BeTrue();
newClientResponse.SignedRequestObject.Should().NotBeNull();
Expand Down Expand Up @@ -175,8 +179,9 @@ public async Task Test_client_crud_sequence()
}
}
};

var updateClientResponse = await fixture.ApiClient.Clients.UpdateAsync(newClientResponse.ClientId, updateClientRequest);

var updateClientResponse =
await fixture.ApiClient.Clients.UpdateAsync(newClientResponse.ClientId, updateClientRequest);
updateClientResponse.Should().NotBeNull();
updateClientResponse.Name.Should().Be(updateClientRequest.Name);
updateClientResponse.TokenEndpointAuthMethod.Should().Be(TokenEndpointAuthMethod.ClientSecretPost);
Expand All @@ -191,7 +196,8 @@ public async Task Test_client_crud_sequence()
updateClientResponse.OrganizationRequireBehavior.Should().Be(OrganizationRequireBehavior.NoPrompt);
updateClientResponse.OidcLogout.BackchannelLogoutUrls[0].Should().Be("https://create.com/logout");
updateClientResponse.OidcLogout.BackchannelLogoutInitiators.Mode.Should().Be(LogoutInitiatorModes.Custom);
updateClientResponse.OidcLogout.BackchannelLogoutInitiators.SelectedInitiators.Should().BeEquivalentTo(selectedInitiators);
updateClientResponse.OidcLogout.BackchannelLogoutInitiators.SelectedInitiators.Should()
.BeEquivalentTo(selectedInitiators);
updateClientResponse.DefaultOrganization.OrganizationId.Should().Be(existingOrganizationId);
updateClientResponse.DefaultOrganization.Flows.Should().HaveCount(1);
updateClientResponse.DefaultOrganization.Flows.First().Should().Be(Flows.ClientCredentials);
Expand Down Expand Up @@ -261,7 +267,8 @@ public async Task Test_when_paging_not_specified_does_not_include_totals()
public async Task Test_paging_does_not_include_totals()
{
// Act
var clients = await fixture.ApiClient.Clients.GetAllAsync(new GetClientsRequest(), new PaginationInfo(0, 50, false));
var clients =
await fixture.ApiClient.Clients.GetAllAsync(new GetClientsRequest(), new PaginationInfo(0, 50, false));

// Assert
Assert.Null(clients.Paging);
Expand All @@ -271,7 +278,8 @@ public async Task Test_paging_does_not_include_totals()
public async Task Test_paging_includes_totals()
{
// Act
var clients = await fixture.ApiClient.Clients.GetAllAsync(new GetClientsRequest(), new PaginationInfo(0, 50, true));
var clients =
await fixture.ApiClient.Clients.GetAllAsync(new GetClientsRequest(), new PaginationInfo(0, 50, true));

// Assert
Assert.NotNull(clients.Paging);
Expand Down Expand Up @@ -309,7 +317,7 @@ public async Task Test_app_type_works_correctly()
// Rotate the secret
var connections = await fixture.ApiClient.Clients.GetAllAsync(new GetClientsRequest
{
AppType = new[] {ClientApplicationType.Native}
AppType = new[] { ClientApplicationType.Native }
}, new PaginationInfo());

// Assert
Expand Down Expand Up @@ -353,7 +361,8 @@ public async Task Test_crud_credentials()
newClient.ClientAuthenticationMethods.PrivateKeyJwt.Credentials[0].Id.Should().NotBeNull();

var allCredentialsForClient = await fixture.ApiClient.Clients.GetAllCredentialsAsync(newClient.ClientId);
var credential1 = await fixture.ApiClient.Clients.GetCredentialAsync(newClient.ClientId, newClient.ClientAuthenticationMethods.PrivateKeyJwt.Credentials[0].Id);
var credential1 = await fixture.ApiClient.Clients.GetCredentialAsync(newClient.ClientId,
newClient.ClientAuthenticationMethods.PrivateKeyJwt.Credentials[0].Id);

allCredentialsForClient.Should().NotBeNull();
allCredentialsForClient.Should().NotBeEmpty();
Expand All @@ -363,22 +372,24 @@ public async Task Test_crud_credentials()
credential1.Name.Should().Be("Test Credential 1");
credential1.CredentialType.Should().Be("public_key");

var newCredential = await fixture.ApiClient.Clients.CreateCredentialAsync(newClient.ClientId, new ClientCredentialCreateRequest
{
CredentialType = "public_key",
Name = "Test Credential 2",
Pem = RsaTestUtils.ExportPublicKey(new RSACryptoServiceProvider(2048)),
});
var newCredential = await fixture.ApiClient.Clients.CreateCredentialAsync(newClient.ClientId,
new ClientCredentialCreateRequest
{
CredentialType = "public_key",
Name = "Test Credential 2",
Pem = RsaTestUtils.ExportPublicKey(new RSACryptoServiceProvider(2048)),
});


newCredential.ExpiresAt.Should().BeNull();

var newExpiry = DateTime.UtcNow.AddDays(2);
var newExpiry = DateTime.UtcNow.AddDays(2);
var newExpiryWithoutMilliSeconds = new DateTime(
newExpiry.Ticks - (newExpiry.Ticks % TimeSpan.TicksPerSecond),
newExpiry.Kind
);
var newCredential2 = await fixture.ApiClient.Clients.UpdateCredentialAsync(newClient.ClientId, newCredential.Id, new ClientCredentialUpdateRequest { ExpiresAt = newExpiryWithoutMilliSeconds });
var newCredential2 = await fixture.ApiClient.Clients.UpdateCredentialAsync(newClient.ClientId, newCredential.Id,
new ClientCredentialUpdateRequest { ExpiresAt = newExpiryWithoutMilliSeconds });

newCredential2.ExpiresAt?.ToString("o").Should().Be(newExpiryWithoutMilliSeconds.ToString("o"));

Expand Down Expand Up @@ -428,7 +439,6 @@ public async Task Test_crud_credentials()
{
Credentials = new List<CredentialId>
{

}
}
}
Expand All @@ -442,4 +452,84 @@ public async Task Test_crud_credentials()
allCredentialsForClient.Should().NotBeNull();
allCredentialsForClient.Should().BeEmpty();
}

[Fact]
public async Task Test_GetEnabledConnectionsForClient()
{
var enabledConnections =
await fixture.ApiClient.Clients.GetEnabledConnectionsForClientAsync(
TestBaseUtils.GetVariable("AUTH0_CLIENT_ID"),
new EnabledConnectionsForClientGetRequest()
{
IncludeFields = true,
Strategy = new[] { "google-oauth2", "auth0" },
},
new CheckpointPaginationInfo()
);
enabledConnections.Should().NotBeNull();
enabledConnections.Count.Should().BeGreaterThan(0);
}

[Fact]
public async Task GetEnabledConnectionsForClientAsync_WithNullId_ThrowsArgumentNullException()
{
await Assert.ThrowsAsync<ArgumentNullException>(() =>
fixture.ApiClient.Clients.GetEnabledConnectionsForClientAsync(null, null, null));
}

[Fact]
public async Task GetEnabledConnectionsForClientAsync_WithEmptyStrategyArray_DoesNotPassStrategyInQuery()
{
var clientId = "client123";
var request = new EnabledConnectionsForClientGetRequest { Strategy = new string[0] };

var mockConnection = new Mock<IManagementConnection>();
mockConnection.Setup(c => c.GetAsync<ICheckpointPagedList<Connection>>(
It.Is<Uri>(uri => !uri.Query.Contains("strategy=")),
It.IsAny<IDictionary<string, string>>(),
It.IsAny<JsonConverter[]>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new CheckpointPagedList<Connection>());

var client = new ClientsClient(mockConnection.Object, new Uri("https://test.auth0.com"),
new Dictionary<string, string>());

await client.GetEnabledConnectionsForClientAsync(clientId, request, null);

mockConnection.Verify();
}

[Fact]
public async Task GetEnabledConnectionsForClientAsync_WithAllParameters_PassesAllParametersInQuery()
{
var clientId = "client123";
var request = new EnabledConnectionsForClientGetRequest
{
Fields = "id,name",
IncludeFields = true,
Strategy = new[] { "auth0", "google-oauth2" }
};
var pagination = new CheckpointPaginationInfo(10, "token");

var mockConnection = new Mock<IManagementConnection>();
mockConnection.Setup(c => c.GetAsync<ICheckpointPagedList<Connection>>(
It.Is<Uri>(uri =>
uri.Query.Contains("fields=id%2Cname") &&
uri.Query.Contains("include_fields=true") &&
uri.Query.Contains("strategy=auth0") &&
uri.Query.Contains("strategy=google-oauth2") &&
uri.Query.Contains("from=token") &&
uri.Query.Contains("take=10")),
It.IsAny<IDictionary<string, string>>(),
It.IsAny<JsonConverter[]>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new CheckpointPagedList<Connection>());

var client = new ClientsClient(mockConnection.Object, new Uri("https://test.auth0.com"),
new Dictionary<string, string>());

await client.GetEnabledConnectionsForClientAsync(clientId, request, pagination);

mockConnection.Verify();
}
}
Loading