From 0da2b352869c79bedde0b516942f0d9cfb8ff699 Mon Sep 17 00:00:00 2001 From: itaihanski Date: Thu, 18 Apr 2024 13:21:41 +0300 Subject: [PATCH] Add SSO application manangement --- .../Management/SsoApplicationTests.cs | 146 ++++++++++ Descope/Internal/Http/Routes.cs | 12 + Descope/Internal/Management/Managment.cs | 3 + Descope/Internal/Management/SsoApplication.cs | 80 ++++++ Descope/Sdk/Managment.cs | 65 +++++ Descope/Types/Sso.cs | 272 ++++++++++++++++++ Descope/Types/Types.cs | 1 - 7 files changed, 578 insertions(+), 1 deletion(-) create mode 100644 Descope.Test/IntegrationTests/Management/SsoApplicationTests.cs create mode 100644 Descope/Internal/Management/SsoApplication.cs create mode 100644 Descope/Types/Sso.cs diff --git a/Descope.Test/IntegrationTests/Management/SsoApplicationTests.cs b/Descope.Test/IntegrationTests/Management/SsoApplicationTests.cs new file mode 100644 index 0000000..764243e --- /dev/null +++ b/Descope.Test/IntegrationTests/Management/SsoApplicationTests.cs @@ -0,0 +1,146 @@ +using Xunit; + +namespace Descope.Test.Integration +{ + public class SsoApplicationTests + { + private readonly DescopeClient _descopeClient = IntegrationTestSetup.InitDescopeClient(); + + [Fact] + public async Task SsoApplication_SamlCreateUpdateAndDelete() + { + string? id = null; + try + { + var name = "name"; + var url = "https://sometestidp.com"; + // Create + var options = new SamlApplicationOptions(name, url); + id = await _descopeClient.Management.SsoApplication.CreateSAMLApplication(options); + + // Load + var loadedApp = await _descopeClient.Management.SsoApplication.Load(id); + Assert.Equal(name, loadedApp.Name); + Assert.Equal(url, loadedApp.SamlSettings!.LoginPageUrl); + + // Update + options.Id = id; + name = "updated name"; + url = "https://someothertestidp.com"; + await _descopeClient.Management.SsoApplication.UpdateSAMLApplication(options); + + // Load All + var apps = await _descopeClient.Management.SsoApplication.LoadAll(); + loadedApp = apps.Find(a => a.Id == id); + Assert.Equal(name, loadedApp!.Name); + Assert.Equal(url, loadedApp.SamlSettings!.LoginPageUrl); + + // Delete + await _descopeClient.Management.SsoApplication.Delete(id); + id = null; + } + finally + { + if (!string.IsNullOrEmpty(id)) + { + try { await _descopeClient.Management.SsoApplication.Delete(id); } + catch { } + } + } + } + + [Fact] + public async Task Sso_SamlByMetadata() + { + string? tenantId = null; + string? roleName = null; + try + { + // Create a tenant + tenantId = await _descopeClient.Management.Tenant.Create(new TenantOptions(Guid.NewGuid().ToString())); + roleName = Guid.NewGuid().ToString()[..20]; + await _descopeClient.Management.Role.Create(roleName, tenantId: tenantId); + + // update sso settings + var settings = new SsoSamlSettingsByMetadata("https://sometestidpmd.com") + { + RoleMappings = new List { new RoleMapping(new List { "group1", "group2" }, roleName) } + }; + await _descopeClient.Management.Sso.ConfigureSamlSettingsByMetadata(tenantId, settings, "https://myredirect.com", new List { "domain1.com" }); + + var loadedSetting = await _descopeClient.Management.Sso.LoadSettings(tenantId); + + // Make sure the settings match + Assert.Equal(settings.IdpMetadataUrl, loadedSetting.Saml.IdpMetadataUrl); + Assert.NotEmpty(loadedSetting.Saml.GroupsMapping?.First()?.Role?.Id ?? ""); + Assert.Equal("group1", loadedSetting.Saml.GroupsMapping?.First()?.Groups?[0]); + Assert.Equal("group2", loadedSetting.Saml.GroupsMapping?.First()?.Groups?[1]); + Assert.Equal("https://myredirect.com", loadedSetting.Saml?.RedirectUrl); + Assert.Equal("domain1.com", loadedSetting.Tenant.Domains.First()); + } + finally + { + if (!string.IsNullOrEmpty(tenantId)) + { + try { await _descopeClient.Management.Tenant.Delete(tenantId); } + catch { } + } + if (!string.IsNullOrEmpty(roleName)) + { + try { await _descopeClient.Management.Role.Delete(roleName); } + catch { } + } + } + } + + [Fact] + public async Task Sso_Oidc() + { + string? tenantId = null; + string? roleName = null; + try + { + // Create a tenant + tenantId = await _descopeClient.Management.Tenant.Create(new TenantOptions(Guid.NewGuid().ToString())); + roleName = Guid.NewGuid().ToString()[..20]; + await _descopeClient.Management.Role.Create(roleName, tenantId: tenantId); + + // Update sso settings + var settings = new SsoOidcSettings + { + Name = "Name", + ClientId = "ClientId", + ClientSecret = "ClientSecret", + AuthUrl = "https://mytestauth.com", + TokenUrl = "https://mytestauth.com", + JwksUrl = "https://mytestauth.com", + AttributeMapping = new OidcAttributeMapping { } + }; + await _descopeClient.Management.Sso.ConfigureOidcSettings(tenantId, settings, new List { "domain1.com" }); + + var loadedSetting = await _descopeClient.Management.Sso.LoadSettings(tenantId); + + // Make sure the settings match + Assert.Equal(settings.Name, loadedSetting.Oidc.Name); + Assert.Equal(settings.ClientId, loadedSetting.Oidc.ClientId); + Assert.Equal(settings.AuthUrl, loadedSetting.Oidc.AuthUrl); + Assert.Equal(settings.TokenUrl, loadedSetting.Oidc.TokenUrl); + Assert.Equal(settings.JwksUrl, loadedSetting.Oidc.JwksUrl); + Assert.Equal("domain1.com", loadedSetting.Tenant.Domains.First()); + } + finally + { + if (!string.IsNullOrEmpty(tenantId)) + { + try { await _descopeClient.Management.Tenant.Delete(tenantId); } + catch { } + } + if (!string.IsNullOrEmpty(roleName)) + { + try { await _descopeClient.Management.Role.Delete(roleName); } + catch { } + } + } + } + } +} diff --git a/Descope/Internal/Http/Routes.cs b/Descope/Internal/Http/Routes.cs index cfdcc14..1ec546c 100644 --- a/Descope/Internal/Http/Routes.cs +++ b/Descope/Internal/Http/Routes.cs @@ -120,6 +120,18 @@ public static class Routes #endregion SSO + #region SSO Application + + public const string SsoApplicationOidcCreate = "/v1/mgmt/sso/idp/app/oidc/create"; + public const string SsoApplicationSamlCreate = "/v1/mgmt/sso/idp/app/saml/create"; + public const string SsoApplicationOidcUpdate = "/v1/mgmt/sso/idp/app/oidc/update"; + public const string SsoApplicationSamlUpdate = "/v1/mgmt/sso/idp/app/saml/update"; + public const string SsoApplicationDelete = "/v1/mgmt/sso/idp/app/delete"; + public const string SsoApplicationLoad = "/v1/mgmt/sso/idp/app/load"; + public const string SsoApplicationLoadAll = "/v1/mgmt/sso/idp/apps/load"; + + #endregion SSO Application + #region Permission public const string PermissionCreate = "/v1/mgmt/permission/create"; diff --git a/Descope/Internal/Management/Managment.cs b/Descope/Internal/Management/Managment.cs index 86e530d..89df6ed 100644 --- a/Descope/Internal/Management/Managment.cs +++ b/Descope/Internal/Management/Managment.cs @@ -6,6 +6,7 @@ internal class Management : IManagement public IUser User => _user; public IAccessKey AccessKey => _accessKey; public ISso Sso => _sso; + public ISsoApplication SsoApplication => _ssoApplication; public IPasswordSettings Password => _password; public IJwt Jwt => _jwt; public IPermission Permission => _permission; @@ -16,6 +17,7 @@ internal class Management : IManagement private readonly User _user; private readonly AccessKey _accessKey; private readonly Sso _sso; + private readonly SsoApplication _ssoApplication; private readonly Password _password; private readonly Jwt _jwt; private readonly Permission _permission; @@ -28,6 +30,7 @@ public Management(IHttpClient client, string managementKey) _user = new User(client, managementKey); _accessKey = new AccessKey(client, managementKey); _sso = new Sso(client, managementKey); + _ssoApplication = new SsoApplication(client, managementKey); _password = new Password(client, managementKey); _jwt = new Jwt(client, managementKey); _permission = new Permission(client, managementKey); diff --git a/Descope/Internal/Management/SsoApplication.cs b/Descope/Internal/Management/SsoApplication.cs new file mode 100644 index 0000000..4ff90a5 --- /dev/null +++ b/Descope/Internal/Management/SsoApplication.cs @@ -0,0 +1,80 @@ + +using System.Text.Json.Serialization; + +namespace Descope.Internal.Management +{ + internal class SsoApplication : ISsoApplication + { + private readonly IHttpClient _httpClient; + private readonly string _managementKey; + + internal SsoApplication(IHttpClient httpClient, string managementKey) + { + _httpClient = httpClient; + _managementKey = managementKey; + } + + public async Task CreateOidcApplication(OidcApplicationOptions options) + { + var resp = await _httpClient.Post(Routes.SsoApplicationOidcCreate, _managementKey, body: options); + return resp.Id; + } + + public async Task CreateSAMLApplication(SamlApplicationOptions options) + { + var resp = await _httpClient.Post(Routes.SsoApplicationSamlCreate, _managementKey, body: options); + return resp.Id; + } + + public async Task UpdateOIDCApplication(OidcApplicationOptions options) + { + await _httpClient.Post(Routes.SsoApplicationOidcUpdate, _managementKey, body: options); + } + + public async Task UpdateSAMLApplication(SamlApplicationOptions options) + { + await _httpClient.Post(Routes.SsoApplicationSamlUpdate, _managementKey, body: options); + } + + public async Task Delete(string id) + { + var body = new { id }; + await _httpClient.Post(Routes.SsoApplicationDelete, _managementKey, body: body); + } + + public async Task Load(string id) + { + return await _httpClient.Get(Routes.SsoApplicationLoad, _managementKey, queryParams: new Dictionary { { "id", id } }); + } + + public async Task> LoadAll() + { + var resp = await _httpClient.Get(Routes.SsoApplicationLoadAll, _managementKey); + return resp.Apps; + } + + } + + internal class SsoApplicationCreateResponse + { + [JsonPropertyName("id")] + public string Id { get; set; } + + public SsoApplicationCreateResponse(string id) + { + Id = id; + } + } + + internal class SsoLoadAllResponse + { + [JsonPropertyName("apps")] + public List Apps { get; set; } + + public SsoLoadAllResponse(List apps) + { + Apps = apps; + } + } + +} diff --git a/Descope/Sdk/Managment.cs b/Descope/Sdk/Managment.cs index 29d243f..9cd0c24 100644 --- a/Descope/Sdk/Managment.cs +++ b/Descope/Sdk/Managment.cs @@ -572,6 +572,66 @@ public interface ISso Task DeleteSettings(string tenantId); } + /// + /// Provides functions for managing SSO applications in a project. + /// + public interface ISsoApplication + { + /// + /// Create a new OIDC SSO application according to the given options. + /// + /// Options to define an OIDC application + /// The created application ID + public Task CreateOidcApplication(OidcApplicationOptions options); + + /// + /// Create a new SAML SSO application according to the given options. + /// + /// + /// The created application ID + public Task CreateSAMLApplication(SamlApplicationOptions options); + + /// + /// Update an existing OIDC SSO application. + /// + /// IMPORTANT: All options are taken as-is and will override whatever value is currently set. Use carefully. + /// + /// + /// The options to set for an existing SSO application + public Task UpdateOIDCApplication(OidcApplicationOptions options); + + /// + /// Update an existing SAML SSO application. + /// + /// IMPORTANT: All options are taken as-is and will override whatever value is currently set. Use carefully. + /// + /// + /// The options to set for an existing SSO application + public Task UpdateSAMLApplication(SamlApplicationOptions options); + + /// + /// Delete an existing SSO application. + /// + /// IMPORTANT: This action is irreversible. Use carefully. + /// + /// + /// The ID of application to delete + public Task Delete(string id); + + /// + /// Load an SSO application + /// + /// The ID of the application to load + /// The loaded SSO application + public Task Load(string id); + + /// + /// Load all project SSO applications. + /// + /// A list of all SSO applications. + public Task> LoadAll(); + } + /// /// Provides functions for managing permissions in a project. /// @@ -788,6 +848,11 @@ public interface IManagement /// public ISso Sso { get; } + /// + /// Provides functions for configuring SSO Applications in a project. + /// + public ISsoApplication SsoApplication { get; } + /// /// Provides functions for managing password policy for a project or a tenant. /// diff --git a/Descope/Types/Sso.cs b/Descope/Types/Sso.cs new file mode 100644 index 0000000..24441bc --- /dev/null +++ b/Descope/Types/Sso.cs @@ -0,0 +1,272 @@ +using System.Text.Json.Serialization; + +namespace Descope +{ + + /// + /// Options to create or update an OIDC application. + /// + public class OidcApplicationOptions + { + /// + /// Optional SSO application ID. If not provided, will be auto-generated. + /// + [JsonPropertyName("id")] + public string? Id { get; set; } + /// + /// The SSO application's name. Must be unique per project. + /// + [JsonPropertyName("name")] + public string Name { get; set; } + /// + /// Optional SSO application description. + /// + [JsonPropertyName("description")] + public string? Description { get; set; } + /// + /// Optionally set the SSO application as enabled or disabled. + /// + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + /// + /// Optional SSO application logo. + /// + [JsonPropertyName("logo")] + public string? Logo { get; set; } + /// + /// The URL where login page is hosted. + /// + [JsonPropertyName("loginPageUrl")] + public string LoginPageUrl { get; set; } + public OidcApplicationOptions(string name, string loginPageUrl) + { + Name = name; + LoginPageUrl = loginPageUrl; + } + } + + /// + /// Options to create or update a SAML application. + /// + public class SamlApplicationOptions + { + /// + /// Optional SSO application ID. If not provided, will be auto-generated. + /// + [JsonPropertyName("id")] + public string? Id { get; set; } + /// + /// The SSO application's name. Must be unique per project. + /// + [JsonPropertyName("name")] + public string Name { get; set; } + /// + /// Optional SSO application description. + /// + [JsonPropertyName("description")] + public string? Description { get; set; } + /// + /// Optionally set the SSO application as enabled or disabled. + /// + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + /// + /// Optional SSO application logo. + /// + [JsonPropertyName("logo")] + public string? Logo { get; set; } + /// + /// The URL where login page is hosted. + /// + [JsonPropertyName("loginPageUrl")] + public string LoginPageUrl { get; set; } + /// + /// Optionally determine whether SP info should be automatically fetched from MetadataURL + /// or by specifying it explicitly via the EntityId, AcsUrl and Certificate properties. + /// + [JsonPropertyName("useMetadataInfo")] + public bool UseMetadataInfo { get; set; } + /// + /// Optional SP metadata URL to fetch the SP SAML info from. Required if UseMetadataInfo is true. + /// + [JsonPropertyName("metadataUrl")] + public string? MetadataURL { get; set; } + /// + /// Optional SP entity ID. Required if UseMetadataInfo is false. + /// + [JsonPropertyName("entityId")] + public string? EntityId { get; set; } + /// + /// Optional SP ACS URL (SAML callback). Required if UseMetadataInfo is false. + /// + [JsonPropertyName("acsUrl")] + public string? AcsUrl { get; set; } + /// + /// Optional SP certificate. Required only when SAML request must be signed and UseMetadataInfo is false. + /// + [JsonPropertyName("certificate")] + public string? Certificate { get; set; } + /// + /// Optional mappings of Descope (IdP) attributes to SP attributes. + /// + [JsonPropertyName("attributeMapping")] + public List? AttributeMapping { get; set; } + /// + /// Optional mappings of Descope (IdP) roles to SP groups. + /// + [JsonPropertyName("groupsMapping")] + public List? GroupsMapping { get; set; } + /// + /// Optional list of URL wildcards. If provided, only URLs from this list will be accepted when receiving SAML callback requests. + /// + [JsonPropertyName("acsAllowedCallbacks")] + public List? AcsAllowedCallbacks { get; set; } + /// + /// Optionally define the SAML Assertion for the subject name type. Leave empty to use the Descope user ID or set to "email"/"phone". + /// + [JsonPropertyName("subjectNameIdType")] + public string? SubjectNameIDType { get; set; } + /// + /// Optionally define the SAML Assertion for subject name format. Defaults to "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified". + /// + [JsonPropertyName("subjectNameIdFormat")] + public string? SubjectNameIDFormat { get; set; } + public SamlApplicationOptions(string name, string loginPageURL) + { + Name = name; + LoginPageUrl = loginPageURL; + } + } + + + public class SamlIdpAttributeMappingInfo + { + [JsonPropertyName("name")] + public string Name { get; set; } + [JsonPropertyName("type")] + public string Type { get; set; } + [JsonPropertyName("value")] + public string Value { get; set; } + public SamlIdpAttributeMappingInfo(string name, string type, string value) + { + Name = name; + Type = type; + Value = value; + } + } + + public class SamlIdpGroupsMappingInfo + { + [JsonPropertyName("name")] + public string Name { get; set; } + [JsonPropertyName("type")] + public string Type { get; set; } + [JsonPropertyName("filterType")] + public string FilterType { get; set; } + [JsonPropertyName("value")] + public string Value { get; set; } + [JsonPropertyName("roles")] + public List Roles { get; set; } + public SamlIdpGroupsMappingInfo(string name, string type, string filterType, string value, List roles) + { + Name = name; + Type = type; + FilterType = filterType; + Value = value; + Roles = roles; + } + } + + public class SamlIdpRoleGroupMappingInfo + { + [JsonPropertyName("id")] + public string Id { get; set; } + [JsonPropertyName("name")] + public string Name { get; set; } + public SamlIdpRoleGroupMappingInfo(string id, string name) + { + Id = id; + Name = name; + } + } + + public class SsoApplicationResponse + { + [JsonPropertyName("id")] + public string Id { get; set; } + [JsonPropertyName("name")] + public string Name { get; set; } + [JsonPropertyName("description")] + public string? Description { get; set; } + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + [JsonPropertyName("logo")] + public string? Logo { get; set; } + [JsonPropertyName("appType")] + public string AppType { get; set; } + [JsonPropertyName("samlSettings")] + public SsoApplicationSamlSettings? SamlSettings { get; set; } + [JsonPropertyName("oidcSettings")] + public SsoApplicationOidcSettings? OidcSettings { get; set; } + public SsoApplicationResponse(string id, string name, string appType) + { + Id = id; + Name = name; + AppType = appType; + } + } + + public class SsoApplicationSamlSettings + { + [JsonPropertyName("loginPageUrl")] + public string LoginPageUrl { get; set; } + [JsonPropertyName("idpCert")] + public string? IdpCert { get; set; } + [JsonPropertyName("useMetadataInfo")] + public bool UseMetadataInfo { get; set; } + [JsonPropertyName("metadataUrl")] + public string? MetadataUrl { get; set; } + [JsonPropertyName("entityId")] + public string? EntityId { get; set; } + [JsonPropertyName("acsUrl")] + public string? AcsUrl { get; set; } + [JsonPropertyName("certificate")] + public string? Certificate { get; set; } + [JsonPropertyName("attributeMapping")] + public List? AttributeMapping { get; set; } + [JsonPropertyName("groupsMapping")] + public List? GroupsMapping { get; set; } + [JsonPropertyName("idpMetadataUrl")] + public string? IdpMetadataUrl { get; set; } + [JsonPropertyName("idpEntityId")] + public string? IdpEntityId { get; set; } + [JsonPropertyName("idpSsoUrl")] + public string? IdpSsoUrl { get; set; } + [JsonPropertyName("acsAllowedCallbacks")] + public List? AcsAllowedCallbacks { get; set; } + [JsonPropertyName("subjectNameIdType")] + public string? SubjectNameIdType { get; set; } + [JsonPropertyName("subjectNameIdFormat")] + public string? SubjectNameIdFormat { get; set; } + public SsoApplicationSamlSettings(string loginPageUrl) + { + LoginPageUrl = loginPageUrl; + } + } + + public class SsoApplicationOidcSettings + { + [JsonPropertyName("loginPageUrl")] + public string LoginPageUrl { get; set; } + [JsonPropertyName("issuer")] + public string Issuer { get; set; } + [JsonPropertyName("discoveryUrl")] + public string DiscoveryUrl { get; set; } + public SsoApplicationOidcSettings(string loginPageUrl, string issuer, string discoveryUrl) + { + LoginPageUrl = loginPageUrl; + Issuer = issuer; + DiscoveryUrl = discoveryUrl; + } + } +} diff --git a/Descope/Types/Types.cs b/Descope/Types/Types.cs index 86c5e76..cbf195b 100644 --- a/Descope/Types/Types.cs +++ b/Descope/Types/Types.cs @@ -1,6 +1,5 @@ using System.Text.Json.Serialization; -using Descope.Internal.Management; using Microsoft.IdentityModel.JsonWebTokens; namespace Descope