diff --git a/Descope.Test/Descope.Test.csproj b/Descope.Test/Descope.Test.csproj new file mode 100644 index 0000000..122d27b --- /dev/null +++ b/Descope.Test/Descope.Test.csproj @@ -0,0 +1,47 @@ + + + + Exe + net6.0 + enable + enable + true + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + Always + + + + + + + + \ No newline at end of file diff --git a/Descope.Test/Descope.Test.sln b/Descope.Test/Descope.Test.sln new file mode 100644 index 0000000..7d56652 --- /dev/null +++ b/Descope.Test/Descope.Test.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Descope.Test", "Descope.Test.csproj", "{E2E0EF99-CE4C-40BA-8A8E-532F14D47F7A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E2E0EF99-CE4C-40BA-8A8E-532F14D47F7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E2E0EF99-CE4C-40BA-8A8E-532F14D47F7A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E2E0EF99-CE4C-40BA-8A8E-532F14D47F7A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E2E0EF99-CE4C-40BA-8A8E-532F14D47F7A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {18F8C6D8-027A-45D4-BFFC-ABFF27D9416F} + EndGlobalSection +EndGlobal diff --git a/Descope.Test/IntegrationTests/Authentication/AuthenticationTests.cs b/Descope.Test/IntegrationTests/Authentication/AuthenticationTests.cs new file mode 100644 index 0000000..922b065 --- /dev/null +++ b/Descope.Test/IntegrationTests/Authentication/AuthenticationTests.cs @@ -0,0 +1,146 @@ +using Xunit; + +namespace Descope.Test.Integration +{ + public class AuthenticationTests + { + private readonly DescopeClient _descopeClient = IntegrationTestSetup.InitDescopeClient(); + + [Fact] + public async Task Authentication_ValidateAndRefresh() + { + string? loginId = null; + try + { + await _descopeClient.Management.User.DeleteAllTestUsers(); + // Create a logged in test user + var testUser = await IntegrationTestSetup.InitTestUser(_descopeClient); + loginId = testUser.User.LoginIds.First(); + + // Make sure the session is valid + var token = await _descopeClient.Auth.ValidateSession(testUser.AuthInfo.SessionJwt); + Assert.Equal(testUser.AuthInfo.SessionJwt, token.Jwt); + Assert.NotEmpty(token.Id); + Assert.NotEmpty(token.ProjectId); + + // Refresh and see we got a new token + var refreshedToken = await _descopeClient.Auth.RefreshSession(testUser.AuthInfo.RefreshJwt!); + Assert.NotNull(refreshedToken.RefreshExpiration); + Assert.Equal(token.Id, refreshedToken.Id); + Assert.Equal(token.ProjectId, refreshedToken.ProjectId); + } + finally + { + if (!string.IsNullOrEmpty(loginId)) + { + try { await _descopeClient.Management.User.Delete(loginId); } + catch { } + } + } + } + + [Fact] + public async Task Authentication_ExchangeAccessKeyAndMe() + { + string? loginId = null; + string? accessKeyId = null; + try + { + // Create a logged in test user + var testUser = await IntegrationTestSetup.InitTestUser(_descopeClient); + loginId = testUser.User.LoginIds.First(); + + // Create an access key and exchange it + var accessKeyResponse = await _descopeClient.Management.AccessKey.Create(loginId, userId: testUser.User.UserId); + accessKeyId = accessKeyResponse.Key.Id; + var token = await _descopeClient.Auth.ExchangeAccessKey(accessKeyResponse.Cleartext); + Assert.NotEmpty(token.Id); + Assert.NotEmpty(token.ProjectId); + Assert.NotEmpty(token.Jwt); + } + finally + { + if (!string.IsNullOrEmpty(loginId)) + { + try { await _descopeClient.Management.User.Delete(loginId); } + catch { } + } + if (!string.IsNullOrEmpty(accessKeyId)) + { + try { await _descopeClient.Management.AccessKey.Delete(accessKeyId); } + catch { } + } + } + } + + [Fact] + public async Task Authentication_SelectTenant() + { + string? loginId = null; + List tenantIds = new() { }; + try + { + // Create a logged in test user + var testUser = await IntegrationTestSetup.InitTestUser(_descopeClient); + loginId = testUser.User.LoginIds.First(); + + // Create a couple of tenants and add to the user + var tenantId = await _descopeClient.Management.Tenant.Create(new TenantOptions(Guid.NewGuid().ToString())); + tenantIds.Add(tenantId); + await _descopeClient.Management.User.AddTenant(loginId, tenantId); + tenantId = await _descopeClient.Management.Tenant.Create(new TenantOptions(Guid.NewGuid().ToString())); + tenantIds.Add(tenantId); + await _descopeClient.Management.User.AddTenant(loginId, tenantId); + var session = await _descopeClient.Auth.SelectTenant(tenantId, testUser.AuthInfo.RefreshJwt!); + Assert.NotEmpty(session.SessionToken.Id); + Assert.NotEmpty(session.SessionToken.ProjectId); + Assert.NotEmpty(session.SessionToken.Jwt); + } + finally + { + if (!string.IsNullOrEmpty(loginId)) + { + try { await _descopeClient.Management.User.Delete(loginId); } + catch { } + } + foreach (var tenantId in tenantIds) + { + try { await _descopeClient.Management.Tenant.Delete(tenantId); } + catch { } + } + } + } + + [Fact] + public async Task Authentication_MeAndLogout() + { + string? loginId = null; + try + { + // Create a logged in test user + var testUser = await IntegrationTestSetup.InitTestUser(_descopeClient); + loginId = testUser.User.LoginIds.First(); + + // Me + var user = await _descopeClient.Auth.Me(testUser.AuthInfo.RefreshJwt!); + Assert.Equal(testUser.User.UserId, user.UserId); + + // Logout + await _descopeClient.Auth.LogOut(testUser.AuthInfo.RefreshJwt!); + + // Try me again + async Task Act() => await _descopeClient.Auth.Me(testUser.AuthInfo.RefreshJwt!); + DescopeException result = await Assert.ThrowsAsync(Act); + Assert.Contains("Expired due to logout", result.Message); + } + finally + { + if (!string.IsNullOrEmpty(loginId)) + { + try { await _descopeClient.Management.User.Delete(loginId); } + catch { } + } + } + } + } +} diff --git a/Descope.Test/IntegrationTests/Management/AccessKeyTests.cs b/Descope.Test/IntegrationTests/Management/AccessKeyTests.cs new file mode 100644 index 0000000..64e2961 --- /dev/null +++ b/Descope.Test/IntegrationTests/Management/AccessKeyTests.cs @@ -0,0 +1,160 @@ +using Xunit; + +namespace Descope.Test.Integration +{ + public class AccessKeyTests + { + private readonly DescopeClient _descopeClient = IntegrationTestSetup.InitDescopeClient(); + + [Fact] + public async Task AccessKey_Create_MissingName() + { + async Task Act() => await _descopeClient.Management.AccessKey.Create(name: ""); + DescopeException result = await Assert.ThrowsAsync(Act); + Assert.Contains("Access key name is required", result.Message); + } + + [Fact] + public async Task AccessKey_CreateAndUpdate() + { + string? id = null; + try + { + // Create an access key + var accessKey = await _descopeClient.Management.AccessKey.Create(name: Guid.NewGuid().ToString()); + id = accessKey.Key.Id; + + // Update and compare + var updatedName = accessKey.Key.Name + "updated"; + var updatedKey = await _descopeClient.Management.AccessKey.Update(id: id, name: updatedName); + Assert.Equal(updatedKey.Name, updatedKey.Name); + } + finally + { + if (!string.IsNullOrEmpty(id)) + { + try { await _descopeClient.Management.AccessKey.Delete(id); } + catch { } + } + } + } + + [Fact] + public async Task AccessKey_Update_MissingId() + { + async Task Act() => await _descopeClient.Management.AccessKey.Update("", "name"); + DescopeException result = await Assert.ThrowsAsync(Act); + Assert.Contains("ID is required", result.Message); + } + + [Fact] + public async Task AccessKey_Update_MissingName() + { + async Task Act() => await _descopeClient.Management.AccessKey.Update("someId", ""); + DescopeException result = await Assert.ThrowsAsync(Act); + Assert.Contains("name cannot be updated to empty", result.Message); + } + + [Fact] + public async Task Accesskey_ActivateDeactivate() + { + string? id = null; + try + { + // Create an access key + var accessKey = await _descopeClient.Management.AccessKey.Create(name: Guid.NewGuid().ToString()); + id = accessKey.Key.Id; + + // Deactivate + await _descopeClient.Management.AccessKey.Deactivate(id); + var loadedKey = await _descopeClient.Management.AccessKey.Load(id); + Assert.Equal("inactive", loadedKey.Status); + + // Activate + await _descopeClient.Management.AccessKey.Activate(id); + loadedKey = await _descopeClient.Management.AccessKey.Load(id); + Assert.Equal("active", loadedKey.Status); + } + finally + { + if (!string.IsNullOrEmpty(id)) + { + try { await _descopeClient.Management.AccessKey.Delete(id); } + catch { } + } + } + } + + [Fact] + public async Task AccessKey_Activate_MissingId() + { + async Task Act() => await _descopeClient.Management.AccessKey.Activate(""); + DescopeException result = await Assert.ThrowsAsync(Act); + Assert.Contains("ID is required", result.Message); + } + + [Fact] + public async Task AccessKey_Deactivate_MissingId() + { + async Task Act() => await _descopeClient.Management.AccessKey.Deactivate(""); + DescopeException result = await Assert.ThrowsAsync(Act); + Assert.Contains("ID is required", result.Message); + } + + [Fact] + public async Task AccessKey_Load_MissingId() + { + async Task Act() => await _descopeClient.Management.AccessKey.Load(""); + DescopeException result = await Assert.ThrowsAsync(Act); + Assert.Contains("Access key ID is required", result.Message); + } + + [Fact] + public async Task AccessKey_SearchAll() + { + string? id = null; + try + { + // Create an access key + var accessKey = await _descopeClient.Management.AccessKey.Create(name: Guid.NewGuid().ToString()); + id = accessKey.Key.Id; + + // Search for it + var accessKeys = await _descopeClient.Management.AccessKey.SearchAll(); + var key = accessKeys.Find(key => key.Id == id); + Assert.NotNull(key); + } + finally + { + if (!string.IsNullOrEmpty(id)) + { + try { await _descopeClient.Management.AccessKey.Delete(id); } + catch { } + } + } + } + + [Fact] + public async Task AccessKey_Delete() + { + // Arrange + var accessKey = await _descopeClient.Management.AccessKey.Create(name: Guid.NewGuid().ToString()); + + // Act + await _descopeClient.Management.AccessKey.Delete(accessKey.Key.Id); + + // Assert + var accessKeys = await _descopeClient.Management.AccessKey.SearchAll(new List() { accessKey.Key.Id }); + Assert.Empty(accessKeys); + } + + [Fact] + public async Task Accesskey_Delete_MissingId() + { + async Task Act() => await _descopeClient.Management.AccessKey.Delete(""); + DescopeException result = await Assert.ThrowsAsync(Act); + Assert.Contains("Access key ID is required", result.Message); + } + + } +} diff --git a/Descope.Test/IntegrationTests/Management/ProjectTests.cs b/Descope.Test/IntegrationTests/Management/ProjectTests.cs new file mode 100644 index 0000000..2bff7e7 --- /dev/null +++ b/Descope.Test/IntegrationTests/Management/ProjectTests.cs @@ -0,0 +1,36 @@ +using Xunit; + +namespace Descope.Test.Integration +{ + public class ProjectTests + { + private readonly DescopeClient _descopeClient = IntegrationTestSetup.InitDescopeClient(); + + [Fact] + public async Task Project_ExportImport() + { + var imported_project = await _descopeClient.Management.Project.Export(); + await _descopeClient.Management.Project.Import(imported_project); + } + + [Fact(Skip = "Test fails due to theme import")] + public async Task Project_CloneRenameDelete() + { + // Clone the current project + var name = Guid.NewGuid().ToString().Split("-").First(); + var result = await _descopeClient.Management.Project.Clone(name, ""); + Assert.NotNull(result); + + // Delete cloned project + await _descopeClient.Management.Project.Delete(result.ProjectId); + + // Rename + var new_name = Guid.NewGuid().ToString().Split("-").First(); + await _descopeClient.Management.Project.Rename(new_name); + + // Rename again so we will have original name + await _descopeClient.Management.Project.Rename("dotnet"); + } + + } +} diff --git a/Descope.Test/IntegrationTests/Management/TenantTests.cs b/Descope.Test/IntegrationTests/Management/TenantTests.cs new file mode 100644 index 0000000..1b69c22 --- /dev/null +++ b/Descope.Test/IntegrationTests/Management/TenantTests.cs @@ -0,0 +1,133 @@ +using Xunit; + +namespace Descope.Test.Integration +{ + public class TenantTests + { + private readonly DescopeClient _descopeClient = IntegrationTestSetup.InitDescopeClient(); + + [Fact] + public async Task Tenant_CreateAndLoad() + { + string? tenantId = null; + try + { + // Create a tenant + var name = Guid.NewGuid().ToString(); + var domain = name + ".com"; + var options = new TenantOptions(name) + { + SelfProvisioningDomains = new List { domain }, + }; + tenantId = await _descopeClient.Management.Tenant.Create(options: options); + + // Load and compare + var loadedTenant = await _descopeClient.Management.Tenant.LoadById(tenantId); + Assert.Equal(loadedTenant.Name, options.Name); + Assert.NotNull(loadedTenant.SelfProvisioningDomains); + Assert.Contains(domain, loadedTenant.SelfProvisioningDomains); + } + finally + { + if (!string.IsNullOrEmpty(tenantId)) + { + try { await _descopeClient.Management.Tenant.Delete(tenantId); } + catch { } + } + } + } + + [Fact] + public async Task Tenant_Create_MissingName() + { + async Task Act() => await _descopeClient.Management.Tenant.Create(new TenantOptions("")); + DescopeException result = await Assert.ThrowsAsync(Act); + Assert.Contains("Tenant name is required", result.Message); + } + + [Fact] + public async Task Tenant_UpdateAndSearch() + { + string? tenantId = null; + try + { + // Create a tenant + string tenantName = Guid.NewGuid().ToString(); + tenantId = await _descopeClient.Management.Tenant.Create(options: new TenantOptions(tenantName)); + var updatedTenantName = tenantName + "updated"; + + // Update and compare + await _descopeClient.Management.Tenant.Update(tenantId, new TenantOptions(updatedTenantName)); + var tenants = await _descopeClient.Management.Tenant.SearchAll(new TenantSearchOptions { Ids = new List { tenantId } }); + Assert.Single(tenants); + Assert.Equal(tenants[0].Name, updatedTenantName); + } + finally + { + // Cleanup + if (!string.IsNullOrEmpty(tenantId)) + { + try { await _descopeClient.Management.Tenant.Delete(tenantId); } + catch { } + } + } + } + + [Fact] + public async Task Tenant_Update_MissingId() + { + async Task Act() => await _descopeClient.Management.Tenant.Update("", new TenantOptions("")); + DescopeException result = await Assert.ThrowsAsync(Act); + Assert.Contains("Tenant ID is required", result.Message); + } + + [Fact] + public async Task Tenant_Update_MissingName() + { + async Task Act() => await _descopeClient.Management.Tenant.Update("someId", new TenantOptions("")); + DescopeException result = await Assert.ThrowsAsync(Act); + Assert.Contains("name cannot be updated to empty", result.Message); + } + + [Fact] + public async Task Tenant_DeleteAndLoadAll() + { + string? tenantId = null; + try + { + // Create a tenant + var id = await _descopeClient.Management.Tenant.Create(options: new TenantOptions(Guid.NewGuid().ToString())); + tenantId = id; + + // Delete it + await _descopeClient.Management.Tenant.Delete(id); + tenantId = null; + + // Load all and make sure it's gone + var tenants = await _descopeClient.Management.Tenant.LoadAll(); + foreach (var tenant in tenants) + { + Assert.NotEqual(id, tenant.Id); + } + } + finally + { + // Cleanup + if (!string.IsNullOrEmpty(tenantId)) + { + try { await _descopeClient.Management.Tenant.Delete(tenantId); } + catch { } + } + } + } + + [Fact] + public async Task Tenant_Delete_MissingId() + { + async Task Act() => await _descopeClient.Management.Tenant.Delete(""); + DescopeException result = await Assert.ThrowsAsync(Act); + Assert.Contains("Tenant ID is required", result.Message); + } + } + +} diff --git a/Descope.Test/IntegrationTests/Management/UserTests.cs b/Descope.Test/IntegrationTests/Management/UserTests.cs new file mode 100644 index 0000000..1e07964 --- /dev/null +++ b/Descope.Test/IntegrationTests/Management/UserTests.cs @@ -0,0 +1,637 @@ +using Xunit; + +namespace Descope.Test.Integration +{ + public class UserTests + { + private readonly DescopeClient _descopeClient = IntegrationTestSetup.InitDescopeClient(); + + [Fact] + public async Task User_Create() + { + string? loginId = null; + try + { + // Create a user + var name = Guid.NewGuid().ToString(); + var result = await _descopeClient.Management.User.Create(loginId: name, new UserRequest() + { + Email = name + "@test.com", + }); + loginId = result.LoginIds.First(); + } + finally + { + // Cleanup + if (!string.IsNullOrEmpty(loginId)) + { + try { await _descopeClient.Management.User.Delete(loginId); } + catch { } + } + } + } + + [Fact] + public async Task User_CreateBatch() + { + List? loginIds = null; + try + { + // Prepare batch info + var user1 = Guid.NewGuid().ToString(); + var user2 = Guid.NewGuid().ToString(); + var batchUsers = new List() + { + new(loginId: user1) + { + Email = user1 + "@test.com", + VerifiedEmail = true, + }, + new(loginId: user2) + { + Email = user2 + "@test.com", + VerifiedEmail = false, + } + }; + + // Create batch and check + var result = await _descopeClient.Management.User.CreateBatch(batchUsers); + Assert.True(result.CreatedUsers.Count == 2); + loginIds = new List(); + foreach (var createdUser in result.CreatedUsers) + { + var loginId = createdUser.LoginIds.First(); + loginIds.Add(loginId); + if (loginId == user1) + { + Assert.True(createdUser.VerifiedEmail); + } + else if (loginId == user2) + { + Assert.False(createdUser.VerifiedEmail); + } + } + } + finally + { + // Cleanup + if (loginIds != null) + { + foreach (var loginId in loginIds) + { + try { await _descopeClient.Management.User.Delete(loginId); } + catch { } + } + } + } + } + + [Fact] + public async Task User_Update() + { + string? loginId = null; + try + { + // Create a user + var name = Guid.NewGuid().ToString(); + var createResult = await _descopeClient.Management.User.Create(loginId: name, new UserRequest() + { + Email = name + "@test.com", + VerifiedEmail = true, + GivenName = "a", + }); + Assert.Equal("a", createResult.GivenName); + loginId = createResult.LoginIds.First(); + + // Update it + var updateResult = await _descopeClient.Management.User.Update(loginId, new UserRequest() + { + Email = name + "@test.com", + VerifiedEmail = true, + GivenName = "b", + }); + Assert.Equal("b", updateResult.GivenName); + } + finally + { + // Cleanup + if (!string.IsNullOrEmpty(loginId)) + { + try { await _descopeClient.Management.User.Delete(loginId); } + catch { } + } + } + } + + [Fact] + public async Task User_Activate() + { + string? loginId = null; + try + { + // Create a user + var name = Guid.NewGuid().ToString(); + var createResult = await _descopeClient.Management.User.Create(loginId: name, new UserRequest() + { + Email = name + "@test.com", + VerifiedEmail = true, + }); + Assert.Equal("invited", createResult.Status); + loginId = createResult.LoginIds.First(); + + // Act + var updateResult = await _descopeClient.Management.User.Deactivate(loginId); + Assert.Equal("disabled", updateResult.Status); + updateResult = await _descopeClient.Management.User.Activate(loginId); + Assert.Equal("enabled", updateResult.Status); + } + finally + { + // Cleanup + if (!string.IsNullOrEmpty(loginId)) + { + try { await _descopeClient.Management.User.Delete(loginId); } + catch { } + } + } + } + + [Fact] + public async Task User_UpdateLoginId() + { + string? loginId = null; + try + { + // Create a user + var name = Guid.NewGuid().ToString(); + var createResult = await _descopeClient.Management.User.Create(loginId: name, new UserRequest() + { + Email = name + "@test.com", + VerifiedEmail = true, + }); + loginId = createResult.LoginIds.First(); + + // Act + var updatedLoginId = Guid.NewGuid().ToString(); + var updateResult = await _descopeClient.Management.User.UpdateLoginId(loginId, updatedLoginId); + loginId = updatedLoginId; + + // Assert + Assert.Equal(updatedLoginId, updateResult.LoginIds.First()); + } + finally + { + // Cleanup + if (!string.IsNullOrEmpty(loginId)) + { + try { await _descopeClient.Management.User.Delete(loginId); } + catch { } + } + } + } + + [Fact] + public async Task User_UpdateEmail() + { + string? loginId = null; + try + { + // Create a user + var name = Guid.NewGuid().ToString(); + var createResult = await _descopeClient.Management.User.Create(loginId: name, new UserRequest() + { + Email = name + "@test.com", + VerifiedEmail = true, + }); + loginId = createResult.LoginIds.First(); + + // Act + var updatedEmail = Guid.NewGuid().ToString() + "@test.com"; + var updateResult = await _descopeClient.Management.User.UpdateEmail(loginId, updatedEmail, true); + + // Assert + Assert.Equal(updatedEmail, updateResult.Email); + } + finally + { + // Cleanup + if (!string.IsNullOrEmpty(loginId)) + { + try { await _descopeClient.Management.User.Delete(loginId); } + catch { } + } + } + } + + [Fact] + public async Task User_UpdatePhone() + { + string? loginId = null; + try + { + // Create a user + var name = Guid.NewGuid().ToString(); + var createResult = await _descopeClient.Management.User.Create(loginId: name, new UserRequest() + { + Phone = "+972555555555", + VerifiedPhone = true, + }); + loginId = createResult.LoginIds.First(); + + // Act + var updatedPhone = "+972555555556"; + var updateResult = await _descopeClient.Management.User.UpdatePhone(loginId, updatedPhone, true); + + // Assert + Assert.Equal(updatedPhone, updateResult.Phone); + } + finally + { + // Cleanup + if (!string.IsNullOrEmpty(loginId)) + { + try { await _descopeClient.Management.User.Delete(loginId); } + catch { } + } + } + } + + + [Fact] + public async Task User_UpdateDisplayName() + { + string? loginId = null; + try + { + // Create a user + var name = Guid.NewGuid().ToString(); + var createResult = await _descopeClient.Management.User.Create(loginId: name, new UserRequest() + { + Phone = "+972555555555", + Name = "a" + }); + loginId = createResult.LoginIds.First(); + + // Act + var updateResult = await _descopeClient.Management.User.UpdateDisplayName(loginId, "b"); + + // Assert + Assert.Equal("b", updateResult.Name); + } + finally + { + // Cleanup + if (!string.IsNullOrEmpty(loginId)) + { + try { await _descopeClient.Management.User.Delete(loginId); } + catch { } + } + } + } + + [Fact] + public async Task User_UpdateUserNames() + { + string? loginId = null; + try + { + // Create a user + var name = Guid.NewGuid().ToString(); + var createResult = await _descopeClient.Management.User.Create(loginId: name, new UserRequest() + { + Phone = "+972555555555", + GivenName = "a", + MiddleName = "a", + FamilyName = "a", + }); + loginId = createResult.LoginIds.First(); + + // Act + var updateResult = await _descopeClient.Management.User.UpdateUserNames(loginId, "b", "b", "b"); + + // Assert + Assert.Equal("b", updateResult.GivenName); + Assert.Equal("b", updateResult.MiddleName); + Assert.Equal("b", updateResult.FamilyName); + } + finally + { + // Cleanup + if (!string.IsNullOrEmpty(loginId)) + { + try { await _descopeClient.Management.User.Delete(loginId); } + catch { } + } + } + } + + [Fact] + public async Task User_UpdatePicture() + { + string? loginId = null; + try + { + // Create a user + var name = Guid.NewGuid().ToString(); + var createResult = await _descopeClient.Management.User.Create(loginId: name, new UserRequest() + { + Phone = "+972555555555", + Picture = "https://pics.com/a", + }); + loginId = createResult.LoginIds.First(); + + // Act + var updateResult = await _descopeClient.Management.User.UpdatePicture(loginId, "https://pics.com/b"); + + // Assert + Assert.Equal("https://pics.com/b", updateResult.Picture); + } + finally + { + // Cleanup + if (!string.IsNullOrEmpty(loginId)) + { + try { await _descopeClient.Management.User.Delete(loginId); } + catch { } + } + } + } + + [Fact(Skip = "Requires the custom attribute 'a' to exist on the project")] + public async Task User_UpdateCustomAttributes() + { + string? loginId = null; + try + { + // Create a user + var name = Guid.NewGuid().ToString(); + var createResult = await _descopeClient.Management.User.Create(loginId: name, new UserRequest() + { + Phone = "+972555555555", + CustomAttributes = new Dictionary { { "a", "b" } }, + }); + loginId = createResult.LoginIds.First(); + + // Update custom attribute + var updateResult = await _descopeClient.Management.User.UpdateCustomAttributes(loginId, "a", "c"); + Assert.NotNull(updateResult.CustomAttributes); + Assert.Equal("c", updateResult.CustomAttributes["a"].ToString()); + } + finally + { + // Cleanup + if (!string.IsNullOrEmpty(loginId)) + { + try { await _descopeClient.Management.User.Delete(loginId); } + catch { } + } + } + } + + [Fact] + public async Task User_Roles() + { + string? loginId = null; + try + { + // Create a user + var name = Guid.NewGuid().ToString(); + var createResult = await _descopeClient.Management.User.Create(loginId: name, new UserRequest() + { + Phone = "+972555555555", + VerifiedPhone = true, + }); + loginId = createResult.LoginIds.First(); + + // Check add roles + var roleNames = new List { "Tenant Admin" }; + var updateResult = await _descopeClient.Management.User.AddRoles(loginId, roleNames); + Assert.NotNull(updateResult.RoleNames); + Assert.Single(updateResult.RoleNames); + Assert.Contains("Tenant Admin", updateResult.RoleNames); + + // Check remove roles + updateResult = await _descopeClient.Management.User.RemoveRoles(loginId, roleNames); + Assert.NotNull(updateResult.RoleNames); + Assert.Empty(updateResult.RoleNames); + + // Check set roles + updateResult = await _descopeClient.Management.User.SetRoles(loginId, roleNames); + Assert.NotNull(updateResult.RoleNames); + Assert.Single(updateResult.RoleNames); + Assert.Contains("Tenant Admin", updateResult.RoleNames); + } + finally + { + // Cleanup + if (!string.IsNullOrEmpty(loginId)) + { + try { await _descopeClient.Management.User.Delete(loginId); } + catch { } + } + } + } + + [Fact] + public async Task User_SsoApps() + { + string? loginId = null; + try + { + // Create a user + var name = Guid.NewGuid().ToString(); + var createResult = await _descopeClient.Management.User.Create(loginId: name, new UserRequest() + { + Phone = "+972555555555", + VerifiedPhone = true, + }); + loginId = createResult.LoginIds.First(); + + // Check add sso apps + var ssoApps = new List { "descope-default-oidc" }; + var updateResult = await _descopeClient.Management.User.AddSsoApps(loginId, ssoApps); + Assert.NotNull(updateResult.SsoAppIds); + Assert.Single(updateResult.SsoAppIds); + Assert.Contains("descope-default-oidc", updateResult.SsoAppIds); + + // Check remove sso apps + updateResult = await _descopeClient.Management.User.RemoveSsoApps(loginId, ssoApps); + Assert.NotNull(updateResult.SsoAppIds); + Assert.Empty(updateResult.SsoAppIds); + + // Check set sso apps + updateResult = await _descopeClient.Management.User.SetSsoApps(loginId, ssoApps); + Assert.NotNull(updateResult.SsoAppIds); + Assert.Single(updateResult.SsoAppIds); + Assert.Contains("descope-default-oidc", updateResult.SsoAppIds); + } + finally + { + // Cleanup + if (!string.IsNullOrEmpty(loginId)) + { + try { await _descopeClient.Management.User.Delete(loginId); } + catch { } + } + } + } + + [Fact] + public async Task User_Tenants() + { + string? loginId = null; + string? tenantId = null; + try + { + // Create a user + var name = Guid.NewGuid().ToString(); + var createResult = await _descopeClient.Management.User.Create(loginId: name, new UserRequest() + { + Phone = "+972555555555", + VerifiedPhone = true, + }); + loginId = createResult.LoginIds.First(); + + // Create a tenant + tenantId = await _descopeClient.Management.Tenant.Create(new TenantOptions(Guid.NewGuid().ToString())); + + // Check add roles + var updateResult = await _descopeClient.Management.User.AddTenant(loginId, tenantId); + Assert.NotNull(updateResult.UserTenants); + Assert.Single(updateResult.UserTenants); + var t = updateResult.UserTenants.Find(t => t.TenantId == tenantId); + Assert.NotNull(t); + + // Check remove roles + updateResult = await _descopeClient.Management.User.RemoveTenant(loginId, tenantId); + Assert.NotNull(updateResult.UserTenants); + Assert.Empty(updateResult.UserTenants); + } + finally + { + // Cleanup + if (!string.IsNullOrEmpty(loginId)) + { + try { await _descopeClient.Management.User.Delete(loginId); } + catch { } + } + if (!string.IsNullOrEmpty(tenantId)) + { + try { await _descopeClient.Management.Tenant.Delete(tenantId); } + catch { } + } + } + } + + [Fact] + public async Task User_Password() + { + string? loginId = null; + try + { + // Create a user + var name = Guid.NewGuid().ToString(); + var createResult = await _descopeClient.Management.User.Create(loginId: name, new UserRequest() + { + Phone = "+972555555555", + VerifiedPhone = true, + }); + loginId = createResult.LoginIds.First(); + Assert.False(createResult.Password); + + // Set a temporary password + await _descopeClient.Management.User.SetActivePassword(loginId, "abCD123#$"); + var loadResult = await _descopeClient.Management.User.Load(loginId); + Assert.True(loadResult.Password); + await _descopeClient.Management.User.ExpirePassword(loginId); + await _descopeClient.Management.User.SetTemporaryPassword(loginId, "abCD123#$"); + } + finally + { + // Cleanup + if (!string.IsNullOrEmpty(loginId)) + { + try { await _descopeClient.Management.User.Delete(loginId); } + catch { } + } + } + } + + [Fact] + public async Task User_DeleteAndSearch() + { + string? loginId = null; + try + { + // Create a user + var name = Guid.NewGuid().ToString(); + var createResult = await _descopeClient.Management.User.Create(loginId: name, new UserRequest() + { + Phone = "+972111111111", + VerifiedPhone = true, + }); + loginId = createResult.LoginIds.First(); + + // Search for it + var users = await _descopeClient.Management.User.SearchAll(new SearchUserOptions() { Text = name, Limit = 1 }); + Assert.Single(users); + await _descopeClient.Management.User.Delete(loginId); + loginId = null; + users = await _descopeClient.Management.User.SearchAll(new SearchUserOptions { Text = name, Limit = 1 }); + Assert.Empty(users); + } + finally + { + // Cleanup + if (!string.IsNullOrEmpty(loginId)) + { + try { await _descopeClient.Management.User.Delete(loginId); } + catch { } + } + } + } + + [Fact] + public async Task User_TestUser() + { + string? loginId = null; + try + { + // Create a test user + var name = Guid.NewGuid().ToString(); + var createResult = await _descopeClient.Management.User.Create(loginId: name, new UserRequest() + { + Phone = "+972111111111", + VerifiedPhone = true, + + }, testUser: true); + loginId = createResult.LoginIds.First(); + + // Generate all manor of auth + var otp = await _descopeClient.Management.User.GenerateOtpForTestUser(DeliveryMethod.Email, loginId); + Assert.Equal(loginId, otp.LoginId); + Assert.NotEmpty(otp.Code); + var ml = await _descopeClient.Management.User.GenerateMagicLinkForTestUser(DeliveryMethod.Email, loginId); + Assert.NotEmpty(ml.Link); + Assert.Equal(loginId, ml.LoginId); + var el = await _descopeClient.Management.User.GenerateEnchantedLinkForTestUser(loginId); + Assert.NotEmpty(el.Link); + Assert.NotEmpty(el.PendingRef); + Assert.Equal(loginId, el.LoginId); + // Note: Enable embedded authentication to test + // var eml = await _descopeClient.Management.User.GenerateEmbeddedLink(loginId); + // Assert.NotEmpty(eml); + } + finally + { + // Cleanup + if (!string.IsNullOrEmpty(loginId)) + { + try { await _descopeClient.Management.User.Delete(loginId); } + catch { } + } + } + } + + } +} diff --git a/Descope.Test/IntegrationTests/Setup.cs b/Descope.Test/IntegrationTests/Setup.cs new file mode 100644 index 0000000..dc5c322 --- /dev/null +++ b/Descope.Test/IntegrationTests/Setup.cs @@ -0,0 +1,57 @@ +using Microsoft.Extensions.Configuration; + +namespace Descope.Test.Integration +{ + internal class IntegrationTestSetup + { + internal static DescopeClient InitDescopeClient() + { + Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Test"); + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettingsTest.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .Build(); + + var projectId = configuration["AppSettings:ProjectId"] ?? throw new ApplicationException("Can't run tests without a project ID"); + var managementKey = configuration["AppSettings:ManagementKey"] ?? throw new ApplicationException("Can't run tests without a management key"); + var baseUrl = configuration["AppSettings:BaseURL"]; + var isUnsafe = bool.Parse(configuration["AppSettings:Unsafe"] ?? "false"); + + var config = new DescopeConfig(projectId: projectId) + { + ManagementKey = managementKey, + BaseURL = baseUrl, + Unsafe = isUnsafe, + }; + + return new DescopeClient(config); + } + + internal static async Task InitTestUser(DescopeClient descopeClient) + { + var loginId = Guid.NewGuid().ToString(); + var user = await descopeClient.Management.User.Create(loginId: loginId, new UserRequest() + { + Phone = "+972555555555", + VerifiedPhone = true, + }, testUser: true); + + var generatedOtp = await descopeClient.Management.User.GenerateOtpForTestUser(DeliveryMethod.Sms, loginId); + var authInfo = await descopeClient.Auth.Otp.Verify(DeliveryMethod.Sms, loginId, generatedOtp.Code); + return new SignedInTestUser(user, authInfo); + } + + } + + internal class SignedInTestUser + { + internal UserResponse User { get; } + internal AuthenticationResponse AuthInfo { get; } + internal SignedInTestUser(UserResponse user, AuthenticationResponse authInfo) + { + User = user; + AuthInfo = authInfo; + } + } +} diff --git a/Descope.Test/packages.lock.json b/Descope.Test/packages.lock.json new file mode 100644 index 0000000..b1d44ac --- /dev/null +++ b/Descope.Test/packages.lock.json @@ -0,0 +1,1240 @@ +{ + "version": 1, + "dependencies": { + "net6.0": { + "Microsoft.Extensions.Configuration": { + "type": "Direct", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "tldQUBWt/xeH2K7/hMPPo5g8zuLc3Ro9I5d4o/XrxvxOCA2EZBtW7bCHHTc49fcBtvB8tLAb/Qsmfrq+2SJ4vA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "7.0.0", + "Microsoft.Extensions.Primitives": "7.0.0" + } + }, + "Microsoft.Extensions.Configuration.EnvironmentVariables": { + "type": "Direct", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "RIkfqCkvrAogirjsqSrG1E1FxgrLsOZU2nhRbl07lrajnxzSU2isj2lwQah0CtCbLWo/pOIukQzM1GfneBUnxA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "7.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "7.0.0" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "Direct", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "LDNYe3uw76W35Jci+be4LDf2lkQZe0A7EEYQVChFbc509CpZ4Iupod8li4PUXPBhEUOFI/rlQNf5xkzJRQGvtA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "7.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "7.0.0", + "Microsoft.Extensions.Configuration.FileExtensions": "7.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "7.0.0", + "System.Text.Json": "7.0.0" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Direct", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "95UnxZkkFdXxF6vSrtJsMHCzkDeSMuUWGs2hDT54cX+U5eVajrCJ3qLyQRW+CtpTt5OJ8bmTvpQVHu1DLhH+cA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "7.0.0", + "Microsoft.Extensions.Configuration.Binder": "7.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0", + "Microsoft.Extensions.Options": "7.0.0", + "Microsoft.Extensions.Primitives": "7.0.0" + } + }, + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[17.8.0, )", + "resolved": "17.8.0", + "contentHash": "BmTYGbD/YuDHmApIENdoyN1jCk0Rj1fJB0+B/fVekyTdVidr91IlzhqzytiUgaEAzL1ZJcYCme0MeBMYvJVzvw==", + "dependencies": { + "Microsoft.CodeCoverage": "17.8.0", + "Microsoft.TestPlatform.TestHost": "17.8.0" + } + }, + "Moq": { + "type": "Direct", + "requested": "[4.20.69, )", + "resolved": "4.20.69", + "contentHash": "8P/oAUOL8ZVyXnzBBcgdhTsOD1kQbAWfOcMI7KDQO3HqQtzB/0WYLdnMa4Jefv8nu/MQYiiG0IuoJdvG0v0Nig==", + "dependencies": { + "Castle.Core": "5.1.1" + } + }, + "NBuilder": { + "type": "Direct", + "requested": "[6.1.0, )", + "resolved": "6.1.0", + "contentHash": "NtDOtz/yc24t5yNivaGJQ1LHczoaYwNDjI04P7KEFDmbb2h6uL5OJZ5ExKx+wcypK2dBhRj2o1SiLAfj3kc8jw==", + "dependencies": { + "NETStandard.Library": "1.6.1" + } + }, + "xunit": { + "type": "Direct", + "requested": "[2.6.1, )", + "resolved": "2.6.1", + "contentHash": "SnTEV7LFf2s3GJua5AJKB/m115jDcWJSG5n02YZS05iezU2QJKjShCsOxlxL8FUO+J7h2/yXGEr+evgpIHc3sA==", + "dependencies": { + "xunit.analyzers": "1.4.0", + "xunit.assert": "2.6.1", + "xunit.core": "[2.6.1]" + } + }, + "xunit.assert": { + "type": "Direct", + "requested": "[2.6.1, )", + "resolved": "2.6.1", + "contentHash": "+4bI81RS88tiYvfsBfC0YsdDd8v7kkLkRtDXmux3YBT8u1afhjdwxwBvkHGgrQ6NPRzE8xZpVGX2iaLkbXvYvg==" + }, + "xunit.extensibility.core": { + "type": "Direct", + "requested": "[2.6.1, )", + "resolved": "2.6.1", + "contentHash": "DA4NqcFGLlRxX2zP3QptlQuRoOSdmBkr17ntK29jfRXqScj2fysIhvQvF5DHtDzAEkoRPqZcfR/IRGSItxmRqw==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "xunit.abstractions": "2.0.3" + } + }, + "xunit.runner.visualstudio": { + "type": "Direct", + "requested": "[2.5.3, )", + "resolved": "2.5.3", + "contentHash": "HFFL6O+QLEOfs555SqHii48ovVa4CqGYanY+B32BjLpPptdE+wEJmCFNXlLHdEOD5LYeayb9EroaUpydGpcybg==" + }, + "Castle.Core": { + "type": "Transitive", + "resolved": "5.1.1", + "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", + "dependencies": { + "System.Diagnostics.EventLog": "6.0.0" + } + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "17.8.0", + "contentHash": "KC8SXWbGIdoFVdlxKk9WHccm0llm9HypcHMLUUFabRiTS3SO2fQXNZfdiF3qkEdTJhbRrxhdRxjL4jbtwPq4Ew==" + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "f34u2eaqIjNO9YLHBz8rozVZ+TcFiFs0F3r7nUJd7FRkVSxk8u4OpoK226mi49MwexHOR2ibP9MFvRUaLilcQQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "7.0.0" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "tgU4u7bZsoS9MKVRiotVMAwHtbREHr5/5zSEV+JPhg46+ox47Au84E3D2IacAaB0bk5ePNaNieTlPrfjbbRJkg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "7.0.0" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "xk2lRJ1RDuqe57BmgvRPyCt6zyePKUmvT6iuXqiHR+/OIIgWVR8Ff5k2p6DwmqY8a17hx/OnrekEhziEIeQP6Q==", + "dependencies": { + "Microsoft.Extensions.Configuration": "7.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "7.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "7.0.0", + "Microsoft.Extensions.FileProviders.Physical": "7.0.0", + "Microsoft.Extensions.Primitives": "7.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "h3j/QfmFN4S0w4C2A6X7arXij/M/OVw3uQHSOFxnND4DyAzO1F9eMX7Eti7lU/OkSthEE0WzRsfT/Dmx86jzCw==" + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "NyawiW9ZT/liQb34k9YqBSNPLuuPkrjMgQZ24Y/xXX1RoiBkLUdPMaQTmxhZ5TYu8ZKZ9qayzil75JX95vGQUg==", + "dependencies": { + "Microsoft.Extensions.Primitives": "7.0.0" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "K8D2MTR+EtzkbZ8z80LrG7Ur64R7ZZdRLt1J5cgpc/pUWl0C6IkAUapPuK28oionHueCPELUqq0oYEvZfalNdg==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "7.0.0", + "Microsoft.Extensions.FileSystemGlobbing": "7.0.0", + "Microsoft.Extensions.Primitives": "7.0.0" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "2jONjKHiF+E92ynz2ZFcr9OvxIw+rTGMPEH+UZGeHTEComVav93jQUWGkso8yWwVBcEJGcNcZAaqY01FFJcj7w==" + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "lP1yBnTTU42cKpMozuafbvNtQ7QcBjr/CcK3bYOGEMH55Fjt+iecXjT6chR7vbgCMqy3PG3aNQSZgo/EuY/9qQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0", + "Microsoft.Extensions.Primitives": "7.0.0" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "um1KU5kxcRp3CNuI8o/GrZtD4AIOXDk+RLsytjZ9QPok3ttLUelLKpilVPuaFT3TFjOhSibUAso0odbOaCDj3Q==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "7.3.1", + "contentHash": "gIw8Sr5ZpuzKFBTfJonh2F54DivTzm5IIK15QB4Y6uE30uQdEO1NnCojTC/b6sWZoZzD0sdBa6SqwMXhucD+nA==" + }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Transitive", + "resolved": "7.3.1", + "contentHash": "mXA6AoaD5uZqtsKghgRiupBhyXNii8p9F2BjNLnDGud0tZLS5+4Fio2YAGjFXhnkc80CqgQ61X5U1gUNnDEoKQ==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "7.3.1" + } + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "7.3.1", + "contentHash": "uPt2aiRUCbcOc0Wk+dDCSClFfPNs3S3Z7fmy50MoxJ1mGmtVUDMpyRJeYzZ/16x4rL19T+g2zrzjcWoitp5+gQ==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "7.3.1" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Transitive", + "resolved": "7.3.1", + "contentHash": "/c/p8/3CAH706c0ii5uTgSb/8M/jwyuurtdMeKTBeKFU9aA+EZrLu1M8aaS3CSlGaxoxsoaxr4/+KXykgQ4VgQ==", + "dependencies": { + "Microsoft.IdentityModel.Logging": "7.3.1" + } + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + }, + "Microsoft.NETCore.Targets": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "17.8.0", + "contentHash": "AYy6vlpGMfz5kOFq99L93RGbqftW/8eQTqjT9iGXW6s9MRP3UdtY8idJ8rJcjeSja8A18IhIro5YnH3uv1nz4g==", + "dependencies": { + "NuGet.Frameworks": "6.5.0", + "System.Reflection.Metadata": "1.6.0" + } + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "17.8.0", + "contentHash": "9ivcl/7SGRmOT0YYrHQGohWiT5YCpkmy/UEzldfVisLm6QxbLaK3FAJqZXI34rnRLmqqDCeMQxKINwmKwAPiDw==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "17.8.0", + "Newtonsoft.Json": "13.0.1" + } + }, + "Microsoft.Win32.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "NETStandard.Library": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.Win32.Primitives": "4.3.0", + "System.AppContext": "4.3.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Console": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.Compression.ZipFile": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.Net.Http": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Net.Sockets": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.InteropServices.RuntimeInformation": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Timer": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0", + "System.Xml.XDocument": "4.3.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "NuGet.Frameworks": { + "type": "Transitive", + "resolved": "6.5.0", + "contentHash": "QWINE2x3MbTODsWT1Gh71GaGb5icBz4chS8VYvTgsBnsi8esgN6wtHhydd7fvToWECYGq7T4cgBBDiKD/363fg==" + }, + "RestSharp": { + "type": "Transitive", + "resolved": "110.2.0", + "contentHash": "FXGw0IMcqY7yO/hzS9QrD3iNswNgb9UxJnxWmfOxmGs4kRlZWqdtBoGPLuhlbgsDzX1RFo4WKui8TGGKXWKalw==", + "dependencies": { + "System.Text.Json": "7.0.2" + } + }, + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "HdSSp5MnJSsg08KMfZThpuLPJpPwE5hBXvHwoKWosyHHfe8Mh5WKT0ylEOf6yNzX6Ngjxe4Whkafh5q7Ymac4Q==" + }, + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+yH1a49wJMy8Zt4yx5RhJrxO/DBDByAiCzNwiETI+1S4mPdCu0OY4djdciC7Vssk0l22wQaDLrXxXkp+3+7bVA==" + }, + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c3YNH1GQJbfIPJeCnr4avseugSqPrxwIqzthYyZDN6EuOyNOzq+y2KSUfRcXauya1sF4foESTgwM5e1A8arAKw==" + }, + "runtime.native.System": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "INBPonS5QPEgn7naufQFXJEp3zX6L4bwHgJ/ZH78aBTpeNfQMtf7C6VrAFhlq2xxWBveIOWyFzQjJ8XzHMhdOQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZVuZJqnnegJhd2k/PtAbbIcZ3aZeITq3sj06oKfMBSfphW3HDmk/t4ObvbOk/JA/swGR0LNqMksAh/f7gpTROg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DloMk88juo0OuOWr56QG7MNchmafTLYWvABy36izkrLI5VledI0rq28KGs1i9wbpeT9NPQrx/wTf8U2vazqQ3Q==", + "dependencies": { + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": "4.3.0" + } + }, + "runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "NS1U+700m4KFRHR5o4vo9DSlTmlCKu/u7dtE5sUHVIPB+xpXxYQvgBgA6wEIeCz6Yfn0Z52/72WYsToCEPJnrw==", + "dependencies": { + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "b3pthNgxxFcD+Pc0WSEoC0+md3MyhRS6aCEeenvNE3Fdw1HyJ18ZhRFVJJzIeR/O/jpxPboB805Ho0T3Ul7w8A==" + }, + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KeLz4HClKf+nFS7p/6Fi/CqyLXh81FpiGzcmuS8DGi9lUqSnZ6Es23/gv2O+1XVGfrbNmviF7CckBpavkBoIFQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kVXCuMTrTlxq4XOOMAysuNwsXWpYeboGddNGpIgNSZmv1b6r/s/DPk0fYMB7Q5Qo4bY68o48jt4T4y5BVecbCQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X7IdhILzr4ROXd8mI1BUCQMSHSQwelUlBjF1JyTKCjXaOGn2fB4EKBxQbCK2VjO3WaWIdlXZL3W6TiIVnrhX4g==" + }, + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "nyFNiCk/r+VOiIqreLix8yN+q3Wga9+SE8BCgkf+2BwEKiNx6DyvFjCgkfV743/grxv8jHJ8gUK4XEQw7yzRYg==" + }, + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ytoewC6wGorL7KoCAvRfsgoJPJbNq+64k2SqW6JcOAebWsFUvCCYgfzQMrnpvPiEl4OrblUlhF2ji+Q1+SVLrQ==" + }, + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "I8bKw2I8k58Wx7fMKQJn2R8lamboCAiHfHeV/pS65ScKWMMI0+wJkLYlEKvgW1D/XvSl/221clBoR2q9QNNM7A==" + }, + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VB5cn/7OzUfzdnC8tqAIMQciVLiq2epm2NrAm1E9OjNRyG4lVhfR61SMcLizejzQP8R8Uf/0l5qOIbUEi+RdEg==" + }, + "System.AppContext": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "fKC+rmaLfeIzUhagxY17Q9siv/sPrjjKcfNg1Ic8IlQkZLipo8ljcaZQu4VtI4Jqbzjc2VTjzGLF6WmsRXAEgA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ratu44uTIHgeBeI0dE8DWvmXVBSo4u7ozRZZHOMmK/JPpYyo0dAfgSiHlpiObMQ5lEtEyIXA40sKRYg5J6A8uQ==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Collections": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Collections.Concurrent": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Console": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DHDrIxiqk1h03m6khKWV2X8p/uvN79rgSqpilL6uzpmSfxfU5ng8VcPtW4qsDsQDHiTv6IPV9TmD5M/vElPNLg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "tD6kosZnTAGdrEa0tZSuFyunMbt/5KYDnHdndJYGqZoNy00XVXyACd5d6KnE1YgYv3ne2CjtAfNXo/fwEhnKUA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" + }, + "System.Diagnostics.Tools": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Calendars": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GUlBtdOWT4LTV3I+9/PJW+56AnnChTaOqqTLFtdmype/L500M2LIyXgmtd9X2P2VOkmJd5c67H5SaC2QcL1bFA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "FhKmdR6MPG+pxow6wGtNAWdZh7noIOpdD5TwQ3CprzgIE1bBBoim0vbR1+AWsWjQmU7zXHgQo4TWSP6lCeiWcQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0" + } + }, + "System.IdentityModel.Tokens.Jwt": { + "type": "Transitive", + "resolved": "7.3.1", + "contentHash": "iE8biOWyAC1NnYcZGcgXErNACvIQ6Gcmg5s28gsjVbyyYdF9NdKsYzAPAsO3KGK86EQjpToI1AO82XbG8chkzA==", + "dependencies": { + "Microsoft.IdentityModel.JsonWebTokens": "7.3.1", + "Microsoft.IdentityModel.Tokens": "7.3.1" + } + }, + "System.IO": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Buffers": "4.3.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.IO.Compression": "4.3.0" + } + }, + "System.IO.Compression.ZipFile": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "G4HwjEsgIwy3JFBduZ9quBkAu+eUwjIdJleuNSgmUojbH6O3mlvEIme+GHx/cLlTAPcrnnL7GqvB9pTlWRfhOg==", + "dependencies": { + "System.Buffers": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.IO.FileSystem": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.FileSystem.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "6QOb2XFLch7bEc4lIcJH49nJN2HV+OC3fHDgsLVsBVBk3Y4hFAnOBGzJ2lUu7CyDDFo9IBWkSsnbkT6IBwwiMw==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Linq": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Linq.Expressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "PGKkrd2khG4CnlyJwxwwaWWiSiWFNBGlgXvJpeO0xCXrZ89ODrQ6tjEWS/kOqZ8GwEOUATtKtzp1eRgmYNfclg==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Linq": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Emit.Lightweight": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "sYg+FtILtRQuYWSIAuNOELwVuVsxVyJGWQyOnlAzhV4xvhyFnON1bAzYYC+jjRW8JREM45R0R5Dgi8MTC5sEwA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.DiagnosticSource": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Net.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Net.Sockets": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "m6icV6TqQOAdgt5N/9I5KNpjom/5NFtkmGseEH+AK/hny8XrytLH3+b5M8zL/Ycg3fhIocFpUMyl/wpFnVRvdw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.ObjectModel": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "bdX+80eKv9bN6K4N+d77OankKHGn6CH711a6fcOpMQu2Fckp/Ft4L/kW9WznHpyR0NRAvJutzOMHNNlBGvxQzQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "228FG0jLcIwTVJyz8CLFKueVqQK36ANazUManGaJHkO0icjiIypKW7YLWLIWahyIkdh5M7mV2dJepllLyA1SKg==", + "dependencies": { + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.ILGeneration": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.Lightweight": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "1.6.0", + "contentHash": "COC1aiAJjCoA5GBF+QKL2uLqEBew4JsCkQmoHKbN3TlOZKa2fKLz5CpiRQKDz0RsAOEGsVKqOD5bomsXq/4STQ==" + }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.TypeExtensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + }, + "System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Runtime.InteropServices.RuntimeInformation": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, + "System.Runtime.Numerics": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==", + "dependencies": { + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Security.Cryptography.Algorithms": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.Apple": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Cng": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "03idZOqFlsKRL4W+LuCpJ6dBYDUWReug6lZjBa3uJWnk5sPCUXckocevTaUA8iT/MFSrY/2HXkOt753xQ/cf8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Security.Cryptography.Csp": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X4s/FCkEUnRGnwR3aSfVIkldBmtURMhmexALNTwpjklzxWU7yjMk7GHLKOZTNkgnWnE0q7+BCf9N2LVRWxewaA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Security.Cryptography.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Linq": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "h4CEgOgv5PKVF/HwaHzJRiVboL2THYCou97zpmhjghx5frc7fIvlkY1jL+lnIQyChrJDMNEXS6r7byGif8Cy4w==", + "dependencies": { + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Security.Cryptography.X509Certificates": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Cng": "4.3.0", + "System.Security.Cryptography.Csp": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Text.Encoding.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "OP6umVGxc0Z0MvZQBVigj4/U31Pw72ITihDWP9WiWDm+q5aoe0GaJivsfYGq53o6dxH7DcXWiCTl7+0o2CGdmg==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Text.Json": { + "type": "Transitive", + "resolved": "7.0.2", + "contentHash": "/LZf/JrGyilojqwpaywb+sSz8Tew7ij4K/Sk+UW8AKfAK7KRhR6mKpKtTm06cYA7bCpGTWfYksIW+mVsdxPegQ==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encodings.Web": "7.0.0" + } + }, + "System.Text.RegularExpressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "RpT2DA+L660cBt1FssIE9CAGpLFdFPuheB7pLpKpn6ZXNby7jDERe8Ua/Ne2xGiwLVG2JOqziiaVCGDon5sKFA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Threading": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==", + "dependencies": { + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "npvJkVKl5rKXrtl1Kkm6OhOUaYGEiF9wFbppFRWSMoApKzt2PiPHT2Bb8a5sAWxprvdOAtvaARS9QYMznEUtug==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Threading.Timer": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Z6YfyYTCg7lOZjJzBjONJTFKGN9/NIYKSxhU5GRd+DTwHSZyvWp1xuI5aR+dLg+ayyC5Xv57KiY4oJ0tMO89fQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Xml.ReaderWriter": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GrprA+Z0RUXaR4N7/eW71j1rgMnEnEVlgii49GZyAjTH7uliMnrOU3HNFBr6fEDBCJCIdlVNq9hHbaDR621XBA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Tasks.Extensions": "4.3.0" + } + }, + "System.Xml.XDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5zJ0XDxAIg8iy+t4aMnQAu0MqVbqyvfoUVl1yDV61xdo3Vth45oA2FoY4pPkxYAH5f8ixpmTqXeEIya95x0aCQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } + }, + "xunit.abstractions": { + "type": "Transitive", + "resolved": "2.0.3", + "contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg==" + }, + "xunit.analyzers": { + "type": "Transitive", + "resolved": "1.4.0", + "contentHash": "7ljnTJfFjz5zK+Jf0h2dd2QOSO6UmFizXsojv/x4QX7TU5vEgtKZPk9RvpkiuUqg2bddtNZufBoKQalsi7djfA==" + }, + "xunit.core": { + "type": "Transitive", + "resolved": "2.6.1", + "contentHash": "Ru0POZXVYwa/G3/tS3TO3Yug/P+08RPeDkuepTmywNjfICYwHHY9zJBoxdeziZ0OintLtLKUMOBcC6VJzjqhwg==", + "dependencies": { + "xunit.extensibility.core": "[2.6.1]", + "xunit.extensibility.execution": "[2.6.1]" + } + }, + "xunit.extensibility.execution": { + "type": "Transitive", + "resolved": "2.6.1", + "contentHash": "sLKPQKuEQhRuhVuLiYEkRdUcwCfp+BIKds3r0JL8AYvOWRmVYYKWYouuYzPjmeUF6iEGC9CHCVz/NF1+wv+Mag==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "xunit.extensibility.core": "[2.6.1]" + } + }, + "descope": { + "type": "Project", + "dependencies": { + "Newtonsoft.Json": "[13.0.3, )", + "RestSharp": "[110.2.0, )", + "System.IdentityModel.Tokens.Jwt": "[7.3.1, )" + } + } + } + } +} \ No newline at end of file diff --git a/Descope.Test/xunit.runner.json b/Descope.Test/xunit.runner.json new file mode 100644 index 0000000..dd80f43 --- /dev/null +++ b/Descope.Test/xunit.runner.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "parallelizeAssembly": false, + "parallelizeTestCollections": false +} diff --git a/Descope.sln b/Descope.sln new file mode 100644 index 0000000..1d748c6 --- /dev/null +++ b/Descope.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.34221.43 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Descope", "..\descope-dotnet\Descope\Descope.csproj", "{4B7D16FE-6007-4F53-84BA-44C5BA8CAD75}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Descope.Test", "..\descope-dotnet\Descope.Test\Descope.Test.csproj", "{A1CD983B-6231-4B0E-A07F-B5A5EE14A16F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4B7D16FE-6007-4F53-84BA-44C5BA8CAD75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4B7D16FE-6007-4F53-84BA-44C5BA8CAD75}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4B7D16FE-6007-4F53-84BA-44C5BA8CAD75}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4B7D16FE-6007-4F53-84BA-44C5BA8CAD75}.Release|Any CPU.Build.0 = Release|Any CPU + {A1CD983B-6231-4B0E-A07F-B5A5EE14A16F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1CD983B-6231-4B0E-A07F-B5A5EE14A16F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1CD983B-6231-4B0E-A07F-B5A5EE14A16F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1CD983B-6231-4B0E-A07F-B5A5EE14A16F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B336F720-C2FB-425E-A428-0F4F747DD7D2} + EndGlobalSection +EndGlobal diff --git a/Descope/Descope.csproj b/Descope/Descope.csproj new file mode 100644 index 0000000..2337c01 --- /dev/null +++ b/Descope/Descope.csproj @@ -0,0 +1,29 @@ + + + + net6.0 + enable + enable + true + + + + + + + + + + + + + + + + + + + + + + diff --git a/Descope/Descope.sln b/Descope/Descope.sln new file mode 100644 index 0000000..f2896a3 --- /dev/null +++ b/Descope/Descope.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.34221.43 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Descope", "Descope.csproj", "{B2EBA0EC-66DF-484A-A140-1937AF5F90CF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Descope.Test", "..\Descope.Test\Descope.Test.csproj", "{2342D0A7-D9EE-4A31-9BA9-0294147938B4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B2EBA0EC-66DF-484A-A140-1937AF5F90CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2EBA0EC-66DF-484A-A140-1937AF5F90CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2EBA0EC-66DF-484A-A140-1937AF5F90CF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2EBA0EC-66DF-484A-A140-1937AF5F90CF}.Release|Any CPU.Build.0 = Release|Any CPU + {2342D0A7-D9EE-4A31-9BA9-0294147938B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2342D0A7-D9EE-4A31-9BA9-0294147938B4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2342D0A7-D9EE-4A31-9BA9-0294147938B4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2342D0A7-D9EE-4A31-9BA9-0294147938B4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {FEFAFD62-DCCD-488B-9ED9-9A7DDBF9CEFA} + EndGlobalSection +EndGlobal diff --git a/Descope/DescopeClient.cs b/Descope/DescopeClient.cs new file mode 100644 index 0000000..53660e0 --- /dev/null +++ b/Descope/DescopeClient.cs @@ -0,0 +1,26 @@ +using Descope.Internal.Management; +using Descope.Internal.Auth; + +namespace Descope +{ + public class DescopeClient + { + public IAuthentication Auth { get; } + public IManagement Management { get; } + + public DescopeClient(DescopeConfig descopeConfig) + { + var httpClient = new Internal.HttpClient(descopeConfig); + var managementKey = descopeConfig.ManagementKey ?? ""; + + Auth = new Authentication(httpClient); + Management = new Management(httpClient, managementKey); + } + } + + public static class SdkInfo + { + public static string Name { get; } = "dotnet"; + public static string Version { get; } = "0.1.0"; + } +} diff --git a/Descope/Exception/DescopeException.cs b/Descope/Exception/DescopeException.cs new file mode 100644 index 0000000..340adec --- /dev/null +++ b/Descope/Exception/DescopeException.cs @@ -0,0 +1,41 @@ +using System.Text.Json.Serialization; + +namespace Descope +{ + [Serializable] + public class DescopeException : ApplicationException + { + public string? ErrorCode { get; set; } + public string? ErrorDescription { get; set; } + public string? ErrorMessage { get; set; } + + public DescopeException(string msg) : base(msg) { } + + public DescopeException(ErrorDetails errorDetails) : base(message: errorDetails.ExceptionMessage) + { + ErrorCode = errorDetails.ErrorCode; + ErrorDescription = errorDetails.ErrorDescription; + ErrorMessage = errorDetails.ErrorMessage; + } + } + + public class ErrorDetails + { + [JsonPropertyName("errorCode")] + public string ErrorCode { get; set; } + + [JsonPropertyName("errorDescription")] + public string ErrorDescription { get; set; } + [JsonPropertyName("errorMessage")] + public string? ErrorMessage { get; set; } + public ErrorDetails(string errorCode, string errorDescription, string? errorMessage) + { + ErrorCode = errorCode; + ErrorDescription = errorDescription; + ErrorMessage = errorMessage; + } + + public string ExceptionMessage { get => $"[{ErrorCode}]: {ErrorDescription}{(ErrorMessage != null ? $" ({ErrorMessage})" : "")}"; } + + } +} diff --git a/Descope/Internal/Authentication/Authentication.cs b/Descope/Internal/Authentication/Authentication.cs new file mode 100644 index 0000000..47e3a67 --- /dev/null +++ b/Descope/Internal/Authentication/Authentication.cs @@ -0,0 +1,284 @@ +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; +using System.Security.Cryptography; +using System.Text.Json.Serialization; + +namespace Descope.Internal.Auth +{ + public class Authentication : IAuthentication + { + public IOtp Otp { get => _otp; } + + private readonly Otp _otp; + + private readonly IHttpClient _httpClient; + private readonly JsonWebTokenHandler _jsonWebTokenHandler = new(); + private readonly Dictionary> _securityKeys = new(); + + private const string ClaimPermissions = "permissions"; + private const string ClaimRoles = "roles"; + + public Authentication(IHttpClient httpClient) + { + _httpClient = httpClient; + _otp = new Otp(httpClient); + } + + public async Task ValidateSession(string sessionJwt) + { + if (string.IsNullOrEmpty(sessionJwt)) throw new DescopeException("sessionJwt empty"); + var token = await ValidateToken(sessionJwt) ?? throw new DescopeException("Session invalid"); + return token; + } + + public async Task RefreshSession(string refreshJwt) + { + if (string.IsNullOrEmpty(refreshJwt)) throw new DescopeException("refreshJwt empty"); + var refreshToken = await ValidateToken(refreshJwt) ?? throw new DescopeException("Refresh token invalid"); + var response = await _httpClient.Post(Routes.AuthRefresh, refreshJwt); + try + { + return new Token(_jsonWebTokenHandler.ReadJsonWebToken(response.SessionJwt)) + { + RefreshExpiration = refreshToken.Expiration + }; + } + catch + { + throw new DescopeException("Unable to parse refreshed session jwt"); + } + } + + public async Task ValidateAndRefreshSession(string sessionJwt, string refreshJwt) + { + if (string.IsNullOrEmpty(sessionJwt) && string.IsNullOrEmpty(refreshJwt)) throw new DescopeException("Both sessionJwt and refreshJwt are empty"); + if (!string.IsNullOrEmpty(sessionJwt)) + { + try { return await ValidateSession(sessionJwt); } + catch { } + } + if (string.IsNullOrEmpty(refreshJwt)) throw new DescopeException("Cannot refresh session with empty refresh JWT"); + return await RefreshSession(refreshJwt); + } + + public async Task ExchangeAccessKey(string accessKey, AccessKeyLoginOptions? loginOptions = null) + { + if (string.IsNullOrEmpty(accessKey)) throw new DescopeException("access key missing"); + var response = await _httpClient.Post(Routes.AuthAccessKeyExchange, accessKey, new { loginOptions }); + return new Token(_jsonWebTokenHandler.ReadJsonWebToken(response.SessionJwt)) ?? throw new DescopeException("Failed to parse exchanged token"); + } + + public bool ValidatePermissions(Token token, List permissions, string? tenant) + { + return ValidateAgainstClaims(token, ClaimPermissions, permissions, tenant); + } + + public List GetMatchedPermissions(Token token, List permissions, string? tenant) + { + return GetMatchedClaimValues(token, ClaimPermissions, permissions, tenant); + } + + public bool ValidateRoles(Token token, List roles, string? tenant) + { + return ValidateAgainstClaims(token, ClaimRoles, roles, tenant); + } + + public List GetMatchedRoles(Token token, List roles, string? tenant) + { + return GetMatchedClaimValues(token, ClaimRoles, roles, tenant); + } + + public async Task SelectTenant(string tenant, string refreshJwt) + { + if (string.IsNullOrEmpty(refreshJwt)) throw new DescopeException("refreshJwt empty"); + var token = await ValidateToken(refreshJwt) ?? throw new DescopeException("invalid refreshJwt"); + var body = new { tenant }; + var response = await _httpClient.Post(Routes.AuthSelectTenant, refreshJwt, body); + return AuthResponseToSession(response, token); + } + + public async Task LogOut(string refreshJwt) + { + if (string.IsNullOrEmpty(refreshJwt)) throw new DescopeException("refreshJwt empty"); + _ = await ValidateToken(refreshJwt) ?? throw new DescopeException("invalid refreshJwt"); + await _httpClient.Post(Routes.AuthLogOut, refreshJwt); + } + + public async Task LogOutAll(string refreshJwt) + { + if (string.IsNullOrEmpty(refreshJwt)) throw new DescopeException("refreshJwt empty"); + _ = await ValidateToken(refreshJwt) ?? throw new DescopeException("invalid refreshJwt"); + await _httpClient.Post(Routes.AuthLogOutAll, refreshJwt); + } + + public async Task Me(string refreshJwt) + { + if (string.IsNullOrEmpty(refreshJwt)) throw new DescopeException("refreshJwt empty"); + _ = await ValidateToken(refreshJwt) ?? throw new DescopeException("invalid refreshJwt"); + return await _httpClient.Get(Routes.AuthMe, refreshJwt); + } + + #region Internal + + private async Task ValidateToken(string jwt) + { + await FetchKeyIfNeeded(); + try + { + var token = _jsonWebTokenHandler.ReadJsonWebToken(jwt) ?? throw new System.Exception("Unable to read token"); + var result = await _jsonWebTokenHandler.ValidateTokenAsync(jwt, new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + IssuerSigningKeyResolver = (token, securityToken, kid, validationParameters) => _securityKeys[kid], + ValidateIssuerSigningKey = true, + RequireExpirationTime = true, + RequireSignedTokens = true, + ClockSkew = TimeSpan.FromSeconds(5), + }); + if (result.Exception != null) throw result.Exception; + return result.IsValid ? new Token(token) : null; + } + catch { return null; } + } + + private async Task FetchKeyIfNeeded() + { + if (!_securityKeys.IsNullOrEmpty()) return; + + var response = await _httpClient.Get(Routes.AuthKeys + $"{_httpClient.DescopeConfig.ProjectId}"); + foreach (var key in response.Keys) + { + var rsa = RSA.Create(); + rsa.ImportParameters(key.ToRsaParameters()); + var list = _securityKeys.ContainsKey(key.Kid) ? _securityKeys[key.Kid] : new List(); + list.Add(new RsaSecurityKey(rsa)); + _securityKeys[key.Kid] = list; + } + } + + private Session AuthResponseToSession(AuthenticationResponse response, Token refreshToken) + { + var sessionToken = new Token(_jsonWebTokenHandler.ReadJsonWebToken(response.SessionJwt)) ?? throw new DescopeException("Failed to parse session JWT"); + if (!string.IsNullOrEmpty(response.RefreshJwt)) + { + refreshToken = new Token(_jsonWebTokenHandler.ReadJsonWebToken(response.RefreshJwt)) ?? throw new DescopeException("Failed to parse refresh JWT"); + } + sessionToken.RefreshExpiration = refreshToken.Expiration; + return new Session(sessionToken, refreshToken, response.User, response.FirstSeen); + } + + private static bool ValidateAgainstClaims(Token token, string claim, List values, string? tenant) + { + if (!string.IsNullOrEmpty(tenant) && !IsAssociatedWithTenant(token, tenant)) return false; + var claimItems = GetAuthorizationClaimItems(token, claim, tenant); + foreach (var value in values) + { + if (!claimItems.Contains(value)) return false; + } + return true; + } + + private static List GetMatchedClaimValues(Token token, string claim, List values, string? tenant) + { + if (!string.IsNullOrEmpty(tenant) && !IsAssociatedWithTenant(token, tenant)) return new List(); + var claimItems = GetAuthorizationClaimItems(token, claim, tenant); + var matched = new List(); + foreach (var value in values) + { + if (claimItems.Contains(value)) matched.Add(value); + } + return matched; + } + + private static List GetAuthorizationClaimItems(Token token, string claim, string? tenant) + { + if (string.IsNullOrEmpty(tenant)) + { + if (token.Claims[claim] is List list) return list; + } + else + { + if (token.GetTenantValue(tenant, claim) is List list) return list; + } + return new List(); + } + + private static bool IsAssociatedWithTenant(Token token, string tenant) + { + return token.GetTenants().Contains(tenant); + } + + #endregion Internal + } + + internal class AccessKeyExchangeResponse + { + [JsonPropertyName("sessionJwt")] + public string SessionJwt { get; set; } + + public AccessKeyExchangeResponse(string sessionJwt) + { + SessionJwt = sessionJwt; + } + } + + internal class JwtKeyResponse + { + [JsonPropertyName("keys")] + public List Keys { get; set; } + + public JwtKeyResponse(List keys) + { + Keys = keys; + } + } + + public class JwtKey + { + [JsonPropertyName("alg")] + public string Alg { get; set; } + + [JsonPropertyName("e")] + public string E { get; set; } + + [JsonPropertyName("kid")] + public string Kid { get; set; } + + [JsonPropertyName("kty")] + public string Kty { get; set; } + + [JsonPropertyName("n")] + public string N { get; set; } + + [JsonPropertyName("use")] + public string Use { get; set; } + + public JwtKey(string alg, string e, string kid, string kty, string n, string use) + { + Alg = alg; + E = e; + Kid = kid; + Kty = kty; + N = n; + Use = use; + } + + public RSAParameters ToRsaParameters() + { + var modulusBase64 = N; + modulusBase64 = modulusBase64.Replace("_", "/").Replace("-", "+").PadRight(modulusBase64.Length + (4 - modulusBase64.Length % 4) % 4, '='); + byte[] modulusBytes = Convert.FromBase64String(modulusBase64); + + var rsaParameters = new RSAParameters + { + Modulus = modulusBytes, + Exponent = Convert.FromBase64String(E) + }; + + return rsaParameters; + } + } + + +} diff --git a/Descope/Internal/Authentication/Otp.cs b/Descope/Internal/Authentication/Otp.cs new file mode 100644 index 0000000..dbb6bee --- /dev/null +++ b/Descope/Internal/Authentication/Otp.cs @@ -0,0 +1,76 @@ +namespace Descope.Internal.Auth +{ + public class Otp : IOtp + { + private readonly IHttpClient _httpClient; + + public Otp(IHttpClient httpClient) + { + _httpClient = httpClient; + } + + public async Task SignUp(DeliveryMethod deliveryMethod, string loginId, SignUpDetails? details) + { + if (string.IsNullOrEmpty(loginId)) throw new DescopeException("loginId missing"); + var body = new { loginId, user = details }; + var response = await _httpClient.Post(Routes.OtpSignUp + deliveryMethod.ToString().ToLower(), body: body); + return deliveryMethod == DeliveryMethod.Email ? response.MaskedEmail ?? "" : response.MaskedPhone ?? ""; + } + + public async Task SignIn(DeliveryMethod deliveryMethod, string loginId, LoginOptions? loginOptions) + { + if (string.IsNullOrEmpty(loginId)) throw new DescopeException("loginId missing"); + var body = new { loginId, loginOptions }; + var response = await _httpClient.Post(Routes.OtpSignIn + deliveryMethod.ToString().ToLower(), body: body); + return deliveryMethod == DeliveryMethod.Email ? response.MaskedEmail ?? "" : response.MaskedPhone ?? ""; + } + + public async Task SignUpOrIn(DeliveryMethod deliveryMethod, string loginId, LoginOptions? loginOptions) + { + if (string.IsNullOrEmpty(loginId)) throw new DescopeException("loginId missing"); + var body = new { loginId, loginOptions }; + var response = await _httpClient.Post(Routes.OtpSignUpOrIn + deliveryMethod.ToString().ToLower(), body: body); + return deliveryMethod == DeliveryMethod.Email ? response.MaskedEmail ?? "" : response.MaskedPhone ?? ""; + } + + public async Task Verify(DeliveryMethod deliveryMethod, string loginId, string code) + { + if (string.IsNullOrEmpty(loginId)) throw new DescopeException("loginId missing"); + var body = new { loginId, code }; + return await _httpClient.Post(Routes.OtpVerify + deliveryMethod.ToString().ToLower(), body: body); + } + + public async Task UpdateEmail(string loginId, string email, string refreshJwt, UpdateOptions? updateOptions) + { + if (string.IsNullOrEmpty(loginId)) throw new DescopeException("loginId missing"); + if (string.IsNullOrEmpty(email)) throw new DescopeException("email missing"); + if (string.IsNullOrEmpty(refreshJwt)) throw new DescopeException("refreshJwt missing"); + var body = new + { + loginId, + email, + addToLoginIDs = updateOptions?.AddToLoginIds, + onMergeUseExisting = updateOptions?.OnMergeUseExisting, + }; + var result = await _httpClient.Post(Routes.OtpUpdateEmail, refreshJwt, body); + return result.MaskedEmail ?? ""; + } + + public async Task UpdatePhone(string loginId, string phone, string refreshJwt, UpdateOptions? updateOptions) + { + if (string.IsNullOrEmpty(loginId)) throw new DescopeException("loginId missing"); + if (string.IsNullOrEmpty(phone)) throw new DescopeException("phone missing"); + if (string.IsNullOrEmpty(refreshJwt)) throw new DescopeException("refreshJwt missing"); + var body = new + { + loginId, + phone, + addToLoginIDs = updateOptions?.AddToLoginIds, + onMergeUseExisting = updateOptions?.OnMergeUseExisting, + }; + var response = await _httpClient.Post(Routes.OtpUpdatePhone, refreshJwt, body); + return response.MaskedPhone ?? ""; + } + + } +} diff --git a/Descope/Internal/Authentication/Shared.cs b/Descope/Internal/Authentication/Shared.cs new file mode 100644 index 0000000..ce04657 --- /dev/null +++ b/Descope/Internal/Authentication/Shared.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace Descope.Internal.Auth +{ + internal class MaskedAddressResponse + { + [JsonPropertyName("maskedEmail")] + public string? MaskedEmail { get; set; } + + [JsonPropertyName("maskedPhone")] + public string? MaskedPhone { get; set; } + } +} diff --git a/Descope/Internal/Http/HttpClient.cs b/Descope/Internal/Http/HttpClient.cs new file mode 100644 index 0000000..d8fef8a --- /dev/null +++ b/Descope/Internal/Http/HttpClient.cs @@ -0,0 +1,84 @@ +using RestSharp; +using JsonSerializer = System.Text.Json.JsonSerializer; + +namespace Descope.Internal +{ + public interface IHttpClient + { + DescopeConfig DescopeConfig { get; set; } + + Task Get(string resource, string? pswd = null); + + Task Post(string resource, string? pswd = null, object? body = null); + + Task Delete(string resource, string pswd); + } + + public class HttpClient : IHttpClient + { + DescopeConfig IHttpClient.DescopeConfig { get => _descopeConfig; set => _descopeConfig = value; } + + private DescopeConfig _descopeConfig; + private readonly RestClient _client; + + public HttpClient(DescopeConfig descopeConfig) + { + _descopeConfig = descopeConfig; + var baseUrl = descopeConfig.BaseURL ?? "https://api.descope.com"; + + // init rest client + var options = new RestClientOptions(baseUrl); + if (_descopeConfig.Unsafe) options.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; + _client = new RestClient(options); + _client.AddDefaultHeader("Accept", "application/json"); + _client.AddDefaultHeader("Content-Type", "application/json"); + _client.AddDefaultHeader("x-descope-sdk-name", SdkInfo.Name); + _client.AddDefaultHeader("x-descope-sdk-version", SdkInfo.Version); + _client.AddDefaultHeader("x-descope-sdk-dotnet-version", Environment.Version.ToString()); + } + + public async Task Get(string resource, string? pswd = null) + { + return await Call(resource, Method.Get, pswd); + } + + public async Task Post(string resource, string? pswd = null, object? body = null) + { + return await Call(resource, Method.Post, pswd, body); + } + + public async Task Delete(string resource, string? pswd = null) + { + return await Call(resource, Method.Delete, pswd); + } + + private async Task Call(string resource, Method method, string? pswd, object? body = null) + { + var request = new RestRequest(resource, method); + + // Add authorization header + var bearer = _descopeConfig.ProjectId; + if (!string.IsNullOrEmpty(pswd)) bearer = $"{bearer}:{pswd}"; + request.AddHeader("Authorization", "Bearer " + bearer); + + if (body != null) + { + var jsonBody = JsonSerializer.Serialize(body); + request.AddJsonBody(jsonBody); + } + + var response = await _client.ExecuteAsync(request); + + if (response.StatusCode == System.Net.HttpStatusCode.OK) + { + string cnt = response.Content ?? "{}"; + return JsonSerializer.Deserialize(cnt) ?? throw new DescopeException("Unable to parse response"); + } + else + { + var ed = JsonSerializer.Deserialize(response.Content ?? "{}"); + throw (ed != null) ? new DescopeException(ed) : new DescopeException("Unexpected server error"); + } + } + } +} diff --git a/Descope/Internal/Http/Routes.cs b/Descope/Internal/Http/Routes.cs new file mode 100644 index 0000000..82472dd --- /dev/null +++ b/Descope/Internal/Http/Routes.cs @@ -0,0 +1,112 @@ +namespace Descope.Internal +{ + public static class Routes + { + #region Auth + + #region General Auth + + public const string AuthKeys = "/v2/keys/"; + public const string AuthMe = "/v1/auth/me"; + public const string AuthRefresh = "/v1/auth/refresh"; + public const string AuthSelectTenant = "/v1/auth/tenant/select"; + public const string AuthLogOut = "/v1/auth/logout"; + public const string AuthLogOutAll = "/v1/auth/logoutall"; + public const string AuthAccessKeyExchange = "/v1/auth/accesskey/exchange"; + + #endregion General Auth + + #region OTP + + public const string OtpSignUp = "/v1/auth/otp/signup/"; + public const string OtpSignIn = "/v1/auth/otp/signin/"; + public const string OtpSignUpOrIn = "/v1/auth/otp/signup-in/"; + public const string OtpVerify = "/v1/auth/otp/verify/"; + public const string OtpUpdateEmail = "/v1/auth/otp/update/email"; + public const string OtpUpdatePhone = "/v1/auth/otp/update/phone"; + + #endregion OTP + + #endregion Auth + + #region Management + + #region Tenant + + public const string TenantCreate = "/v1/mgmt/tenant/create"; + public const string TenantUpdate = "/v1/mgmt/tenant/update"; + public const string TenantDelete = "/v1/mgmt/tenant/delete"; + public const string TenantLoad = "/v1/mgmt/tenant"; + public const string TenantLoadAll = "/v1/mgmt/tenant/all"; + public const string TenantSearch = "/v1/mgmt/tenant/search"; + + #endregion Tenant + + #region User + + public const string UserCreate = "/v1/mgmt/user/create"; + public const string UserCreateBatch = "/v1/mgmt/user/create/batch"; + public const string UserLoad = "/v1/mgmt/user"; + public const string UserSearch = "/v1/mgmt/user/search"; + public const string UserUpdate = "/v1/mgmt/user/update"; + public const string UserUpdateStatus = "/v1/mgmt/user/update/status"; + public const string UserUpdateEmail = "/v1/mgmt/user/update/email"; + public const string UserUpdatePhone = "/v1/mgmt/user/update/phone"; + public const string UserUpdateName = "/v1/mgmt/user/update/name"; + public const string UserUpdatePicture = "/v1/mgmt/user/update/picture"; + public const string UserUpdateCustomAttribute = "/v1/mgmt/user/update/customAttribute"; + public const string UserUpdateLoginId = "/v1/mgmt/user/update/loginid"; + public const string UserPasswordSet = "/v1/mgmt/user/password/set"; + public const string UserPasswordExpire = "/v1/mgmt/user/password/expire"; + public const string UserPasskeyRemoveAll = "/v1/mgmt/user/passkeys/delete"; + public const string UserLogout = "/v1/mgmt/user/logout"; + public const string UserDelete = "/v1/mgmt/user/delete"; + public const string UserTenantAdd = "/v1/mgmt/user/update/tenant/add"; + public const string UserTenantRemove = "/v1/mgmt/user/update/tenant/remove"; + public const string UserRolesSet = "/v1/mgmt/user/update/role/set"; + public const string UserRolesAdd = "/v1/mgmt/user/update/role/add"; + public const string UserRoleRemove = "/v1/mgmt/user/update/role/remove"; + public const string UserSsoAppSet = "/v1/mgmt/user/update/ssoapp/set"; + public const string UserSsoAppAdd = "/v1/mgmt/user/update/ssoapp/add"; + public const string UserSsoAppRemove = "/v1/mgmt/user/update/ssoapp/remove"; + public const string UserProviderToken = "/v1/mgmt/user/provider/token"; + public const string UserDeleteAllTestUsers = "/v1/mgmt/user/test/delete/all"; + public const string UserTestsGenerateOtp = "/v1/mgmt/tests/generate/otp"; + public const string UserTestsGenerateMagicLink = "/v1/mgmt/tests/generate/magiclink"; + public const string UserTestsGenerateEnchantedLink = "/v1/mgmt/tests/generate/enchantedlink"; + public const string UserTestsGenerateEmbeddedLink = "/v1/mgmt/user/signin/embeddedlink"; + + #endregion User + + #region JWT + + public const string JwtUpdate = "/v1/mgmt/jwt/update"; + + #endregion JWT + + #region AccessKey + + public const string AccessKeyCreate = "/v1/mgmt/accesskey/create"; + public const string AccessKeyLoad = "/v1/mgmt/accesskey"; + public const string AccessKeySearch = "/v1/mgmt/accesskey/search"; + public const string AccessKeyUpdate = "/v1/mgmt/accesskey/update"; + public const string AccessKeyActivate = "v1/mgmt/accesskey/activate"; + public const string AccessKeyDeactivate = "/v1/mgmt/accesskey/deactivate"; + public const string AccessKeyDelete = "/v1/mgmt/accesskey/delete"; + + #endregion AccessKey + + #region Project + + public const string ProjectExport = "/v1/mgmt/project/export"; + public const string ProjectImport = "/v1/mgmt/project/import"; + public const string ProjectClone = "/v1/mgmt/project/clone"; + public const string ProjectRename = "/v1/mgmt/project/update/name"; + public const string ProjectDelete = "/v1/mgmt/project/delete"; + + #endregion Project + + + #endregion Management + } +} diff --git a/Descope/Internal/Management/AccessKey.cs b/Descope/Internal/Management/AccessKey.cs new file mode 100644 index 0000000..b8ba42e --- /dev/null +++ b/Descope/Internal/Management/AccessKey.cs @@ -0,0 +1,90 @@ +using System.Text.Json.Serialization; + +namespace Descope.Internal.Management +{ + internal class AccessKey : IAccessKey + { + private readonly IHttpClient _httpClient; + private readonly string _managementKey; + + internal AccessKey(IHttpClient httpClient, string managementKey) + { + _httpClient = httpClient; + _managementKey = managementKey; + } + + public async Task Create(string name, int? expireTime, List? roleNames, List? keyTenants, string? userId) + { + if (string.IsNullOrEmpty(name)) throw new DescopeException("Access key name is required for creation"); + var body = new { expireTime, name, keyTenants, roleNames, userId }; + return await _httpClient.Post(Routes.AccessKeyCreate, _managementKey, body); + } + + public async Task Update(string id, string name) + { + if (string.IsNullOrEmpty(id)) throw new DescopeException("Access key ID is required for update"); + if (string.IsNullOrEmpty(name)) throw new DescopeException("Access key name cannot be updated to empty"); + var body = new { id, name }; + var result = await _httpClient.Post(Routes.AccessKeyUpdate, _managementKey, body); + return result.Key; + } + + public async Task Activate(string id) + { + if (string.IsNullOrEmpty(id)) throw new DescopeException("Access key ID is required for activation"); + var request = new { id = id }; + await _httpClient.Post(Routes.AccessKeyActivate, _managementKey, request); + } + + public async Task Deactivate(string id) + { + if (string.IsNullOrEmpty(id)) throw new DescopeException("Access key ID is required for deactivation"); + var request = new { id = id }; + await _httpClient.Post(Routes.AccessKeyDeactivate, _managementKey, request); + } + + public async Task Delete(string id) + { + if (string.IsNullOrEmpty(id)) throw new DescopeException("Access key ID is required for deletion"); + var request = new { id = id }; + await _httpClient.Post(Routes.AccessKeyDelete, _managementKey, request); + } + + public async Task Load(string id) + { + if (string.IsNullOrEmpty(id)) throw new DescopeException("Access key ID is required to load"); + var result = await _httpClient.Get(Routes.AccessKeyLoad + $"?id={id}", _managementKey); + return result.Key; + } + + public async Task> SearchAll(List? tenantIds) + { + var request = new { tenantIds }; + var result = await _httpClient.Post(Routes.AccessKeySearch, _managementKey, request); + return result.Keys; + } + + } + + internal class LoadAccessKeyResponse + { + [JsonPropertyName("key")] + public AccessKeyResponse Key { get; set; } + + public LoadAccessKeyResponse(AccessKeyResponse key) + { + Key = key; + } + } + + public class SearchAccessKeyResponse + { + [JsonPropertyName("keys")] + public List Keys { get; set; } + + public SearchAccessKeyResponse(List keys) + { + Keys = keys; + } + } +} diff --git a/Descope/Internal/Management/Managment.cs b/Descope/Internal/Management/Managment.cs new file mode 100644 index 0000000..d1e3c25 --- /dev/null +++ b/Descope/Internal/Management/Managment.cs @@ -0,0 +1,23 @@ +namespace Descope.Internal.Management +{ + internal class Management : IManagement + { + public ITenant Tenant => _tenant; + public IUser User => _user; + public IAccessKey AccessKey => _accessKey; + public IProject Project => _project; + + private readonly Tenant _tenant; + private readonly User _user; + private readonly AccessKey _accessKey; + private readonly Project _project; + + public Management(IHttpClient client, string managementKey) + { + _tenant = new Tenant(client, managementKey); + _user = new User(client, managementKey); + _accessKey = new AccessKey(client, managementKey); + _project = new Project(client, managementKey); + } + } +} diff --git a/Descope/Internal/Management/Project.cs b/Descope/Internal/Management/Project.cs new file mode 100644 index 0000000..5675587 --- /dev/null +++ b/Descope/Internal/Management/Project.cs @@ -0,0 +1,50 @@ +namespace Descope.Internal.Management +{ + internal class Project : IProject + { + private readonly IHttpClient _httpClient; + private readonly string _managementKey; + + internal Project(IHttpClient httpClient, string managementKey) + { + _httpClient = httpClient; + _managementKey = managementKey; + } + + + public async Task Export() + { + return await _httpClient.Post(Routes.ProjectExport, _managementKey, null!); + } + + public async Task Import(object files) + { + if (files == null) throw new DescopeException("files missing"); + await _httpClient.Post(Routes.ProjectImport, _managementKey, files); + } + + public async Task Rename(string name) + { + if (string.IsNullOrEmpty(name)) throw new DescopeException("name missing"); + var request = new { name }; + await _httpClient.Post(Routes.ProjectRename, _managementKey, request); + } + + public async Task Clone(string name, string tag) + { + if (string.IsNullOrWhiteSpace(name)) throw new DescopeException("name missing"); + var request = new { name, tag }; + return await _httpClient.Post(Routes.ProjectClone, _managementKey, request); + } + + public async Task Delete(string projectId) + { + if (string.IsNullOrWhiteSpace(projectId)) throw new DescopeException("projectId missing"); + var config = new DescopeConfig(_httpClient.DescopeConfig) + { + ProjectId = projectId + }; + await new HttpClient(config).Post(Routes.ProjectDelete, _managementKey); + } + } +} diff --git a/Descope/Internal/Management/Tenant.cs b/Descope/Internal/Management/Tenant.cs new file mode 100644 index 0000000..f08b3f2 --- /dev/null +++ b/Descope/Internal/Management/Tenant.cs @@ -0,0 +1,101 @@ +using System.Text.Json.Serialization; + +namespace Descope.Internal.Management +{ + internal class Tenant : ITenant + { + private readonly IHttpClient _httpClient; + private readonly string _managementKey; + + internal Tenant(IHttpClient httpClient, string managementKey) + { + _httpClient = httpClient; + _managementKey = managementKey; + } + + public async Task Create(TenantOptions options, string? id = null) + { + if (string.IsNullOrEmpty(options.Name)) throw new DescopeException("Tenant name is required for creation"); + var body = new + { + id, + name = options.Name, + selfProvisioningDomains = options.SelfProvisioningDomains, + customAttributes = options.CustomAttributes + }; + var result = await _httpClient.Post(Routes.TenantCreate, _managementKey, body); + return result.Id; + } + + public async Task Update(string id, TenantOptions options) + { + if (string.IsNullOrEmpty(id)) throw new DescopeException("Tenant ID is required for update"); + if (string.IsNullOrEmpty(options.Name)) throw new DescopeException("Tenant name cannot be updated to empty"); + var body = new + { + id, + name = options.Name, + selfProvisioningDomains = options.SelfProvisioningDomains, + customAttributes = options.CustomAttributes + }; + await _httpClient.Post(Routes.TenantUpdate, _managementKey, body); + } + + public async Task Delete(string id) + { + if (string.IsNullOrEmpty(id)) throw new DescopeException("Tenant ID is required for deletion"); + var body = new { id }; + await _httpClient.Post(Routes.TenantDelete, _managementKey, body); + } + + public async Task LoadById(string id) + { + if (string.IsNullOrEmpty(id)) throw new DescopeException("Tenant ID is required to load By ID"); + return await _httpClient.Get(Routes.TenantLoad + $"?id={id}", _managementKey); + } + + public async Task> LoadAll() + { + var tenantList = await _httpClient.Get(Routes.TenantLoadAll, _managementKey); + return tenantList.Tenants; + } + + public async Task> SearchAll(TenantSearchOptions? options) + { + var body = new + { + tenantIds = options?.Ids, + tenantNames = options?.Names, + tenantSelfProvisioningDomains = options?.SelfProvisioningDomains, + customAttributes = options?.CustomAttributes, + authType = options?.AuthType, + }; + var tenantList = await _httpClient.Post(Routes.TenantSearch, _managementKey, body); + return tenantList.Tenants; + } + + } + + internal class TenantServerResponse + { + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonConstructor] + public TenantServerResponse(string id) + { + Id = id; + } + } + + internal class TenantListResponse + { + [JsonPropertyName("tenants")] + public List Tenants { get; set; } + + public TenantListResponse(List tenants) + { + Tenants = tenants; + } + } +} diff --git a/Descope/Internal/Management/User.cs b/Descope/Internal/Management/User.cs new file mode 100644 index 0000000..9f353c5 --- /dev/null +++ b/Descope/Internal/Management/User.cs @@ -0,0 +1,377 @@ +using System.Text.Json.Serialization; + +namespace Descope.Internal.Management +{ + internal class User : IUser + { + private readonly IHttpClient _httpClient; + private readonly string _managementKey; + + internal User(IHttpClient httpClient, string managementKey) + { + _httpClient = httpClient; + _managementKey = managementKey; + } + + #region IUser Implementation + + public async Task Create(string loginId, UserRequest? request, bool sendInvite, InviteOptions? inviteOptions, bool testUser) + { + request ??= new UserRequest(); + var body = MakeCreateUserRequestBody(loginId, request, sendInvite, inviteOptions, testUser); + var result = await _httpClient.Post(Routes.UserCreate, _managementKey, body); + return result.User; + } + + public async Task CreateBatch(List batchUsers, bool sendInvite, InviteOptions? inviteOptions) + { + batchUsers ??= new List(); + var body = MakeCreateBatchUsersRequestBody(batchUsers, sendInvite, inviteOptions); + return await _httpClient.Post(Routes.UserCreateBatch, _managementKey, body); + } + + public async Task Update(string loginId, UserRequest? request) + { + request ??= new UserRequest(); + var body = MakeUpdateUserRequestBody(loginId, request); + var result = await _httpClient.Post(Routes.UserUpdate, _managementKey, body); + return result.User; + } + + public async Task Activate(string loginId) + { + var result = await updateStatus(loginId, "enabled"); + return result; + } + + public async Task Deactivate(string loginId) + { + var result = await updateStatus(loginId, "disabled"); + return result; + } + + private async Task updateStatus(string loginId, string status) + { + var body = new { loginId, status }; + var result = await _httpClient.Post(Routes.UserUpdateStatus, _managementKey, body); + return result.User; + } + + public async Task UpdateLoginId(string loginId, string? newLoginId) + { + var body = new { loginId, newLoginId }; + var result = await _httpClient.Post(Routes.UserUpdateLoginId, _managementKey, body); + return result.User; + } + + public async Task UpdateEmail(string loginId, string? email, bool verified) + { + var body = new { loginId, email, verified }; + var result = await _httpClient.Post(Routes.UserUpdateEmail, _managementKey, body); + return result.User; + } + + public async Task UpdatePhone(string loginId, string? phone, bool verified) + { + var body = new { loginId, phone, verified }; + var result = await _httpClient.Post(Routes.UserUpdatePhone, _managementKey, body); + return result.User; + } + + public async Task UpdateDisplayName(string loginId, string? displayName) + { + var body = new { loginId, displayName }; + var result = await _httpClient.Post(Routes.UserUpdateName, _managementKey, body); + return result.User; + } + + public async Task UpdateUserNames(string loginId, string? givenName, string? middleName, string? familyName) + { + var body = new { loginId, givenName, middleName, familyName }; + var result = await _httpClient.Post(Routes.UserUpdateName, _managementKey, body); + return result.User; + } + + public async Task UpdatePicture(string loginId, string? picture) + { + var body = new { loginId, picture }; + var result = await _httpClient.Post(Routes.UserUpdatePicture, _managementKey, body); + return result.User; + } + + public async Task UpdateCustomAttributes(string loginId, string attributeKey, object attributeValue) + { + var body = new { loginId, attributeKey, attributeValue }; + var result = await _httpClient.Post(Routes.UserUpdateCustomAttribute, _managementKey, body); + return result.User; + } + + public async Task SetRoles(string loginId, List roleNames, string? tenantId) + { + var body = new { loginId, roleNames, tenantId }; + var result = await _httpClient.Post(Routes.UserRolesSet, _managementKey, body); + return result.User; + } + + public async Task AddRoles(string loginId, List roleNames, string? tenantId) + { + var body = new { loginId, roleNames, tenantId }; + var result = await _httpClient.Post(Routes.UserRolesAdd, _managementKey, body); + return result.User; + } + + public async Task RemoveRoles(string loginId, List roleNames, string? tenantId) + { + var body = new { loginId, roleNames, tenantId }; + var result = await _httpClient.Post(Routes.UserRoleRemove, _managementKey, body); + return result.User; + } + + public async Task SetSsoApps(string loginId, List ssoAppIds) + { + var body = new { loginId, ssoAppIds }; + var result = await _httpClient.Post(Routes.UserSsoAppSet, _managementKey, body); + return result.User; + } + + public async Task AddSsoApps(string loginId, List ssoAppIds) + { + var body = new { loginId, ssoAppIds }; + var result = await _httpClient.Post(Routes.UserSsoAppAdd, _managementKey, body); + return result.User; + } + + public async Task RemoveSsoApps(string loginId, List ssoAppIds) + { + var body = new { loginId, ssoAppIds }; + var result = await _httpClient.Post(Routes.UserSsoAppRemove, _managementKey, body); + return result.User; + } + + public async Task AddTenant(string loginId, string tenantId) + { + var body = new { loginId, tenantId }; + var result = await _httpClient.Post(Routes.UserTenantAdd, _managementKey, body); + return result.User; + } + + public async Task RemoveTenant(string loginId, string tenantId) + { + var body = new { loginId, tenantId }; + var result = await _httpClient.Post(Routes.UserTenantRemove, _managementKey, body); + return result.User; + } + + public async Task SetTemporaryPassword(string loginId, string password) + { + await SetPassword(loginId, password, false); + } + + public async Task SetActivePassword(string loginId, string password) + { + await SetPassword(loginId, password, true); + } + + private async Task SetPassword(string loginId, string password, bool setActive) + { + var body = new { loginId, password, setActive }; + await _httpClient.Post(Routes.UserPasswordSet, _managementKey, body); + } + + public async Task ExpirePassword(string loginId) + { + var body = new { loginId }; + await _httpClient.Post(Routes.UserPasswordExpire, _managementKey, body); + } + + public async Task RemoveAllPasskeys(string loginId) + { + var body = new { loginId }; + await _httpClient.Post(Routes.UserPasskeyRemoveAll, _managementKey, body); + } + + public async Task GetProviderToken(string loginId, string provider) + { + var result = await _httpClient.Get(Routes.UserProviderToken + $"?loginId={loginId}&provider={provider}", _managementKey); + return result; + } + + public async Task Logout(string? loginId, string? userId) + { + if (string.IsNullOrEmpty(loginId) && string.IsNullOrEmpty(userId)) throw new DescopeException("User loginId or userId are required to log out"); + var body = new { loginId, userId }; + await _httpClient.Post(Routes.UserLogout, _managementKey, body); + } + + public async Task Delete(string loginId) + { + var body = new { loginId }; + await _httpClient.Post(Routes.UserDelete, _managementKey, body); + } + + public async Task DeleteAllTestUsers() + { + await _httpClient.Delete(Routes.UserDeleteAllTestUsers, _managementKey); + } + + public async Task Load(string loginId) + { + var result = await _httpClient.Get(Routes.UserLoad + $"?loginId={loginId}", _managementKey); + return result.User; + } + + public async Task> SearchAll(SearchUserOptions? options) + { + options ??= new SearchUserOptions(); + var result = await _httpClient.Post(Routes.UserSearch, _managementKey, options); + return result.Users; + } + + public async Task GenerateOtpForTestUser(DeliveryMethod deliveryMethod, string loginId, LoginOptions? loginOptions) + { + if (string.IsNullOrEmpty(loginId)) throw new DescopeException("loginId missing"); + var body = new { loginId, deliveryMethod = deliveryMethod.ToString().ToLower(), loginOptions }; + return await _httpClient.Post(Routes.UserTestsGenerateOtp, _managementKey, body); + } + + public async Task GenerateMagicLinkForTestUser(DeliveryMethod deliveryMethod, string loginId, string? redirectUrl, LoginOptions? loginOptions) + { + if (string.IsNullOrEmpty(loginId)) throw new DescopeException("loginId missing"); + var body = new { loginId, deliveryMethod = deliveryMethod.ToString().ToLower(), redirectUrl, loginOptions }; + return await _httpClient.Post(Routes.UserTestsGenerateMagicLink, _managementKey, body); + } + + public async Task GenerateEnchantedLinkForTestUser(string loginId, string? redirectUrl, LoginOptions? loginOptions) + { + if (string.IsNullOrEmpty(loginId)) throw new DescopeException("loginId missing"); + var request = new { loginId, redirectUrl, loginOptions }; + return await _httpClient.Post(Routes.UserTestsGenerateEnchantedLink, _managementKey, request); + } + + public async Task GenerateEmbeddedLink(string loginId, Dictionary? customClaims) + { + if (string.IsNullOrEmpty(loginId)) throw new DescopeException("loginId missing"); + customClaims ??= new Dictionary(); + var body = new { loginId, customClaims }; + var result = await _httpClient.Post(Routes.UserTestsGenerateEmbeddedLink, _managementKey, body); + return result.Token; + } + + #endregion IUser Implementation + + #region Internal + + private static Dictionary MakeCreateUserRequestBody(string loginId, UserRequest request, bool sendInvite, InviteOptions? options, bool test) + { + var body = MakeUpdateUserRequestBody(loginId, request); + body["test"] = test; + body["invite"] = sendInvite; + if (options != null) + { + if (!string.IsNullOrEmpty(options.InviteUrl)) body["inviteUrl"] = options.InviteUrl; + body["sendMail"] = options.SendMail; + body["sendSMS"] = options.SendSms; + } + return body; + } + + private static Dictionary MakeCreateBatchUsersRequestBody(List users, bool sendInvite, InviteOptions? options) + { + var body = new Dictionary(); + var userList = new List>(); + foreach (var user in users) + { + var dict = MakeUpdateUserRequestBody(user.LoginId, user); + if (!string.IsNullOrEmpty(user.Password?.Cleartext)) + { + dict["password"] = user.Password.Cleartext; + } + if (user.Password?.Hashed != null) + { + dict["hashedPassword"] = user.Password.Hashed; + } + userList.Add(dict); + } + body["users"] = userList; + body["invite"] = sendInvite; + if (options != null) + { + if (!string.IsNullOrEmpty(options.InviteUrl)) body["inviteUrl"] = options.InviteUrl; + body["sendMail"] = options.SendMail; + body["sendSMS"] = options.SendSms; + } + return body; + } + + private static Dictionary MakeUpdateUserRequestBody(string loginId, UserRequest request) + { + var body = new Dictionary + { + {"loginId", loginId}, + {"verifiedEmail", request.VerifiedEmail}, + {"verifiedPhone", request.VerifiedPhone}, + }; + if (!string.IsNullOrEmpty(request.Email)) body["email"] = request.Email; + if (!string.IsNullOrEmpty(request.Phone)) body["phone"] = request.Phone; + if (!string.IsNullOrEmpty(request.Name)) body["displayName"] = request.Name; + if (!string.IsNullOrEmpty(request.GivenName)) body["givenName"] = request.GivenName; + if (!string.IsNullOrEmpty(request.MiddleName)) body["middleName"] = request.MiddleName; + if (!string.IsNullOrEmpty(request.FamilyName)) body["familyName"] = request.FamilyName; + if (request.RoleNames != null) body["roleNames"] = request.RoleNames; + if (request.UserTenants != null) body["userTenants"] = MakeAssociatedTenantList(request.UserTenants); + if (request.CustomAttributes != null) body["customAttributes"] = request.CustomAttributes; + if (!string.IsNullOrEmpty(request.Picture)) body["picture"] = request.Picture; + if (request.AdditionalLoginIds != null) body["additionalLoginIds"] = request.AdditionalLoginIds; + if (request.SsoAppIds != null) body["ssoAppIDs"] = request.SsoAppIds; + return body; + } + + private static List> MakeAssociatedTenantList(List tenants) + { + tenants ??= new List(); + var list = new List>(); + foreach (var tenant in tenants) + { + var dict = new Dictionary { { "tenantId", tenant.TenantId } }; + if (tenant.RoleNames != null) dict["roleNames"] = tenant.RoleNames; + list.Add(dict); + }; + return list; + } + + #endregion Internal + } + + internal class WrappedUserResponse + { + [JsonPropertyName("user")] + public UserResponse User { get; set; } + + public WrappedUserResponse(UserResponse user) + { + User = user; + } + } + + internal class WrappedUsersResponse + { + [JsonPropertyName("users")] + public List Users { get; set; } + + public WrappedUsersResponse(List users) + { + Users = users; + } + } + + internal class GenerateEmbeddedLinkResponse + { + [JsonPropertyName("token")] + internal string Token { get; set; } + + internal GenerateEmbeddedLinkResponse(string token) + { + Token = token; + } + } +} diff --git a/Descope/Sdk/Authentication.cs b/Descope/Sdk/Authentication.cs new file mode 100644 index 0000000..677bf81 --- /dev/null +++ b/Descope/Sdk/Authentication.cs @@ -0,0 +1,133 @@ +namespace Descope +{ + /// + /// Provides functions for authenticating users using OTP (one-time password) + /// + public interface IOtp + { + Task SignUp(DeliveryMethod deliveryMethod, string loginId, SignUpDetails? details = null); + + Task SignIn(DeliveryMethod deliveryMethod, string loginId, LoginOptions? loginOptions = null); + + Task SignUpOrIn(DeliveryMethod deliveryMethod, string loginId, LoginOptions? loginOptions = null); + + Task UpdateEmail(string loginId, string email, string refreshJwt, UpdateOptions? updateOptions = null); + + Task UpdatePhone(string loginId, string phone, string refreshJwt, UpdateOptions? updateOptions = null); + + Task Verify(DeliveryMethod deliveryMethod, string loginId, string code); + } + + /// + /// Provides various APIs for authenticating and authorizing users of a Descope project. + /// + public interface IAuthentication + { + /// + /// Provides functions for authenticating users using OTP (one-time password) + /// + public IOtp Otp { get; } + + /// + /// Validate a session JWT. + /// + /// Should be called before any private API call that requires authorization. + /// + /// + /// The session JWT to validate + /// A valid session token if valid + Task ValidateSession(string sessionJwt); + + /// + /// Refresh an expired session with a given refresh JWT. + /// + /// Should be called when a session has expired (failed validation) to renew it. + /// + /// + /// A valid refresh JWT + /// A refreshed session token + Task RefreshSession(string refreshJwt); + + /// + /// Validate a session JWT. If the session has expired, it will automatically be + /// renewed by using the provided refresh JWT. + /// + /// + /// + /// A valid, potentially refreshed, session token + Task ValidateAndRefreshSession(string sessionJwt, string refreshJwt); + + /// + /// Exchange an access key for a session token. + /// + /// The accessKey cleartext to exchange + /// Optional login options for the exchange + /// A valid session token if successful + Task ExchangeAccessKey(string accessKey, AccessKeyLoginOptions? loginOptions = null); + + /// + /// Ensure a validated session token has been granted the specified permissions. + /// + /// A valid session token + /// A list of permission to check + /// Provide a tenant ID if the permission belongs to a specific tenant + /// True if the token has been granted the given permission + bool ValidatePermissions(Token token, List permissions, string? tenant = null); + + /// + /// Retrieves the permissions from top level token's claims, or for the provided + /// tenant's claim, that match the specified permissions list. + /// + /// A valid session token + /// A list of permission to check + /// Provide a tenant ID if the permission belongs to a specific tenant + /// A list of matched permissions + List GetMatchedPermissions(Token token, List permissions, string? tenant = null); + + /// + /// Ensure a validated session token has been granted the specified roles. + /// + /// A valid session token + /// A list of roles to check + /// Provide a tenant ID if the roles belongs to a specific tenant + /// True if the token has been granted the given roles + bool ValidateRoles(Token token, List roles, string? tenant = null); + + /// + /// Retrieves the roles from top level token's claims, or for the provided + /// tenant's claim, that match the specified role list. + /// + /// A valid session token + /// A list of roles to check + /// Provide a tenant ID if the roles belongs to a specific tenant + /// A list of matched roles + List GetMatchedRoles(Token token, List roles, string? tenant = null); + + /// + /// Adds a dedicated claim to the JWTs to indicate which tenant the user is currently authenticated to. + /// + /// The tenant the user is logged into + /// A valid refresh JWT + /// An updated session with a new claim indicating which tenant the user is currently logged into + Task SelectTenant(string tenant, string refreshJwt); + + /// + /// Logs out from the current session. + /// + /// A valid refresh JWT + Task LogOut(string refreshJwt); + + /// + /// Logout from all active sessions for the request user. + /// + /// A valid refresh JWT + Task LogOutAll(string refreshJwt); + + /// + /// Retrieve the current session user details. + /// + /// A valid refresh JWT + /// The current session user details + Task Me(string refreshJwt); + } +} diff --git a/Descope/Sdk/DescopeConfig.cs b/Descope/Sdk/DescopeConfig.cs new file mode 100644 index 0000000..5e95622 --- /dev/null +++ b/Descope/Sdk/DescopeConfig.cs @@ -0,0 +1,23 @@ +namespace Descope +{ + public class DescopeConfig + { + public string ProjectId { get; set; } + public string? ManagementKey { get; set; } + public string? BaseURL { get; set; } = null; + public bool Unsafe { get; set; } = false; + + public DescopeConfig(string projectId) + { + ProjectId = projectId; + } + + public DescopeConfig(DescopeConfig other) + { + ProjectId = other.ProjectId; + ManagementKey = other.ManagementKey; + BaseURL = other.BaseURL; + Unsafe = other.Unsafe; + } + } +} diff --git a/Descope/Sdk/Managment.cs b/Descope/Sdk/Managment.cs new file mode 100644 index 0000000..c53be58 --- /dev/null +++ b/Descope/Sdk/Managment.cs @@ -0,0 +1,607 @@ +namespace Descope +{ + + /// + /// Provides functions for managing tenants in a project. + /// + public interface ITenant + { + /// + /// Create a new tenant with the given name. + /// + /// options.SelfProvisioningDomains is an optional list of domains that are associated with this + /// tenant. Users authenticating from these domains will be associated with this tenant. + /// + /// + /// The tenant tenantRequest.Name must be unique per project. + /// The tenant ID is generated automatically for the tenant, unless given explicitly by the ID. + /// + /// + /// The options to create a tenant according to + /// Optional ID to use for the tenant. Leave null to it automatically generated + /// The newly created tenant's ID + Task Create(TenantOptions options, string? id = null); + + /// + /// Update an existing tenant's name and domains. + /// + /// IMPORTANT: All parameters are required and will override whatever value is currently + /// set in the existing tenant. Use carefully. + /// + /// + /// The ID of the tenant to update + /// The tenants updated details + Task Update(string id, TenantOptions options); + + /// + /// Delete an existing tenant. + /// + /// IMPORTANT: This action is irreversible. Use carefully. + /// + /// + /// The ID of the tenant to delete + Task Delete(string id); + + /// + /// Load project tenant by id + /// + /// + /// The loaded tenant + Task LoadById(string id); + + /// + /// Load all project tenants + /// + /// + Task> LoadAll(); + + /// + /// Search all tenants according to given filters + /// + /// The options optional parameter allows to fine-tune the search filters + /// and results. Using nil will result in a filter-less query with a set amount of + /// results. + /// + /// + /// Fine tune filters for the search + /// A list of found tenants + Task> SearchAll(TenantSearchOptions? options = null); + } + + /// + /// Provides functions for managing users in a project + /// + public interface IUser + { + /// + /// Create a new user. + /// + /// The loginID is required and will determine what the user will use to + /// sign in. + /// + /// + /// IMPORTANT: When opting into invitations, since the invitation is sent by email / phone, make sure either + /// the email / phone is explicitly set, or the loginId itself is an email address / phone number. + /// You must configure the invitation URL in the Descope console prior to + /// calling the method. + /// + /// + /// A login ID to identify the created user + /// Optional information about the user being created + /// Whether or not to send an invitation to the user + /// Optional invite options used to send an invitation to the created user + /// Optionally create a test user + /// The created user + Task Create(string loginId, UserRequest? request = null, bool sendInvite = false, InviteOptions? inviteOptions = null, bool testUser = false); + + /// + /// Create users in batch. + /// + /// Functions exactly the same as the Create function with the additional behavior that + /// users can be created with a cleartext or hashed password. + /// + /// + /// IMPORTANT: When opting into invitations, since the invitation is sent by email / phone, make sure either + /// the email / phone is explicitly set, or the loginId itself is an email address / phone number. + /// You must configure the invitation URL in the Descope console prior to + /// calling the method. + /// + /// + /// The list of users to create + /// Whether or not to send an invitation to the users + /// Optional invite options used to send an invitation to the created users + /// A list of created users and a list of failures if occurred + Task CreateBatch(List batchUsers, bool sendInvite = false, InviteOptions? inviteOptions = null); + + /// + /// Update an existing user. + /// + /// The parameters follow the same convention as those for the Create function. + /// + /// + /// IMPORTANT: All parameters will override whatever values are currently set + /// in the existing user. Use carefully. + /// + /// The login ID of the user to update + /// The information to set + /// The updated user + Task Update(string loginId, UserRequest? request = null); + + /// + /// Activate an existing user. + /// + /// The login ID of the user to activate + /// The activated user + Task Activate(string loginId); + + /// + /// Deactivate an existing user. + /// + /// The login ID of the user to deactivate + /// The deactivated user + Task Deactivate(string loginId); + + /// + /// Change current loginID to new one. + /// + /// Leave null to remove the current login ID. + /// Pay attention that if this is the only login ID, it cannot be removed + /// + /// + /// The login ID of the user to update + /// The new login ID + /// The updated user + Task UpdateLoginId(string loginId, string? newLoginId = null); + + /// + /// Update the email address for an existing user. + /// + /// The email parameter can be null in which case the email will be removed. + /// + /// + /// The verified flag must be true for the user to be able to login with + /// the email address. + /// + /// + /// The login ID of the user to update + /// The email to update + /// Whether this email is verified + /// The updated user + Task UpdateEmail(string loginId, string? email = null, bool verified = false); + + /// + /// Update the phone number for an existing user. + /// + /// The phone parameter can be null in which case the phone will be removed. + /// + /// + /// The verified flag must be true for the user to be able to login with + /// the phone number. + /// + /// + /// The login ID of the user to update + /// The phone to update + /// Whether this phone is verified + /// The updated user + Task UpdatePhone(string loginId, string? phone = null, bool verified = false); + + /// + /// Update an existing user's display name (i.e., their full name). + /// + /// The displayName parameter can be null in which case the name will be removed. + /// + /// + /// The login ID of the user to update< + /// The display name to update + /// The updated user + Task UpdateDisplayName(string loginId, string? displayName = null); + + /// + /// Update an existing user's first/last/middle name. + /// + /// A null parameter, means that this value will be removed. + /// + /// + /// The login ID of the user to update + /// The given name to update + /// The middle name to update + /// The family name to update + /// The updated user + Task UpdateUserNames(string loginId, string? givenName = null, string? middleName = null, string? familyName = null); + + /// + /// Update an existing user's picture (i.e., url to the avatar). + /// + /// The picture parameter can be null in which case the picture will be removed. + /// + /// + /// The login ID of the user to update + /// The updated picture URL + /// The updated user + Task UpdatePicture(string loginId, string? picture); + + /// + /// Update an existing user's custom attribute. + /// + /// key should be a custom attribute that was already declared in the Descope console app. + /// value should match the type of the declared attribute + /// + /// + /// The login ID of the user to update + /// Existing attribute key + /// Value matching the given key + /// The updated user + Task UpdateCustomAttributes(string loginId, string key, object value); + + /// + /// Set roles for a user. If the intended roles are associated with a tenant, provide + /// a tenantId. + /// + /// The login ID of the user to update + /// The roles to set + /// Optional tenant association + /// The updated user + Task SetRoles(string loginId, List roleNames, string? tenantId = null); + + /// + /// Add roles for a user. If the intended roles are associated with a tenant, provide + /// a tenantId. + /// + /// The login ID of the user to update + /// The roles to add + /// Optional tenant association + /// The updated user + Task AddRoles(string loginId, List roleNames, string? tenantId = null); + + /// + /// Remove roles from a user. If the intended roles are associated with a tenant, provide + /// a tenantId. + /// + /// The login ID of the user to update + /// The roles to remove + /// Optional tenant association + /// The updated user + Task RemoveRoles(string loginId, List roleNames, string? tenantId = null); + + /// + /// Set (associate) SSO applications for a user. + /// + /// The login ID of the user to update + /// The SSO app IDs to set + /// The updated user + Task SetSsoApps(string loginId, List ssoAppIds); + + /// + /// Associate SSO application for a user. + /// + /// The login ID of the user to update + /// The SSO app IDs to add + /// The updated user + Task AddSsoApps(string loginId, List ssoAppIds); + + /// + /// Remove SSO application association from a user. + /// + /// The login ID of the user to update + /// The SSO app IDs to remove + /// The updated user + Task RemoveSsoApps(string loginId, List ssoAppIds); + + /// + /// Add a tenant association for an existing user. + /// + /// The login ID of the user to update + /// The tenant ID to add + /// The updated user + Task AddTenant(string loginId, string tenantId); + + /// + /// Remove a tenant association from an existing user. + /// + /// The login ID of the user to update + /// The tenant ID to remove + /// The updated user + Task RemoveTenant(string loginId, string tenantId); + + /// + /// Set a temporary password for the given login ID. + /// + /// Note: The password will automatically be set as expired. + /// The user will not be able to log-in with this password, and will be required to replace it on next login. + /// See also: ExpirePassword + /// + /// + /// The login ID of the user to update + /// The temporary password to set + Task SetTemporaryPassword(string loginId, string password); + + /// + /// Set a password for the given login ID. + /// + /// The password will not be expired on the next login. + /// + /// + /// The login ID of the user to update + /// The active password to set + Task SetActivePassword(string loginId, string password); + + /// + /// Expire the password for the given login ID. + /// + /// Note: user sign-in with an expired password, the user will get `errors.ErrPasswordExpired` error. + /// Use the `SendPasswordReset` or `ReplaceUserPassword` methods to reset/replace the password. + /// + /// + /// The login ID of the user to update + Task ExpirePassword(string loginId); + + /// + /// Removes all registered passkeys (WebAuthn devices) for the user with the given login ID. + /// + /// Note: The user might not be able to login anymore if they have no other authentication + /// methods or a verified email/phone. + /// + /// + /// The login ID of the user to update + Task RemoveAllPasskeys(string loginId); + + /// + /// Get the provider token for the given login ID. + /// + /// Only users that sign-in using social providers will have token. + /// Note: The 'Manage tokens from provider' setting must be enabled. + /// + /// + /// The login ID of the user to fetch tokens for + /// The provider to fetch from + /// The provider token for the given user + Task GetProviderToken(string loginId, string provider); + + /// + /// Logout given user from all their devices, by loginId or userId + /// + /// The login ID of the user to logout. Alternatively, provide the userId + /// The user ID of the user to logout. Alternatively, provide the loginId + Task Logout(string? loginId = null, string? userId = null); + + /// + /// Delete an existing user. + /// + /// IMPORTANT: This action is irreversible. Use carefully. + /// + /// + /// The login ID of the user to delete + Task Delete(string loginId); + + /// + /// Delete all test users in the project. + /// + /// IMPORTANT: This action is irreversible. Use carefully. + /// + /// + Task DeleteAllTestUsers(); + + /// + /// Load an existing user. + /// + /// The login ID of the user to load + /// The loaded user + Task Load(string loginId); + + /// + /// Search all users according to given filters + /// + /// The options optional parameter allows to fine-tune the search filters + /// and results. Using nil will result in a filter-less query with a set amount of + /// results. + /// + /// + /// Parameter to fine tune the search by + /// A list of found users + Task> SearchAll(SearchUserOptions? options = null); + + /// + /// Generate OTP for the given login ID of a test user. + /// + /// Choose the selected delivery method for verification. (see auth/DeliveryMethod) + /// It returns the code for the login (exactly as it sent via Email or SMS) + /// This is useful when running tests and don't want to use 3rd party messaging services + /// The redirect URI is optional. If provided however, it will be used instead of any global configuration. + /// + /// + /// The intended delivery method + /// The login ID of the test user + /// Optional login options + /// The generated otp response + Task GenerateOtpForTestUser(DeliveryMethod deliveryMethod, string loginId, LoginOptions? loginOptions = null); + + /// + /// Generate Magic Link for the given login ID of a test user. + /// + /// Choose the selected delivery method for verification. (see auth/DeliveryMethod) + /// It returns the link for the login (exactly as it sent via Email) + /// This is useful when running tests and don't want to use 3rd party messaging services + /// The redirect URI is optional. If provided however, it will be used instead of any global configuration. + /// + /// + /// The intended delivery method + /// The login ID of the test user + /// + /// Optional login options + /// The generated magic link response + Task GenerateMagicLinkForTestUser(DeliveryMethod deliveryMethod, string loginId, string? redirectUrl = null, LoginOptions? loginOptions = null); + + /// + /// Generate Enchanted Link for the given login ID of a test user. + /// + /// It returns the link for the login (exactly as it sent via Email) and pendingRef which is used to poll for a valid session + /// This is useful when running tests and don't want to use 3rd party messaging services + /// The redirect URI is optional. If provided however, it will be used instead of any global configuration. + /// + /// + /// The login ID of the test user + /// + /// Optional login options + /// The generated enchanted link response + Task GenerateEnchantedLinkForTestUser(string loginId, string? redirectUrl = null, LoginOptions? loginOptions = null); + + /// + /// Generate an embedded link token, later can be used to authenticate via magiclink verify method + /// or via flow verify step + /// + /// The login ID of the test user + /// Optional custom claims to be placed on the generated JWT after login + /// The generated embedded link response + Task GenerateEmbeddedLink(string loginId, Dictionary? customClaims = null); + } + + /// + /// Provides functions for managing access keys in a project. + /// + public interface IAccessKey + { + /// + /// Create a new access key. + /// + /// IMPORTANT: The access key cleartext will be returned only when first created. + /// Make sure to save it in a secure manner. + /// + /// + /// The access key's name. It doesn't have to be unique + /// Optional expiration time, leave null to make indefinite + /// An optional list of the access key's roles for access keys that aren't associated with a tenant + /// An optional list of tenants to associate the access key with and what roles the access key has in each one + /// If userID is supplied, then authorization would be ignored, and access key will be bound to the users authorization + /// A newly created access key along with its cleartext + Task Create(string name, int? expireTime = null, List? roleNames = null, List? keyTenants = null, string? userId = null); + + /// + /// Load an existing access key. + /// + /// The ID of the access key to update + /// The key's updated name + /// The updated access key + Task Update(string id, string name); + + /// + /// Activate an existing access key. + /// + /// The ID of the access key to activate + Task Activate(string id); + + /// + /// Deactivate an existing access key. + /// + /// The ID of the access key to deactivate + Task Deactivate(string id); + + /// + /// Delete an existing access key. + /// + /// The ID of the access key to delete + Task Delete(string id); + + /// + /// Load an existing access key. + /// + /// The ID of the access key to load + /// The loaded access key + Task Load(string id); + + /// + /// Search all access keys according to given filters + /// + /// Optional list of tenant IDs to filter by. + /// A list of found access keys + Task> SearchAll(List? tenantIds = null); + } + + /// + /// Provide functions for manipulating valid JWT + /// + public interface IJwt + { + /// + /// Update a valid JWT with the custom claims provided + /// + /// A valid JWT to update + /// The custom claims to be added to the JWT + /// An updated JWT + Task UpdateJwtWithCustomClaims(string jwt, Dictionary customClaims); + } + + /// + /// Provides functions for exporting and importing project settings, flows, styles, etc. + /// + public interface IProject + { + /// + /// Exports all settings and configurations for a project and returns the raw JSON + /// files response as a map. + /// + /// It's advised to use descopeCLI for easier importing and exporting + /// + /// + /// The exported project + Task Export(); + + /// + /// Imports all settings and configurations for a project overriding any current configuration. + /// + /// The result of an exported project + Task Import(object files); + + /// + /// Update the current project name. + /// + /// The project's new name + Task Rename(string name); + + /// + /// Clone the current project, including its settings and configurations. + /// + /// - This action is supported only with a pro license or above. + /// + /// + /// - Users, tenants and access keys are not cloned. + /// + /// + /// + /// + /// The new project details (name, id, and tag) + Task Clone(string name, string tag); + + /// + /// Delete a project. + /// + /// The ID of the project to be deleted + Task Delete(string projectId); + } + + /// + /// Provides various APIs for managing a Descope project programmatically. A management key must + /// be provided in the DescopeClient configuration. Management keys can be generated in the Descope console. + /// + public interface IManagement + { + /// + /// Provides functions for managing tenants in a project. + /// + public ITenant Tenant { get; } + + /// + /// Provides functions for managing users in a project. + /// + public IUser User { get; } + + /// + /// Provides functions for managing access keys in a project. + /// + public IAccessKey AccessKey { get; } + + /// + /// Provides functions for exporting and importing project settings, flows, styles, etc. + /// + public IProject Project { get; } + } +} diff --git a/Descope/Types/Types.cs b/Descope/Types/Types.cs new file mode 100644 index 0000000..2b04e11 --- /dev/null +++ b/Descope/Types/Types.cs @@ -0,0 +1,601 @@ + +using System.Text.Json.Serialization; +using Microsoft.IdentityModel.JsonWebTokens; + +namespace Descope +{ + + public class SignUpDetails + { + [JsonPropertyName("name")] + public string? Name { get; set; } + [JsonPropertyName("email")] + public string? Email { get; set; } + [JsonPropertyName("phone")] + public string? Phone { get; set; } + [JsonPropertyName("givenName")] + public string? GivenName { get; set; } + [JsonPropertyName("middleName")] + public string? MiddleName { get; set; } + [JsonPropertyName("familyName")] + public string? FamilyName { get; set; } + } + + public class LoginOptions + { + [JsonPropertyName("stepup")] + public bool StepUp { get; set; } + + [JsonPropertyName("customClaims")] + public Dictionary? CustomClaims { get; set; } + + [JsonPropertyName("mfa")] + public bool Mfa { get; set; } + } + + public class UpdateOptions + { + public bool AddToLoginIds { get; set; } = false; + public bool OnMergeUseExisting { get; set; } = false; + } + + public class AuthenticationResponse + { + [JsonPropertyName("sessionJwt")] + public string SessionJwt { get; set; } + + [JsonPropertyName("refreshJwt")] + public string? RefreshJwt { get; set; } + + [JsonPropertyName("cookieDomain")] + public string CookieDomain { get; set; } + + [JsonPropertyName("cookiePath")] + public string CookiePath { get; set; } + + [JsonPropertyName("cookieMaxAge")] + public int CookieMaxAge { get; set; } + + [JsonPropertyName("cookieExpiration")] + public int CookieExpiration { get; set; } + + [JsonPropertyName("user")] + public UserResponse User { get; set; } + + [JsonPropertyName("firstSeen")] + public bool FirstSeen { get; set; } + public AuthenticationResponse(string sessionJwt, string? refreshJwt, string cookieDomain, string cookiePath, int cookieMaxAge, int cookieExpiration, UserResponse user, bool firstSeen) + { + SessionJwt = sessionJwt; + RefreshJwt = refreshJwt; + CookieDomain = cookieDomain; + CookiePath = cookiePath; + CookieMaxAge = cookieMaxAge; + CookieExpiration = cookieExpiration; + User = user; + FirstSeen = firstSeen; + } + } + + public class Session + { + public Token SessionToken { get; set; } + public Token RefreshToken { get; set; } + public UserResponse User { get; set; } + public bool FirstSeen { get; set; } + public Session(Token sessionToken, Token refreshToken, UserResponse user, bool firstSeen) + { + SessionToken = sessionToken; + RefreshToken = refreshToken; + User = user; + FirstSeen = firstSeen; + } + } + + public class Token + { + [JsonPropertyName("jwt")] + public string Jwt { get; set; } + [JsonPropertyName("id")] + public string Id { get; set; } + [JsonPropertyName("projectId")] + public string ProjectId { get; set; } + [JsonPropertyName("expiration")] + public DateTime Expiration { get; set; } + [JsonPropertyName("claims")] + public Dictionary Claims { get; set; } + [JsonPropertyName("refreshExpiration")] + public DateTime? RefreshExpiration { get; set; } + public Token(string jwt, string id, string projectId, DateTime expiration, Dictionary claims, DateTime? refreshExpiration = null) + { + Jwt = jwt; + Id = id; + ProjectId = projectId; + Expiration = expiration; + Claims = claims; + RefreshExpiration = refreshExpiration; + } + public Token(JsonWebToken jsonWebToken) + { + Jwt = jsonWebToken.EncodedToken; + Id = jsonWebToken.Subject; + Expiration = jsonWebToken.ValidTo; + var parts = jsonWebToken.Issuer.Split("/"); + ProjectId = parts.Last(); + Claims = new Dictionary(); + foreach (var claim in jsonWebToken.Claims) + { + Claims[claim.Type] = claim.Value; + } + } + + internal List GetTenants() + { + return new List(GetTenantsClaim().Keys); + } + + internal object? GetTenantValue(string tenant, string key) + { + return (GetTenantsClaim()[tenant] is Dictionary info) ? info[key] : null; + } + + private Dictionary GetTenantsClaim() + { + return Claims["tenants"] as Dictionary ?? new Dictionary(); + } + } + + public enum DeliveryMethod + { + Email, Sms, Whatsapp + } + + public class UserResponse + { + [JsonPropertyName("loginIds")] + public List LoginIds { get; set; } + [JsonPropertyName("userId")] + public string UserId { get; set; } + [JsonPropertyName("name")] + public string? Name { get; set; } + [JsonPropertyName("givenName")] + public string? GivenName { get; set; } + [JsonPropertyName("middleName")] + public string? MiddleName { get; set; } + [JsonPropertyName("familyName")] + public string? FamilyName { get; set; } + [JsonPropertyName("email")] + public string? Email { get; set; } + [JsonPropertyName("phone")] + public string? Phone { get; set; } + [JsonPropertyName("verifiedEmail")] + public bool VerifiedEmail { get; set; } + [JsonPropertyName("verifiedPhone")] + public bool VerifiedPhone { get; set; } + [JsonPropertyName("roleNames")] + public List? RoleNames { get; set; } + [JsonPropertyName("userTenants")] + public List? UserTenants { get; set; } + [JsonPropertyName("status")] + public string Status { get; set; } + [JsonPropertyName("picture")] + public string? Picture { get; set; } + [JsonPropertyName("test")] + public bool Test { get; set; } + [JsonPropertyName("customAttributes")] + public Dictionary? CustomAttributes { get; set; } + [JsonPropertyName("createdTime")] + public int CreatedTime { get; set; } + [JsonPropertyName("totp")] + public bool Totp { get; set; } + [JsonPropertyName("webauthn")] + public bool Webauthn { get; set; } + [JsonPropertyName("password")] + public bool Password { get; set; } + [JsonPropertyName("saml")] + public bool Saml { get; set; } + [JsonPropertyName("oauth")] + public Dictionary? oauth { get; set; } + [JsonPropertyName("ssoAppIds")] + public List? SsoAppIds { get; set; } + public UserResponse(List loginIds, string userId, string status) + { + LoginIds = loginIds; + UserId = userId; + Status = status; + } + } + + public class BatchCreateUserResponse + { + [JsonPropertyName("createdUsers")] + public List CreatedUsers { get; set; } + + [JsonPropertyName("failedUsers")] + public List FailedUsers { get; set; } + + public BatchCreateUserResponse(List createdUsers, List failedUsers) + { + CreatedUsers = createdUsers; + FailedUsers = failedUsers; + } + + } + + public class UsersFailedResponse + { + [JsonPropertyName("failure")] + public string Failure { get; set; } + [JsonPropertyName("user")] + public UserResponse User { get; set; } + + public UsersFailedResponse(string failure, UserResponse user) + { + Failure = failure; + User = user; + } + } + + public class UserRequest + { + public string? Name { get; set; } + public string? GivenName { get; set; } + public string? MiddleName { get; set; } + public string? FamilyName { get; set; } + public string? Email { get; set; } + public string? Phone { get; set; } + public List? RoleNames { get; set; } + public List? UserTenants { get; set; } + public Dictionary? CustomAttributes { get; set; } + public string? Picture { get; set; } + public bool VerifiedEmail { get; set; } + public bool VerifiedPhone { get; set; } + public List? AdditionalLoginIds { get; set; } + public List? SsoAppIds { get; set; } + } + + public class BatchUser : UserRequest + { + public string LoginId { get; set; } + + public BatchUserPassword? Password { get; set; } + + public BatchUser(string loginId) + { + LoginId = loginId; + } + } + + public class BatchUserPassword + { + public string? Cleartext { get; set; } + public BatchUserPasswordHashed? Hashed { get; set; } + } + + public class BatchUserPasswordHashed + { + public BatchUserPasswordBcrypt? Bcrypt { get; set; } + public BatchUserPasswordFirebase? Firebase { get; set; } + public BatchUserPasswordPbkdf2? Pbkdf2 { get; set; } + public BatchUserPasswordDjango? Django { get; set; } + } + + public class BatchUserPasswordBcrypt + { + public string Hash { get; set; } + + public BatchUserPasswordBcrypt(string hash) + { + Hash = hash; + } + } + + public class BatchUserPasswordFirebase + { + public byte[] Hash { get; set; } // the hash in raw bytes (base64 strings should be decoded first) + public byte[] Salt { get; set; } // the salt in raw bytes (base64 strings should be decoded first) + public byte[] SaltSeparator { get; set; } // the salt separator (usually 1 byte long) + public byte[] SignerKey { get; set; } // the signer key (base64 strings should be decoded first) + public int Memory { get; set; } // the memory cost value (usually between 12 to 17) + public int Rounds { get; init; } // the rounds cost value (usually between 6 to 10) + + public BatchUserPasswordFirebase(byte[] hash, byte[] salt, byte[] saltSeparator, byte[] signerKey, int memory, int rounds) + { + Hash = hash; + Salt = salt; + SaltSeparator = saltSeparator; + SignerKey = signerKey; + Memory = memory; + Rounds = rounds; + } + } + + public class BatchUserPasswordPbkdf2 + { + public byte[] Hash { get; set; } // the hash in raw bytes (base64 strings should be decoded first) + public byte[] Salt { get; set; } // the salt in raw bytes (base64 strings should be decoded first) + public int Iterations { get; set; } // the iterations cost value (usually in the thousands) + public string Type { get; set; } // the hash name (sha1, sha256, sha512) + public BatchUserPasswordPbkdf2(byte[] hash, byte[] salt, int iterations, string type) + { + Hash = hash; + Salt = salt; + Iterations = iterations; + Type = type; + } + } + + public class BatchUserPasswordDjango + { + public string Hash { get; set; } + + public BatchUserPasswordDjango(string hash) + { + Hash = hash; + } + } + + public class InviteOptions + { + public string? InviteUrl { get; set; } + public bool SendMail { get; set; } // send invite via mail, default is according to project settings + public bool SendSms { get; set; } // send invite via text message, default is according to project settings + } + + // Options for searching and filtering users + // + // Limit - limits the number of returned users. Leave at 0 to return the default amount. + // Page - allows to paginate over the results. Pages start at 0 and must non-negative. + // Sort - allows to sort by fields. + // Text - allows free text search among all user's attributes. + // TenantIDs - filter by tenant IDs. + // Roles - filter by role names. + // CustomAttributes map is an optional filter for custom attributes: + // where the keys are the attribute names and the values are either a value we are searching for or list of these values in a slice. + // We currently support string, int and bool values + public class SearchUserOptions + { + [JsonPropertyName("page")] + public int Page { get; set; } + [JsonPropertyName("limit")] + public int Limit { get; set; } + [JsonPropertyName("sort")] + public List? Sort; + [JsonPropertyName("text")] + public string? Text { get; set; } + [JsonPropertyName("emails")] + public List? Emails { get; set; } + [JsonPropertyName("phones")] + public List? Phones { get; set; } + [JsonPropertyName("statuses")] + public List? Statuses { get; set; } + [JsonPropertyName("roles")] + public List? Roles { get; set; } + [JsonPropertyName("tenantIds")] + public List? TenantIds { get; set; } + [JsonPropertyName("ssoAppIDs")] + public List? SsoAppIds { get; set; } + [JsonPropertyName("customAttributes")] + public Dictionary? CustomAttributes { get; set; } + [JsonPropertyName("withTestUsers")] + public bool WithTestUsers { get; set; } + [JsonPropertyName("testUsersOnly")] + public bool TestUsersOnly { get; set; } + } + + public class UserSearchSort + { + [JsonPropertyName("field")] + public string Field { get; set; } + [JsonPropertyName("desc")] + public bool Desc { get; set; } + public UserSearchSort(string field, bool desc) + { + Field = field; + Desc = desc; + } + } + + public class UserTestOTPResponse + { + [JsonPropertyName("loginId")] + public string LoginId { get; set; } + + [JsonPropertyName("code")] + public string Code { get; set; } + + public UserTestOTPResponse(string loginId, string code) + { + LoginId = loginId; + Code = code; + } + } + + public class UserTestMagicLinkResponse + { + [JsonPropertyName("loginId")] + public string LoginId { get; set; } + + [JsonPropertyName("link")] + public string Link { get; set; } + + public UserTestMagicLinkResponse(string loginId, string link) + { + LoginId = loginId; + Link = link; + } + } + + public class UserTestEnchantedLinkResponse + { + [JsonPropertyName("loginId")] + public string LoginId { get; set; } + + [JsonPropertyName("link")] + public string Link { get; set; } + + [JsonPropertyName("pendingRef")] + public string PendingRef { get; set; } + + public UserTestEnchantedLinkResponse(string loginId, string link, string pendingRef) + { + LoginId = loginId; + Link = link; + PendingRef = pendingRef; + } + } + + // Represents a tenant association for a User or an Access Key. The tenant ID is required + // to denote which tenant the user / access key belongs to. Roles is an optional list of + // roles for the user / access key in this specific tenant. + public class AssociatedTenant + { + [JsonPropertyName("tenantId")] + public string TenantId { get; set; } + [JsonPropertyName("tenantName")] + public string? TenantName { get; set; } + [JsonPropertyName("roleNames")] + public List? RoleNames { get; set; } + public AssociatedTenant(string tenantId) + { + TenantId = tenantId; + } + } + + public class TenantResponse + { + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("selfProvisioningDomains")] + public List? SelfProvisioningDomains { get; set; } + + [JsonPropertyName("customAttributes")] + public Dictionary? CustomAttributes { get; set; } + + public TenantResponse(string Id, string Name, List? SelfProvisioningDomains = null, Dictionary? CustomAttributes = null) + { + this.Id = Id; + this.Name = Name; + this.SelfProvisioningDomains = SelfProvisioningDomains; + this.CustomAttributes = CustomAttributes; + } + } + + public class TenantOptions + { + public string Name { get; set; } + public List? SelfProvisioningDomains { get; set; } + public Dictionary? CustomAttributes { get; set; } + public TenantOptions(string name) + { + Name = name; + } + } + + public class TenantSearchOptions + { + public List? Ids { get; set; } + public List? Names { get; set; } + public List? SelfProvisioningDomains { get; set; } + public Dictionary? CustomAttributes { get; set; } + public string? AuthType { get; set; } + } + + public class ProviderTokenResponse + { + [JsonPropertyName("provider")] + public string Provider { get; set; } + [JsonPropertyName("providerUserID")] + public string ProviderUserID { get; set; } + [JsonPropertyName("accessToken")] + public string AccessToken { get; set; } + [JsonPropertyName("expiration")] + public int Expiration { get; set; } + [JsonPropertyName("scopes")] + public List Scopes { get; set; } + public ProviderTokenResponse(string provider, string providerUserID, string accessToken, int expiration, List scopes) + { + Provider = provider; + ProviderUserID = providerUserID; + AccessToken = accessToken; + Expiration = expiration; + Scopes = scopes; + } + } + + public class AccessKeyCreateResponse + { + [JsonPropertyName("cleartext")] + public string Cleartext { get; set; } + [JsonPropertyName("key")] + public AccessKeyResponse Key { get; set; } + public AccessKeyCreateResponse(string cleartext, AccessKeyResponse key) + { + Cleartext = cleartext; + Key = key; + } + } + + public class AccessKeyResponse + { + [JsonPropertyName("id")] + public string Id { get; set; } + [JsonPropertyName("name")] + public string Name { get; set; } + [JsonPropertyName("roleNames")] + public List RoleNames { get; set; } + [JsonPropertyName("keyTenants")] + public List KeyTenants { get; set; } + [JsonPropertyName("status")] + public string Status { get; set; } + [JsonPropertyName("createdTime")] + public int CreatedTime { get; set; } + [JsonPropertyName("expireTime")] + public int ExpireTime { get; set; } + [JsonPropertyName("createdBy")] + public string CreatedBy { get; set; } + [JsonPropertyName("clientId")] + public string ClientId { get; set; } + [JsonPropertyName("boundUserId")] + public string UserId { get; set; } + public AccessKeyResponse(string id, string name, List roleNames, List keyTenants, string status, int createdTime, int expireTime, string createdBy, string clientId, string userId) + { + Id = id; + Name = name; + RoleNames = roleNames; + KeyTenants = keyTenants; + Status = status; + CreatedTime = createdTime; + ExpireTime = expireTime; + CreatedBy = createdBy; + ClientId = clientId; + UserId = userId; + } + } + + public class AccessKeyLoginOptions + { + [JsonPropertyName("customClaims")] + public Dictionary? CustomClaims { get; set; } + } + + public class ProjectCloneResponse + { + [JsonPropertyName("projectId")] + public string ProjectId { get; set; } + [JsonPropertyName("projectName")] + public string ProjectName { get; set; } + [JsonPropertyName("tag")] + public string Tag { get; set; } + public ProjectCloneResponse(string projectId, string projectName, string tag) + { + ProjectId = projectId; + ProjectName = projectName; + Tag = tag; + } + } +} diff --git a/Descope/packages.lock.json b/Descope/packages.lock.json new file mode 100644 index 0000000..65e1d0f --- /dev/null +++ b/Descope/packages.lock.json @@ -0,0 +1,83 @@ +{ + "version": 1, + "dependencies": { + "net6.0": { + "Newtonsoft.Json": { + "type": "Direct", + "requested": "[13.0.3, )", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "RestSharp": { + "type": "Direct", + "requested": "[110.2.0, )", + "resolved": "110.2.0", + "contentHash": "FXGw0IMcqY7yO/hzS9QrD3iNswNgb9UxJnxWmfOxmGs4kRlZWqdtBoGPLuhlbgsDzX1RFo4WKui8TGGKXWKalw==", + "dependencies": { + "System.Text.Json": "7.0.2" + } + }, + "System.IdentityModel.Tokens.Jwt": { + "type": "Direct", + "requested": "[7.3.1, )", + "resolved": "7.3.1", + "contentHash": "iE8biOWyAC1NnYcZGcgXErNACvIQ6Gcmg5s28gsjVbyyYdF9NdKsYzAPAsO3KGK86EQjpToI1AO82XbG8chkzA==", + "dependencies": { + "Microsoft.IdentityModel.JsonWebTokens": "7.3.1", + "Microsoft.IdentityModel.Tokens": "7.3.1" + } + }, + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "7.3.1", + "contentHash": "gIw8Sr5ZpuzKFBTfJonh2F54DivTzm5IIK15QB4Y6uE30uQdEO1NnCojTC/b6sWZoZzD0sdBa6SqwMXhucD+nA==" + }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Transitive", + "resolved": "7.3.1", + "contentHash": "mXA6AoaD5uZqtsKghgRiupBhyXNii8p9F2BjNLnDGud0tZLS5+4Fio2YAGjFXhnkc80CqgQ61X5U1gUNnDEoKQ==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "7.3.1" + } + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "7.3.1", + "contentHash": "uPt2aiRUCbcOc0Wk+dDCSClFfPNs3S3Z7fmy50MoxJ1mGmtVUDMpyRJeYzZ/16x4rL19T+g2zrzjcWoitp5+gQ==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "7.3.1" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Transitive", + "resolved": "7.3.1", + "contentHash": "/c/p8/3CAH706c0ii5uTgSb/8M/jwyuurtdMeKTBeKFU9aA+EZrLu1M8aaS3CSlGaxoxsoaxr4/+KXykgQ4VgQ==", + "dependencies": { + "Microsoft.IdentityModel.Logging": "7.3.1" + } + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "OP6umVGxc0Z0MvZQBVigj4/U31Pw72ITihDWP9WiWDm+q5aoe0GaJivsfYGq53o6dxH7DcXWiCTl7+0o2CGdmg==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Text.Json": { + "type": "Transitive", + "resolved": "7.0.2", + "contentHash": "/LZf/JrGyilojqwpaywb+sSz8Tew7ij4K/Sk+UW8AKfAK7KRhR6mKpKtTm06cYA7bCpGTWfYksIW+mVsdxPegQ==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encodings.Web": "7.0.0" + } + } + } + } +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7b3fc71 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Descope + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7feb336 --- /dev/null +++ b/README.md @@ -0,0 +1,489 @@ +# Descope SDK for .NET + +The Descope SDK for .NET provides convenient access to the Descope user management and authentication API for a backend written in .NET. You can read more on the [Descope Website](https://descope.com). + +## Requirements + +The SDK supports .NET 5 and above + +## Setup + +A Descope `Project ID` is required to initialize the SDK. Find it on the +[project page in the Descope Console](https://app.descope.com/settings/project). + +```cs +using Descope; + +// ... In your setup code + +var config = new DescopeConfig(projectId: "projectId"); +var descopeClient = new DescopeClient(config); +``` + +## Authentication Functions + +These sections show how to use the SDK to perform various authentication/authorization functions: + +1. [OTP Authentication](#otp-authentication) + +## Management Functions + +These sections show how to use the SDK to perform API management functions. Before using any of them, you will need to create a Management Key. The instructions for this can be found under [Setup](#setup-1). + +1. [Manage Tenants](#manage-tenants) +2. [Manage Users](#manage-users) +3. [Manage Access Keys](#manage-access-keys) +4. [Manage Project](#manage-project) + +--- + +### OTP Authentication + +Send a user a one-time password (OTP) using your preferred delivery method (_email / SMS_). An email address or phone number must be provided accordingly. + +The user can either `sign up`, `sign in` or `sign up or in` + +```cs +// Every user must have a loginID. All other user information is optional +var loginId = "desmond@descope.com"; +var signUpDetails = new SignUpDetails +{ + Name = "Desmond Copeland", + GivenName = "Desmond", + FamilyName = "Copeland", + Phone = "212-555-1234", + Email = loginId, +}; +try +{ + var maskedAddress = await descopeClient.Auth.Otp.SignUp(DeliveryMethod.Email, loginId, signUpDetails); +} +catch (DescopeException e) +{ + // handle errors +} +``` + +The user will receive a code using the selected delivery method. Verify that code using: + +```cs +try +{ + var authInfo = await descopeClient.Auth.Otp.VerifyCode(DeliveryMethod.Email, loginId, code); +} +catch +{ + // handle error +} +``` + +The session and refresh JWTs should be returned to the caller, and passed with every request in the session. Read more on [session validation](#session-validation) + +### Session Validation + +Every secure request performed between your client and server needs to be validated. The client sends +the session and refresh tokens with every request, and they are validated using one of the following: + +```cs +// Validate the session. Will return an error if expired +try +{ + var sessionToken = await descopeClient.Auth.ValidateSession(sessionJwt); +} +catch (DescopeException e) +{ + // unauthorized error +} + +// If ValidateSession throws an exception, you will need to refresh the session using +try +{ + var sessionToken = await descopeClient.Auth.RefreshSession(refreshJwt); +} +catch (DescopeException e) +{ + // unauthorized error +} + +// Alternatively, you could combine the two and +// have the session validated and automatically refreshed when expired +try +{ + var sessionToken := descopeClient.Auth.ValidateAndRefreshSession(sessionJwt, refreshJwt); +} +catch (DescopeException e) +{ + // unauthorized error +} +``` + +Choose the right session validation and refresh combination that suits your needs. + +Refreshed sessions return the same response as is returned when users first sign up / log in, +Make sure to return the session token from the response to the client if tokens are validated directly. + +Usually, the tokens can be passed in and out via HTTP headers or via a cookie. +The implementation can defer according to your implementation. + +If Roles & Permissions are used, validate them immediately after validating the session. See the [next section](#roles--permission-validation) +for more information. + +### Roles & Permission Validation + +When using Roles & Permission, it's important to validate the user has the required +authorization immediately after making sure the session is valid. Taking the `sessionToken` +received by the [session validation](#session-validation), call the following functions, while for multi-tenant use cases +make sure to pass in the tenant ID, otherwise leave as `null`: + +```cs +// You can validate specific permissions +if (!descopeClient.Auth.ValidatePermissions(sessionToken, new List { "Permission to validate" }, "optional-tenant-ID")) +{ + // Deny access +} + +// Or validate roles directly +if (!descopeClient.Auth.ValidateRoles(sessionToken, new List { "Role to validate" }, "optional-tenant-ID")) +{ + // Deny access +} + +var matchedRoles = descopeClient.Auth.GetMatchedRoles(sessionToken, new List { "role-name1", "role-name2" }, "optional-tenant-ID"); +var matchedPermissions = descopeClient.Auth.GetMatchedPermissions(sessionToken, new List { "permission-name1", "permission-name2" }, "optional-tenant-ID"); +``` + +### Tenant selection + +For a user that has permissions to multiple tenants, you can set a specific tenant as the current selected one +This will add an extra attribute to the refresh JWT and the session JWT with the selected tenant ID + +```cs +var tenantId = "t1"; +try +{ + var session = await descopeClient.Auth.SelectTenant(tenantId, refreshJwt); +} +catch (DescopeException e) +{ + // failed to select a tenant +} +``` + +### Logging Out + +You can log out a user from an active session by providing their `refreshJwt` for that session. +After calling this function, you must invalidate or remove any cookies you have created. + +```cs +await descopeClient.Auth.Logout(refreshJwt); +``` + +It is also possible to sign the user out of all the devices they are currently signed-in with. Calling `LogoutAll` will +invalidate all user's refresh tokens. After calling this function, you must invalidate or remove any cookies you have created. + +```cs +await descopeClient.Auth.LogoutAll(refreshJwt); +``` + +## Management Functions + +It is very common for some form of management or automation to be required. These can be performed +using the management functions. Please note that these actions are more sensitive as they are administrative +in nature. Please use responsibly. + +### Setup + +To use the management API you'll need a `Management Key` along with your `Project ID`. +Create one in the [Descope Console](https://app.descope.com/settings/company/managementkeys). + +```cs +using Descope; + +// ... In your setup code +var config = new DescopeConfig(projectId: "projectId"); +var descopeClient = new DescopeClient(config) +{ + ManagementKey = "management-key", +}; +``` + +### Manage Tenants + +You can create, update, delete or load tenants: + +```cs +// The self provisioning domains or optional. If given they'll be used to associate +// Users logging in to this tenant + +// Creating and updating tenants takes a TenantOptions. For example: +var options = new TenantOptions("name") +{ + SelfProvisioningDomains = new List { "domain" }, + CustomAttributes = new Dictionary { { "mycustomattribute", "test" } }, +}; +try +{ + // Create tenant + var tenantId = await descopeClient.Management.Tenant.Create(options: options); + + // You can optionally set your own ID when creating a tenant + var tenantId = await descopeClient.Management.Tenant.Create(options: options, id: "my-tenant-id"); + + // Update will override all fields as is. Use carefully. + await descopeClient.Management.Tenant.Update(tenantId, updatedTenantOptions); + + // Tenant deletion cannot be undone. Use carefully. + await descopeClient.Management.Tenant.Delete(tenantId); + + // Load tenant by id + var tenant = await descopeClient.Management.Tenant.Load("my-custom-id"); + + // Load all tenants + var tenants = await descopeClient.Management.Tenant.LoadAll(); + foreach (var tenant in tenants) + { + // do something + } + + // Search tenants - takes the &descope.TenantSearchOptions type. This is an example of a &descope.TenantSearchOptions + var searchOptions = new TenantSearchOptions + { + Ids = new List {"my-custom-id"}, + Names = new List { "My Tenant" }, + SelfProvisioningDomains = new List {"domain.com", "company.com"}, + CustomAttributes = new Dictionary {{ "mycustomattribute": "Test" }}, + }; + var tenants = await descopeClient.Management.Tenant.SearchAll(searchOptions); + foreach (var tenant in tenants) + { + // do something + } +} +catch (DescopeException e) +{ + // handle errors +} +``` + +### Manage Users + +You can create, update, delete, logout, get user history and load users, as well as search according to filters: + +```cs +try +{ + // A user must have a loginId, other fields are optional. + // Roles should be set directly if no tenants exist, otherwise set + // on a per-tenant basis. + await descopeClient.Management.User.Create(loginId: "desmond@descope.com", new UserRequest() + { + Email = "desmond@descope.com", + Name = "Desmond Copeland", + GivenName = "Desmond", + FamilyName = "Copeland", + UserTenants = new List + { + new(tenantId:"tenant-ID1") { RoleNames = new List { "role-name1" }}, + new(tenantId:"tenant-ID2"), + }, + SsoAppIds = new List { "appId1", "appId2" }, + }); + + // Alternatively, a user can be created and invited via an email or text message. + // Make sure to configure the invite URL in the Descope console prior to using this function, + // or provide the necessary invite options for sending invitation, and that an email address + // or phone number is provided in the information. + var inviteOptions = new InviteOptions() + { + // options can be null, and in this case, value will be taken from project settings page + // otherwise provide them here + }; + await descopeClient.Management.User.Create(loginId: "desmond@descope.com", new UserRequest() + { + Email = "desmond@descope.com", + SsoAppIds = new List { "appId1", "appId2" }, + }, sendInvite: true, inviteOptions: new InviteOptions()); + + // User creation and invitation can also be performed in a similar fashion but as a batch operation + var batchUsers = new List() + { + new(loginId: "user1@something.com") + { + Email = "user1@something.com", + VerifiedEmail = true, + }, + new(loginId: "user2@something.com") + { + Email = "user2@something.com", + VerifiedEmail = false, + } + }; + var result = await descopeClient.Management.User.CreateBatch(batchUsers); + + // Import users from another service by calling CreateBatch with each user's password hash + var user = new BatchUser("desmond@descope.com") + { + Password = new BatchUserPassword + { + Hashed = new BatchUserPasswordHashed + { + Bcrypt = new BatchUserPasswordBcrypt(hash: "$2a$...") + }, + }, + }; + var users = await descopeClient.Management.User.CreateBatch(new List { user }); + + // Update will override all fields as is. Use carefully. + await descopeClient.Management.User.Update(loginId: "desmond@descope.com", new UserRequest() + { + Email = "desmond@descope.com", + Name = "Desmond Copeland", + GivenName = "Desmond", + FamilyName = "Copeland", + UserTenants = new List + { + new(tenantId:"tenant-ID2"), + }, + SsoAppIds = new List { "appId3" }, + }); + + // Update loginId of a user, or remove a login ID (last login ID cannot be removed) + await descopeClient.Management.User.UpdateLoginIs("desmond@descope.com", "bane@descope.com"); + + // Associate SSO application for a user. + var user = await descopeClient.Management.User.AddSsoApps("desmond@descope.com", new List { "appId1" }); + + // Set (associate) SSO application for a user. + var user = await descopeClient.Management.User.SetSsoApps("desmond@descope.com", new List { "appId1" }); + + // Remove SSO application association from a user. + var user = await descopeClient.Management.User.RemoveSsoApps("desmond@descope.com", new List { "appId1" }); + + // User deletion cannot be undone. Use carefully. + await descopeClient.Management.User.Delete("desmond@descope.com"); + + // Load specific user + var userRes = descopeClient.Management.User.Load("desmond@descope.com"); + + // Search all users, optionally according to tenant and/or role filter + // Results can be paginated using the limit and page parameters + var usersResp = descopeClient.Management.User.SearchAll(new SearchUserOptions + { + TenantIds = new List { "my-tenant-id" }, + }); + + // Logout given user from all its devices, by loginId or by userId + await descopeClient.Management.User.Logout(loginId: "", userId: ""); +} +catch (DescopeException e) +{ + // handle any errors +} +``` + +#### Set or Expire User Password + +You can set a new active password for a user, which they can then use to sign in. You can also set a temporary +password that the user will be forced to change on the next login. + +```cs +// Set a temporary password for the user which they'll need to replace it on next login +await descopeClient.Management.User.SetTemporaryPassword("", ""); + +// Set an active password for the user which they can use to login +await descopeClient.Management.User.SetActivePassword("", "") +``` + +For a user that already has a password, you can expire it to require them to change it on the next login. + +```cs +// Expire the user's active password +await descopeClient.Management.User.ExpirePassword(""); + +// Later, if the user is signing in with an expired password, the returned error will be ErrPasswordExpired +``` + +### Manage Access Keys + +You can create, update, delete or load access keys, as well as search according to filters: + +```cs +try +{ + // An access key must have a name and expireTime, other fields are optional. + // Roles should be set directly if no tenants exist, otherwise set + // on a per-tenant basis. + // If userID is supplied, then authorization would be ignored, and access key would be bound to the users authorization + var accessKey = await descopeClient.Management.AccessKey.Create(name: "access-key-1", expireTime: 0, keyTenants: new List { + new("tenant-ID1"){RoleNames= new List{"role-name1"}}, + new("tenant-ID2"), + }); + + // Load specific access key + var accessKey = await descopeClient.Management.AccessKey.Load("access-key-id"); + + // Search all access keys, optionally according to tenant and/or role filter + var accessKeys = await descopeClient.Management.AccessKey.SearchAll(new List{ "my-tenant-id" }); + + // Update will override all fields as is. Use carefully. + var accessKey = await descopeClient.Management.AccessKey.Update("access-key-id", "updated-name"); + + // Access keys can be deactivated to prevent usage. This can be undone using "activate". + await descopeClient.Management.AccessKey.Deactivate("access-key-id"); + + // Disabled access keys can be activated once again. + await descopeClient.Management.AccessKey.Activate("access-key-id"); + + // Access key deletion cannot be undone. Use carefully. + await descopeClient.Management.AccessKey.Delete("access-key-id"); +} +catch (DescopeException e) +{ + // handle errors +} +``` + +Exchange the access key and provide optional access key login options: + +```cs +var loginOptions = new AccessKeyLoginOptions +{ + CustomClaims = new Dictionary { {"k1": "v1"} }, +} +var token = await descopeClient.Auth.ExchangeAccessKey("accessKey", loginOptions); +``` + +### Manage Project + +You can update project name, as well as to clone the current project to a new one: + +```cs +try +{ + // Update project name + await descopeClient.Management.Project.Rename("new-project-name"); + + // Clone the current project to a new one + // Note that this action is supported only with a pro license or above. + var res = await descopeClient.Management.Project.Clone("new-project-name", ""); + + // Delete the current project. Kindly note that following calls on the `descopeClient` are most likely to fail because the current project has been deleted + await descopeClient.Management.Project.Delete("projectId"); +} +catch (DescopeException e) +{ + // handle errors +} +``` + +## Learn More + +To learn more please see the [Descope Documentation and API reference page](https://docs.descope.com/). + +## Contact Us + +If you need help you can email [Descope Support](mailto:support@descope.com) + +## License + +The Descope SDK for Go is licensed for use under the terms and conditions of the [MIT license Agreement](https://github.com/descope/descope-dotnet/blob/main/LICENSE).