diff --git a/src/Auth0.AuthenticationApi/AuthenticationApiClient.cs b/src/Auth0.AuthenticationApi/AuthenticationApiClient.cs index 9a643e4c4..870a853a5 100644 --- a/src/Auth0.AuthenticationApi/AuthenticationApiClient.cs +++ b/src/Auth0.AuthenticationApi/AuthenticationApiClient.cs @@ -1,14 +1,10 @@ using Auth0.AuthenticationApi.Models; using Auth0.AuthenticationApi.Tokens; using Auth0.Core.Http; -using Microsoft.IdentityModel.Tokens; using System; using System.Collections.Generic; using System.Dynamic; -using System.IdentityModel.Tokens.Jwt; using System.Net.Http; -using System.Security.Claims; -using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; @@ -189,6 +185,8 @@ public Task GetTokenAsync(ClientCredentialsTokenRequest req { "client_id", request.ClientId }, { "audience", request.Audience } }; + body.AddIfNotEmpty("organization", request.Organization); + ApplyClientAuthentication(request, body); return connection.SendAsync( diff --git a/src/Auth0.AuthenticationApi/Models/ClientCredentialsTokenRequest.cs b/src/Auth0.AuthenticationApi/Models/ClientCredentialsTokenRequest.cs index f13154a4c..d6f5bd8e0 100644 --- a/src/Auth0.AuthenticationApi/Models/ClientCredentialsTokenRequest.cs +++ b/src/Auth0.AuthenticationApi/Models/ClientCredentialsTokenRequest.cs @@ -37,5 +37,13 @@ public class ClientCredentialsTokenRequest : IClientAuthentication /// of Id Tokens. /// public JwtSignatureAlgorithm SigningAlgorithm { get; set; } + + /// + /// Organization. + /// + /// + /// This can be an Organization Name or ID. When included, the access token returned will include the org_id and org_name claims + /// + public string Organization { get; set; } } } \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Clients/ClientGrantsClient.cs b/src/Auth0.ManagementApi/Clients/ClientGrantsClient.cs index 896f69193..3bb464fa3 100644 --- a/src/Auth0.ManagementApi/Clients/ClientGrantsClient.cs +++ b/src/Auth0.ManagementApi/Clients/ClientGrantsClient.cs @@ -15,6 +15,7 @@ namespace Auth0.ManagementApi.Clients public class ClientGrantsClient : BaseClient, IClientGrantsClient { readonly JsonConverter[] converters = new JsonConverter[] { new PagedListConverter("client_grants") }; + readonly JsonConverter[] organizationsConverters = new JsonConverter[] { new PagedListConverter("organizations") }; /// /// Initializes a new instance of . @@ -66,6 +67,8 @@ public Task> GetAllAsync(GetClientGrantsRequest request, {"audience", request.Audience}, {"client_id", request.ClientId}, }; + + queryStrings.AddIfNotEmpty("allow_any_organization", request.AllowAnyOrganization?.ToString() ?? string.Empty); if (pagination != null) { @@ -88,5 +91,64 @@ public Task UpdateAsync(string id, ClientGrantUpdateRequest request { return Connection.SendAsync(new HttpMethod("PATCH"), BuildUri($"client-grants/{EncodePath(id)}"), request, DefaultHeaders, cancellationToken: cancellationToken); } + + /// + /// Get the organizations associated to a client grant + /// + /// The identifier of the client grant. + /// The cancellation token to cancel operation. + /// A containing the organizations requested. + public Task> GetAllOrganizationsAsync(string id, CancellationToken cancellationToken = default) + { + var queryStrings = new Dictionary(); + return Connection.GetAsync>(BuildUri($"client-grants/{EncodePath(id)}/organizations", queryStrings), DefaultHeaders, organizationsConverters, cancellationToken); + + } + /// + /// Get the organizations associated to a client grant + /// + /// The identifier of the client grant. + /// Specifies to use in requesting paged results. + /// The cancellation token to cancel operation. + /// A containing the organizations requested. + public Task> GetAllOrganizationsAsync(string id, PaginationInfo pagination, CancellationToken cancellationToken = default) + { + if (pagination == null) + { + throw new ArgumentNullException(nameof(pagination)); + } + + var queryStrings = new Dictionary + { + ["page"] = pagination.PageNo.ToString(), + ["per_page"] = pagination.PerPage.ToString(), + ["include_totals"] = pagination.IncludeTotals.ToString().ToLower() + }; + + return Connection.GetAsync>(BuildUri($"client-grants/{EncodePath(id)}/organizations", queryStrings), DefaultHeaders, organizationsConverters, cancellationToken); + } + + /// + /// Get the organizations associated to a client grant + /// + /// The identifier of the client grant. + /// Specifies to use in requesting checkpoint-paginated results. + /// The cancellation token to cancel operation. + /// A containing the organizations requested. + public Task> GetAllOrganizationsAsync(string id, CheckpointPaginationInfo pagination = null, CancellationToken cancellationToken = default) + { + if (pagination == null) + { + throw new ArgumentNullException(nameof(pagination)); + } + + var queryStrings = new Dictionary + { + ["from"] = pagination.From?.ToString(), + ["take"] = pagination.Take.ToString() + }; + + return Connection.GetAsync>(BuildUri($"client-grants/{EncodePath(id)}/organizations", queryStrings), DefaultHeaders, organizationsConverters, cancellationToken); + } } } diff --git a/src/Auth0.ManagementApi/Clients/IClientGrantsClient.cs b/src/Auth0.ManagementApi/Clients/IClientGrantsClient.cs index 8a8003686..055830444 100644 --- a/src/Auth0.ManagementApi/Clients/IClientGrantsClient.cs +++ b/src/Auth0.ManagementApi/Clients/IClientGrantsClient.cs @@ -40,5 +40,33 @@ public interface IClientGrantsClient /// The cancellation token to cancel operation. /// The that has been updated. Task UpdateAsync(string id, ClientGrantUpdateRequest request, CancellationToken cancellationToken = default); + + /// + /// Get the organizations associated to a client grant + /// + /// The identifier of the client grant. + /// The cancellation token to cancel operation. + /// A containing the organizations requested. + Task> GetAllOrganizationsAsync(string id, CancellationToken cancellationToken = default); + + /// + /// Get the organizations associated to a client grant + /// + /// The identifier of the client grant. + /// Specifies to use in requesting paged results. + /// The cancellation token to cancel operation. + /// A containing the organizations requested. + Task> GetAllOrganizationsAsync(string id, PaginationInfo pagination, + CancellationToken cancellationToken = default); + + /// + /// Get the organizations associated to a client grant + /// + /// The identifier of the client grant. + /// Specifies to use in requesting checkpoint-paginated results. + /// The cancellation token to cancel operation. + /// A containing the organizations requested. + Task> GetAllOrganizationsAsync(string id, CheckpointPaginationInfo pagination, + CancellationToken cancellationToken = default); } } diff --git a/src/Auth0.ManagementApi/Clients/IOrganizationsClient.cs b/src/Auth0.ManagementApi/Clients/IOrganizationsClient.cs index 10013babb..63afcb9c1 100644 --- a/src/Auth0.ManagementApi/Clients/IOrganizationsClient.cs +++ b/src/Auth0.ManagementApi/Clients/IOrganizationsClient.cs @@ -233,5 +233,37 @@ public interface IOrganizationsClient /// The cancellation token to cancel operation. /// A that represents the asynchronous delete operation. Task DeleteInvitationAsync(string organizationId, string invitationId, CancellationToken cancellationToken = default); + + /// + /// Get client grants associated with an organization. + /// + /// The id of the organization for which you want to retrieve the client grants. + /// Specifies criteria to use when querying client grants for the organization. + /// Specifies to use in requesting paged results. + /// The cancellation token to cancel operation. + /// A containing the client grants requested. + Task> GetAllClientGrantsAsync(string organizationId, + OrganizationGetClientGrantsRequest request, PaginationInfo pagination = null, + CancellationToken cancellationToken = default); + + /// + /// Associate a client grant with an organization + /// + /// The id of the organization to which you want to associate the client grant. + /// The containing the properties of the Client Grant to associate with the organization. + /// The cancellation token to cancel operation. + /// The new that has been created. + Task CreateClientGrantAsync(string organizationId, + OrganizationCreateClientGrantRequest request, CancellationToken cancellationToken = default); + + /// + /// Remove a client grant from an organization. + /// + /// The id of the organization for which you want to delete the client grant. + /// The id of the client grant you want to delete from the organization + /// The cancellation token to cancel operation. + /// A that represents the asynchronous delete operation. + Task DeleteClientGrantAsync(string organizationId, string clientGrantId, + CancellationToken cancellationToken = default); } } diff --git a/src/Auth0.ManagementApi/Clients/OrganizationsClient.cs b/src/Auth0.ManagementApi/Clients/OrganizationsClient.cs index 098dd1fd3..28ab44554 100644 --- a/src/Auth0.ManagementApi/Clients/OrganizationsClient.cs +++ b/src/Auth0.ManagementApi/Clients/OrganizationsClient.cs @@ -18,6 +18,7 @@ public class OrganizationsClient : BaseClient, IOrganizationsClient readonly JsonConverter[] memberRolesConverters = new JsonConverter[] { new PagedListConverter("roles") }; readonly JsonConverter[] membersCheckpointConverters = new JsonConverter[] { new CheckpointPagedListConverter("members") }; readonly JsonConverter[] invitationsConverters = new JsonConverter[] { new PagedListConverter("invitations") }; + readonly JsonConverter[] clientGrantsConverters = new JsonConverter[] { new PagedListConverter("client_grants") }; /// /// Initializes a new instance of . @@ -420,6 +421,59 @@ public Task DeleteInvitationAsync(string organizationId, string invitationId, Ca { return Connection.SendAsync(HttpMethod.Delete, BuildUri($"organizations/{EncodePath(organizationId)}/invitations/{EncodePath(invitationId)}"), null, DefaultHeaders, cancellationToken: cancellationToken); } + + /// + /// Get client grants associated to an organization. + /// + /// The id of the organization for which you want to retrieve the client grants. + /// Specifies criteria to use when querying client grants for the organization. + /// Specifies to use in requesting paged results. + /// The cancellation token to cancel operation. + /// A containing the client grants requested. + public Task> GetAllClientGrantsAsync(string organizationId, OrganizationGetClientGrantsRequest request, PaginationInfo pagination = null, CancellationToken cancellationToken = default) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + + var queryStrings = new Dictionary + { + {"audience", request.Audience}, + {"client_id", request.ClientId}, + }; + + 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($"organizations/{EncodePath(organizationId)}/client-grants", queryStrings), DefaultHeaders, clientGrantsConverters, cancellationToken); + } + + /// + /// Associate a client grant with an organization + /// + /// The id of the organization to which you want to associate the client grants. + /// The containing the properties of the Client Grant to associate with the organization. + /// The cancellation token to cancel operation. + /// The new that has been created. + public Task CreateClientGrantAsync(string organizationId, OrganizationCreateClientGrantRequest request, CancellationToken cancellationToken = default) + { + return Connection.SendAsync(HttpMethod.Post, BuildUri($"organizations/{EncodePath(organizationId)}/client-grants"), request, DefaultHeaders, cancellationToken: cancellationToken); + } + + /// + /// Remove a client grant from an organization. + /// + /// The id of the organization for which you want to delete the client grant. + /// The id of the client grant you want to delete from the organization + /// The cancellation token to cancel operation. + /// A that represents the asynchronous delete operation. + public Task DeleteClientGrantAsync(string organizationId, string clientGrantId, CancellationToken cancellationToken = default) + { + return Connection.SendAsync(HttpMethod.Delete, BuildUri($"organizations/{EncodePath(organizationId)}/client-grants/{EncodePath(clientGrantId)}"), null, DefaultHeaders, cancellationToken: cancellationToken); + } } } diff --git a/src/Auth0.ManagementApi/Models/ClientGrantBase.cs b/src/Auth0.ManagementApi/Models/ClientGrantBase.cs index 57dff35a1..6fb673fd1 100644 --- a/src/Auth0.ManagementApi/Models/ClientGrantBase.cs +++ b/src/Auth0.ManagementApi/Models/ClientGrantBase.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Newtonsoft.Json; +using Newtonsoft.Json.Converters; namespace Auth0.ManagementApi.Models { @@ -25,5 +26,21 @@ public class ClientGrantBase /// [JsonProperty("scope")] public List Scope { get; set; } + + /// + /// Defines whether organizations can be used with client credentials exchanges for this grant. (defaults to deny when not defined) + /// + /// + /// Possible values: [deny, allow, require] + /// + [JsonProperty("organization_usage")] + [JsonConverter(typeof(StringEnumConverter))] + public OrganizationUsage? OrganizationUsage { get; set; } + + /// + /// If enabled, any organization can be used with this grant. If disabled (default), the grant must be explicitly assigned to the desired organizations. + /// + [JsonProperty("allow_any_organization")] + public bool? AllowAnyOrganization { get; set; } } } \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/ClientGrantUpdateRequest.cs b/src/Auth0.ManagementApi/Models/ClientGrantUpdateRequest.cs index 0f2775d81..3e659bff0 100644 --- a/src/Auth0.ManagementApi/Models/ClientGrantUpdateRequest.cs +++ b/src/Auth0.ManagementApi/Models/ClientGrantUpdateRequest.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Newtonsoft.Json; +using Newtonsoft.Json.Converters; namespace Auth0.ManagementApi.Models { @@ -14,5 +15,20 @@ public class ClientGrantUpdateRequest [JsonProperty("scope")] public List Scope { get; set; } + /// + /// Defines whether organizations can be used with client credentials exchanges for this grant. (defaults to deny when not defined) + /// + /// + /// Possible values: [deny, allow, require] + /// + [JsonProperty("organization_usage")] + [JsonConverter(typeof(StringEnumConverter))] + public OrganizationUsage? OrganizationUsage { get; set; } + + /// + /// If enabled, any organization can be used with this grant. If disabled (default), the grant must be explicitly assigned to the desired organizations. + /// + [JsonProperty("allow_any_organization")] + public bool? AllowAnyOrganization { get; set; } } } \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/GetClientGrantsRequest.cs b/src/Auth0.ManagementApi/Models/GetClientGrantsRequest.cs index b17eff097..1d669e85d 100644 --- a/src/Auth0.ManagementApi/Models/GetClientGrantsRequest.cs +++ b/src/Auth0.ManagementApi/Models/GetClientGrantsRequest.cs @@ -13,6 +13,11 @@ public class GetClientGrantsRequest /// /// The Id of a client to filter by. /// - public string ClientId { get; set; } + public string ClientId { get; set; } + + /// + /// If enabled, any organization can be used with this grant. If disabled (default), the grant must be explicitly assigned to the desired organizations. + /// + public bool? AllowAnyOrganization { get; set; } } } \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/OrganizationClientGrant.cs b/src/Auth0.ManagementApi/Models/OrganizationClientGrant.cs new file mode 100644 index 000000000..d01c2d776 --- /dev/null +++ b/src/Auth0.ManagementApi/Models/OrganizationClientGrant.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Auth0.ManagementApi.Models +{ + public class OrganizationClientGrant + { + /// + /// Gets or sets the identifier for a Client Grant. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// Gets or sets the audience + /// + [JsonProperty("audience")] + public string Audience { get; set; } + + /// + /// Gets or sets the identifier of the + /// + [JsonProperty("client_id")] + public string ClientId { get; set; } + + /// + /// Gets or sets the list of scopes + /// + [JsonProperty("scope")] + public List Scope { get; set; } + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/OrganizationCreateClientGrantRequest.cs b/src/Auth0.ManagementApi/Models/OrganizationCreateClientGrantRequest.cs new file mode 100644 index 000000000..df692dfc8 --- /dev/null +++ b/src/Auth0.ManagementApi/Models/OrganizationCreateClientGrantRequest.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace Auth0.ManagementApi.Models +{ + public class OrganizationCreateClientGrantRequest + { + /// + /// A Client Grant ID to add to the organization. + /// + [JsonProperty("grant_id")] + public string GrantId { get; set; } + } +} \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/OrganizationGetClientGrantsRequest.cs b/src/Auth0.ManagementApi/Models/OrganizationGetClientGrantsRequest.cs new file mode 100644 index 000000000..9ed4e92ad --- /dev/null +++ b/src/Auth0.ManagementApi/Models/OrganizationGetClientGrantsRequest.cs @@ -0,0 +1,15 @@ +namespace Auth0.ManagementApi.Models +{ + public class OrganizationGetClientGrantsRequest + { + /// + /// URL Encoded audience of a client grant to filter. + /// + public string Audience { get; set; } + + /// + /// The Id of a client to filter by. + /// + public string ClientId { get; set; } + } +} \ No newline at end of file diff --git a/tests/Auth0.AuthenticationApi.IntegrationTests/TokenTests.cs b/tests/Auth0.AuthenticationApi.IntegrationTests/TokenTests.cs index 0febce202..53b26c61d 100644 --- a/tests/Auth0.AuthenticationApi.IntegrationTests/TokenTests.cs +++ b/tests/Auth0.AuthenticationApi.IntegrationTests/TokenTests.cs @@ -3,6 +3,8 @@ using FluentAssertions; using Microsoft.IdentityModel.Tokens; using System; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; using System.Security.Cryptography; using System.Threading.Tasks; using Xunit; @@ -28,6 +30,33 @@ public async Task Can_get_token_using_client_credentials() token.Should().NotBeNull(); } } + + [Fact] + public async Task Can_get_token_using_client_credentials_for_organization() + { + var existingOrgId = "org_V6ojENVd1ERs5YY1"; + using (var authenticationApiClient = new AuthenticationApiClient(GetVariable("AUTH0_AUTHENTICATION_API_URL"))) + { + // Get the access token + var token = await authenticationApiClient.GetTokenAsync(new ClientCredentialsTokenRequest + { + ClientId = GetVariable("AUTH0_CLIENT_ID"), + ClientSecret = GetVariable("AUTH0_CLIENT_SECRET"), + Audience = "dotnet-testing", + Organization = existingOrgId + }); + + // Ensure that we received an access token back + token.Should().NotBeNull(); + + var handler = new JwtSecurityTokenHandler(); + var jwtSecurityToken = handler.ReadJwtToken(token.AccessToken); + var orgIdClaim = jwtSecurityToken.Claims.First(claim => claim.Type == "org_id").Value; + + orgIdClaim.Should().NotBeNull(); + orgIdClaim.Should().Be(existingOrgId); + } + } [Fact(Skip = "Run Manual")] public async Task Can_get_token_using_client_credentials_and_ca() diff --git a/tests/Auth0.ManagementApi.IntegrationTests/ClientGrantTests.cs b/tests/Auth0.ManagementApi.IntegrationTests/ClientGrantTests.cs index 0ea310471..d332d5450 100644 --- a/tests/Auth0.ManagementApi.IntegrationTests/ClientGrantTests.cs +++ b/tests/Auth0.ManagementApi.IntegrationTests/ClientGrantTests.cs @@ -169,5 +169,64 @@ public async Task Test_without_paging() Assert.Null(grants.Paging); } + [Fact] + public async Task Organization_Client_Grants() + { + var apiId = "dotnet-testing"; + var clientId = fixture.TestClient.ClientId; + var existingOrgId = "org_V6ojENVd1ERs5YY1"; + + await fixture.ApiClient.Clients.UpdateAsync(clientId, new ClientUpdateRequest + { + ApplicationType = ClientApplicationType.NonInteractive, + OrganizationUsage = OrganizationUsage.Allow, + GrantTypes = new[] { "client_credentials" } + }); + + var newGrant = await fixture.ApiClient.ClientGrants.CreateAsync(new ClientGrantCreateRequest + { + ClientId = clientId, + Audience = apiId, + Scope = new List { "dotnet:testing" }, + OrganizationUsage = OrganizationUsage.Allow, + AllowAnyOrganization = true + }); + + fixture.TrackIdentifier(CleanUpType.ClientGrants, newGrant.Id); + + var orgsBefore = await fixture.ApiClient.ClientGrants.GetAllOrganizationsAsync(newGrant.Id); + + var orgGrantsBefore = await fixture.ApiClient.Organizations.GetAllClientGrantsAsync(existingOrgId, + new OrganizationGetClientGrantsRequest() + { + ClientId = clientId, + Audience = apiId, + }); + + orgsBefore.Count.Should().Be(0); + orgGrantsBefore.Count.Should().Be(0); + + await fixture.ApiClient.Organizations.CreateClientGrantAsync(existingOrgId, new OrganizationCreateClientGrantRequest() + { + GrantId = newGrant.Id + }); + + var orgsAfter = await fixture.ApiClient.ClientGrants.GetAllOrganizationsAsync(newGrant.Id); + + var orgGrantsAfter = await fixture.ApiClient.Organizations.GetAllClientGrantsAsync(existingOrgId, + new OrganizationGetClientGrantsRequest() + { + ClientId = clientId, + Audience = apiId, + }); + + + orgsAfter.Count.Should().Be(1); + orgGrantsAfter.Count.Should().Be(1); + + await fixture.ApiClient.ClientGrants.DeleteAsync(newGrant.Id); + fixture.UnTrackIdentifier(CleanUpType.ClientGrants, newGrant.Id); + } + } } \ No newline at end of file