From b589735dff8b3a0bd688cdf8a9bed0ded1669ff2 Mon Sep 17 00:00:00 2001 From: Slavik Markovich Date: Wed, 14 Feb 2024 15:16:40 -0800 Subject: [PATCH 1/3] new SSO logic and SSO apps --- examples/management-cli/pom.xml | 2 +- pom.xml | 2 +- .../exception/ServerCommonException.java | 11 ++ .../java/com/descope/literals/Routes.java | 22 +++- .../model/auth/AuthenticationServices.java | 2 + .../descope/model/magiclink/LoginOptions.java | 1 + .../model/magiclink/SignUpOptions.java | 16 +++ .../magiclink/request/SignUpRequest.java | 2 + .../com/descope/model/mgmt/IDResponse.java | 14 +++ .../model/mgmt/ManagementServices.java | 2 + .../com/descope/model/otp/SignUpRequest.java | 2 + .../descope/model/sso/AttributeMapping.java | 9 ++ .../model/sso/OIDCAttributeMapping.java | 27 +++++ .../com/descope/model/sso/RoleMapping.java | 3 +- .../descope/model/sso/SSOOIDCSettings.java | 31 +++++ .../descope/model/sso/SSOSAMLSettings.java | 19 +++ .../model/sso/SSOSAMLSettingsByMetadata.java | 17 +++ .../model/sso/SSOSAMLSettingsResponse.java | 24 ++++ .../model/sso/SSOTenantSettingsResponse.java | 17 +++ .../model/ssoapp/OIDCApplicationRequest.java | 37 ++++++ .../model/ssoapp/SAMLApplicationRequest.java | 82 +++++++++++++ .../ssoapp/SAMLIDPAttributeMappingInfo.java | 16 +++ .../ssoapp/SAMLIDPGroupsMappingInfo.java | 19 +++ .../ssoapp/SAMLIDPRoleGroupMappingInfo.java | 15 +++ .../descope/model/ssoapp/SSOApplication.java | 21 ++++ .../ssoapp/SSOApplicationOIDCSettings.java | 16 +++ .../ssoapp/SSOApplicationSAMLSettings.java | 29 +++++ .../descope/model/ssoapp/SSOApplications.java | 15 +++ .../tenant/request/TenantSearchRequest.java | 1 + .../model/user/request/UserRequest.java | 2 + .../model/user/response/UserResponse.java | 1 + .../descope/proxy/impl/AbstractProxyImpl.java | 20 ++-- .../sdk/auth/EnchantedLinkService.java | 14 +++ .../descope/sdk/auth/MagicLinkService.java | 16 +++ .../java/com/descope/sdk/auth/OTPService.java | 29 +++++ .../descope/sdk/auth/SSOServiceProvider.java | 44 +++++++ .../impl/AuthenticationServiceBuilder.java | 1 + .../auth/impl/EnchantedLinkServiceImpl.java | 10 ++ .../sdk/auth/impl/MagicLinkServiceImpl.java | 14 ++- .../descope/sdk/auth/impl/OTPServiceImpl.java | 30 +++-- .../sdk/auth/impl/SSOServiceProviderImpl.java | 59 ++++++++++ .../sdk/mgmt/SsoApplicationService.java | 72 ++++++++++++ .../java/com/descope/sdk/mgmt/SsoService.java | 60 +++++++++- .../com/descope/sdk/mgmt/UserService.java | 33 ++++++ .../mgmt/impl/ManagementServiceBuilder.java | 1 + .../mgmt/impl/SsoApplicationServiceImpl.java | 110 ++++++++++++++++++ .../descope/sdk/mgmt/impl/SsoServiceImpl.java | 90 ++++++++++++-- .../sdk/mgmt/impl/UserServiceImpl.java | 32 +++++ src/test/java/com/descope/sdk/TestUtils.java | 3 +- .../impl/SsoApplicationServiceImplTest.java | 94 +++++++++++++++ .../sdk/mgmt/impl/SsoServiceImplTest.java | 84 ++++++++++++- 51 files changed, 1259 insertions(+), 34 deletions(-) create mode 100644 src/main/java/com/descope/model/magiclink/SignUpOptions.java create mode 100644 src/main/java/com/descope/model/mgmt/IDResponse.java create mode 100644 src/main/java/com/descope/model/sso/OIDCAttributeMapping.java create mode 100644 src/main/java/com/descope/model/sso/SSOOIDCSettings.java create mode 100644 src/main/java/com/descope/model/sso/SSOSAMLSettings.java create mode 100644 src/main/java/com/descope/model/sso/SSOSAMLSettingsByMetadata.java create mode 100644 src/main/java/com/descope/model/sso/SSOSAMLSettingsResponse.java create mode 100644 src/main/java/com/descope/model/sso/SSOTenantSettingsResponse.java create mode 100644 src/main/java/com/descope/model/ssoapp/OIDCApplicationRequest.java create mode 100644 src/main/java/com/descope/model/ssoapp/SAMLApplicationRequest.java create mode 100644 src/main/java/com/descope/model/ssoapp/SAMLIDPAttributeMappingInfo.java create mode 100644 src/main/java/com/descope/model/ssoapp/SAMLIDPGroupsMappingInfo.java create mode 100644 src/main/java/com/descope/model/ssoapp/SAMLIDPRoleGroupMappingInfo.java create mode 100644 src/main/java/com/descope/model/ssoapp/SSOApplication.java create mode 100644 src/main/java/com/descope/model/ssoapp/SSOApplicationOIDCSettings.java create mode 100644 src/main/java/com/descope/model/ssoapp/SSOApplicationSAMLSettings.java create mode 100644 src/main/java/com/descope/model/ssoapp/SSOApplications.java create mode 100644 src/main/java/com/descope/sdk/auth/SSOServiceProvider.java create mode 100644 src/main/java/com/descope/sdk/auth/impl/SSOServiceProviderImpl.java create mode 100644 src/main/java/com/descope/sdk/mgmt/SsoApplicationService.java create mode 100644 src/main/java/com/descope/sdk/mgmt/impl/SsoApplicationServiceImpl.java create mode 100644 src/test/java/com/descope/sdk/mgmt/impl/SsoApplicationServiceImplTest.java diff --git a/examples/management-cli/pom.xml b/examples/management-cli/pom.xml index e7bba2de..5cb24ae9 100644 --- a/examples/management-cli/pom.xml +++ b/examples/management-cli/pom.xml @@ -19,7 +19,7 @@ com.descope java-sdk - 1.0.13 + 1.0.14 info.picocli diff --git a/pom.xml b/pom.xml index da62baa9..79600a0d 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.descope java-sdk 4.0.0 - 1.0.13 + 1.0.14 ${project.groupId}:${project.artifactId} Java library used to integrate with Descope. https://github.com/descope/descope-java diff --git a/src/main/java/com/descope/exception/ServerCommonException.java b/src/main/java/com/descope/exception/ServerCommonException.java index ea92c470..43652bbe 100644 --- a/src/main/java/com/descope/exception/ServerCommonException.java +++ b/src/main/java/com/descope/exception/ServerCommonException.java @@ -19,6 +19,11 @@ protected ServerCommonException(String message, String code) { setCode(code); } + protected ServerCommonException(String message, String code, Throwable cause) { + super(message, cause); + setCode(code); + } + public static ServerCommonException invalidArgument(String property) { String message = String.format("The %s argument is invalid", property); return new ServerCommonException(message, INVALID_ARGUMENT); @@ -41,4 +46,10 @@ public static ServerCommonException genericServerError(String message, String co e.serverResponse = serverResponse; return e; } + + public static ServerCommonException parseResponseError(String message, String serverResponse, Throwable cause) { + ServerCommonException e = new ServerCommonException(message, null, cause); + e.serverResponse = serverResponse; + return e; + } } diff --git a/src/main/java/com/descope/literals/Routes.java b/src/main/java/com/descope/literals/Routes.java index 616a683b..a9bf552c 100644 --- a/src/main/java/com/descope/literals/Routes.java +++ b/src/main/java/com/descope/literals/Routes.java @@ -54,6 +54,10 @@ public static class AuthEndPoints { public static final String COMPOSE_SAML_START_LINK = "/v1/auth/saml/authorize"; public static final String EXCHANGE_SAML_LINK = "/v1/auth/saml/exchange"; + // SSO + public static final String COMPOSE_SSO_START_LINK = "/v1/auth/sso/authorize"; + public static final String EXCHANGE_SSO_LINK = "/v1/auth/sso/exchange"; + // Password public static final String SIGN_UP_PASSWORD_LINK = "/v1/auth/password/signup"; public static final String SIGN_IN_PASSWORD_LINK = "/v1/auth/password/signin"; @@ -97,6 +101,9 @@ public static class ManagementEndPoints { public static final String USER_SET_ROLES_LINK = "/v1/mgmt/user/update/role/set"; public static final String USER_ADD_ROLES_LINK = "/v1/mgmt/user/update/role/add"; public static final String USER_REMOVE_ROLES_LINK = "/v1/mgmt/user/update/role/remove"; + public static final String USER_SET_SSO_APPS_LINK = "/v1/mgmt/user/update/ssoapp/set"; + public static final String USER_ADD_SSO_APPS_LINK = "/v1/mgmt/user/update/ssoapp/add"; + public static final String USER_REMOVE_SSO_APPS_LINK = "/v1/mgmt/user/update/ssoapp/remove"; public static final String USER_ADD_TENANT_LINK = "/v1/mgmt/user/update/tenant/add"; public static final String USER_REMOVE_TENANT_LINK = "/v1/mgmt/user/update/tenant/remove"; public static final String GET_PROVIDER_TOKEN = "/v1/mgmt/user/provider/token"; @@ -118,11 +125,24 @@ public static class ManagementEndPoints { public static final String GET_TENANT_SETTINGS_LINK = "/v1/mgmt/tenant/settings"; // SSO - public static final String SSO_GET_SETTINGS_LINK = "/v2/mgmt/sso/settings"; + public static final String SSO_GET_SETTINGS_LINK = "/v1/mgmt/sso/settings"; public static final String SSO_DELETE_SETTINGS_LINK = "/v1/mgmt/sso/settings"; public static final String SSO_CONFIGURE_SETTINGS_LINK = "/v1/mgmt/sso/settings"; public static final String SSO_CONFIGURE_METADATA_LINK = "/v1/mgmt/sso/metadata"; public static final String SSO_CONFIGURE_MAPPING_LINK = "/v1/mgmt/sso/mapping"; + public static final String SSO_GET_SETTINGS_V2_LINK = "/v2/mgmt/sso/settings"; + public static final String SSO_CONFIGURE_SAML_SETTINGS_LINK = "/v1/mgmt/sso/saml"; + public static final String SSO_CONFIGURE_SAML_SETTINGS_BY_MD_LINK = "/v1/mgmt/sso/saml/metadata"; + public static final String SSO_CONFIGURE_OIDC_SETTINGS_LINK = "/v1/mgmt/sso/oidc"; + + // SSO Application + public static final String SSO_APPLICATION_OIDC_CREATE_LINK = "/v1/mgmt/sso/idp/app/oidc/create"; + public static final String SSO_APPLICATION_SAML_CREATE_LINK = "/v1/mgmt/sso/idp/app/saml/create"; + public static final String SSO_APPLICATION_OIDC_UPDATE_LINK = "/v1/mgmt/sso/idp/app/oidc/update"; + public static final String SSO_APPLICATION_SAML_UPDATE_LINK = "/v1/mgmt/sso/idp/app/saml/update"; + public static final String SSO_APPLICATION_DELETE_LINK = "/v1/mgmt/sso/idp/app/delete"; + public static final String SSO_APPLICATION_LOAD_LINK = "/v1/mgmt/sso/idp/app/load"; + public static final String SSO_APPLICATION_LOAD_ALL_LINK = "/v1/mgmt/sso/idp/apps/load"; // Group public static final String GROUP_LOAD_ALL_LINK = "/v1/mgmt/group/all"; diff --git a/src/main/java/com/descope/model/auth/AuthenticationServices.java b/src/main/java/com/descope/model/auth/AuthenticationServices.java index e6a58cc2..d2cc1704 100644 --- a/src/main/java/com/descope/model/auth/AuthenticationServices.java +++ b/src/main/java/com/descope/model/auth/AuthenticationServices.java @@ -7,6 +7,7 @@ import com.descope.sdk.auth.OTPService; import com.descope.sdk.auth.PasswordService; import com.descope.sdk.auth.SAMLService; +import com.descope.sdk.auth.SSOServiceProvider; import com.descope.sdk.auth.TOTPService; import com.descope.sdk.auth.WebAuthnService; import lombok.Builder; @@ -18,6 +19,7 @@ public class AuthenticationServices { AuthenticationService authService; OTPService otpService; SAMLService samlService; + SSOServiceProvider ssoServiceProvider; TOTPService totpService; OAuthService oauthService; PasswordService passwordService; diff --git a/src/main/java/com/descope/model/magiclink/LoginOptions.java b/src/main/java/com/descope/model/magiclink/LoginOptions.java index 2151a167..6d3f6778 100644 --- a/src/main/java/com/descope/model/magiclink/LoginOptions.java +++ b/src/main/java/com/descope/model/magiclink/LoginOptions.java @@ -14,4 +14,5 @@ public class LoginOptions { private boolean stepup; private boolean mfa; private Map customClaims; + private Map templateOptions; } diff --git a/src/main/java/com/descope/model/magiclink/SignUpOptions.java b/src/main/java/com/descope/model/magiclink/SignUpOptions.java new file mode 100644 index 00000000..1010d3b3 --- /dev/null +++ b/src/main/java/com/descope/model/magiclink/SignUpOptions.java @@ -0,0 +1,16 @@ +package com.descope.model.magiclink; + +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SignUpOptions { + private Map customClaims; + private Map templateOptions; +} diff --git a/src/main/java/com/descope/model/magiclink/request/SignUpRequest.java b/src/main/java/com/descope/model/magiclink/request/SignUpRequest.java index c5cead9b..26e29163 100644 --- a/src/main/java/com/descope/model/magiclink/request/SignUpRequest.java +++ b/src/main/java/com/descope/model/magiclink/request/SignUpRequest.java @@ -1,5 +1,6 @@ package com.descope.model.magiclink.request; +import com.descope.model.magiclink.SignUpOptions; import com.descope.model.user.User; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; @@ -18,4 +19,5 @@ public class SignUpRequest { @JsonProperty("URI") private String uri; + private SignUpOptions loginOptions; } diff --git a/src/main/java/com/descope/model/mgmt/IDResponse.java b/src/main/java/com/descope/model/mgmt/IDResponse.java new file mode 100644 index 00000000..af537496 --- /dev/null +++ b/src/main/java/com/descope/model/mgmt/IDResponse.java @@ -0,0 +1,14 @@ +package com.descope.model.mgmt; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class IDResponse { + private String id; +} diff --git a/src/main/java/com/descope/model/mgmt/ManagementServices.java b/src/main/java/com/descope/model/mgmt/ManagementServices.java index 8c7778e8..5203f870 100644 --- a/src/main/java/com/descope/model/mgmt/ManagementServices.java +++ b/src/main/java/com/descope/model/mgmt/ManagementServices.java @@ -10,6 +10,7 @@ import com.descope.sdk.mgmt.PermissionService; import com.descope.sdk.mgmt.ProjectService; import com.descope.sdk.mgmt.RolesService; +import com.descope.sdk.mgmt.SsoApplicationService; import com.descope.sdk.mgmt.SsoService; import com.descope.sdk.mgmt.TenantService; import com.descope.sdk.mgmt.UserService; @@ -26,6 +27,7 @@ public class ManagementServices { PermissionService permissionService; RolesService rolesService; SsoService ssoService; + SsoApplicationService ssoApplicationService; FlowService flowService; GroupService groupService; AuditService auditService; diff --git a/src/main/java/com/descope/model/otp/SignUpRequest.java b/src/main/java/com/descope/model/otp/SignUpRequest.java index 661d6417..fe02a4f6 100644 --- a/src/main/java/com/descope/model/otp/SignUpRequest.java +++ b/src/main/java/com/descope/model/otp/SignUpRequest.java @@ -1,5 +1,6 @@ package com.descope.model.otp; +import com.descope.model.magiclink.SignUpOptions; import com.descope.model.user.User; import lombok.AllArgsConstructor; import lombok.Builder; @@ -16,4 +17,5 @@ public class SignUpRequest { private String email; private String loginId; private User user; + private SignUpOptions loginOptions; } diff --git a/src/main/java/com/descope/model/sso/AttributeMapping.java b/src/main/java/com/descope/model/sso/AttributeMapping.java index 82f99279..50b76b10 100644 --- a/src/main/java/com/descope/model/sso/AttributeMapping.java +++ b/src/main/java/com/descope/model/sso/AttributeMapping.java @@ -1,17 +1,26 @@ package com.descope.model.sso; +import java.util.Map; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +/** + * Represents a SAML mapping between Descope and IDP user attributes. + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class AttributeMapping { private String name; + private String givenName; + private String middleName; + private String familyName; + private String picture; private String email; private String phoneNumber; private String group; + private Map customAttributes; } diff --git a/src/main/java/com/descope/model/sso/OIDCAttributeMapping.java b/src/main/java/com/descope/model/sso/OIDCAttributeMapping.java new file mode 100644 index 00000000..41899e62 --- /dev/null +++ b/src/main/java/com/descope/model/sso/OIDCAttributeMapping.java @@ -0,0 +1,27 @@ +package com.descope.model.sso; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents a SAML mapping between Descope and IDP user attributes. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class OIDCAttributeMapping { + private String loginId; + private String name; + private String givenName; + private String middleName; + private String familyName; + private String email; + private String verifiedEmail; + private String username; + private String phoneNumber; + private String verifiedPhone; + private String picture; +} diff --git a/src/main/java/com/descope/model/sso/RoleMapping.java b/src/main/java/com/descope/model/sso/RoleMapping.java index 08d85eec..c9c94fac 100644 --- a/src/main/java/com/descope/model/sso/RoleMapping.java +++ b/src/main/java/com/descope/model/sso/RoleMapping.java @@ -11,7 +11,6 @@ @NoArgsConstructor @AllArgsConstructor public class RoleMapping { - private List groups; - private String role; + private String roleName; } diff --git a/src/main/java/com/descope/model/sso/SSOOIDCSettings.java b/src/main/java/com/descope/model/sso/SSOOIDCSettings.java new file mode 100644 index 00000000..458b3e31 --- /dev/null +++ b/src/main/java/com/descope/model/sso/SSOOIDCSettings.java @@ -0,0 +1,31 @@ +package com.descope.model.sso; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SSOOIDCSettings { + private String name; + private String clientId; + private String clientSecret; + private String redirectUrl; + private String authUrl; + private String tokenUrl; + private String userDataUrl; + private List scope; + @JsonProperty("JWKsUrl") + private String jwksUrl; + private OIDCAttributeMapping userAttrMapping; + private Boolean manageProviderTokens; + private String callbackDomain; + private List prompt; + private String grantType; + private String issuer; +} diff --git a/src/main/java/com/descope/model/sso/SSOSAMLSettings.java b/src/main/java/com/descope/model/sso/SSOSAMLSettings.java new file mode 100644 index 00000000..8538e358 --- /dev/null +++ b/src/main/java/com/descope/model/sso/SSOSAMLSettings.java @@ -0,0 +1,19 @@ +package com.descope.model.sso; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SSOSAMLSettings { + private String idpUrl; + private String entityId; + private String idpCert; + private AttributeMapping attributeMapping; + private List roleMappings; +} diff --git a/src/main/java/com/descope/model/sso/SSOSAMLSettingsByMetadata.java b/src/main/java/com/descope/model/sso/SSOSAMLSettingsByMetadata.java new file mode 100644 index 00000000..d643c170 --- /dev/null +++ b/src/main/java/com/descope/model/sso/SSOSAMLSettingsByMetadata.java @@ -0,0 +1,17 @@ +package com.descope.model.sso; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SSOSAMLSettingsByMetadata { + private String idpMetadataUrl; + private AttributeMapping attributeMapping; + private List roleMappings; +} diff --git a/src/main/java/com/descope/model/sso/SSOSAMLSettingsResponse.java b/src/main/java/com/descope/model/sso/SSOSAMLSettingsResponse.java new file mode 100644 index 00000000..745d14b8 --- /dev/null +++ b/src/main/java/com/descope/model/sso/SSOSAMLSettingsResponse.java @@ -0,0 +1,24 @@ +package com.descope.model.sso; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SSOSAMLSettingsResponse { + private String idpEntityId; + private String idpSSOUrl; + private String idpCertificate; + private String idpMetadataUrl; + private String spEntityId; + private String spACSUrl; + private String spCertificate; + private AttributeMapping attributeMapping; + private List groupsMapping; + private String redirectUrl; +} diff --git a/src/main/java/com/descope/model/sso/SSOTenantSettingsResponse.java b/src/main/java/com/descope/model/sso/SSOTenantSettingsResponse.java new file mode 100644 index 00000000..85c5dd63 --- /dev/null +++ b/src/main/java/com/descope/model/sso/SSOTenantSettingsResponse.java @@ -0,0 +1,17 @@ +package com.descope.model.sso; + +import com.descope.model.tenant.Tenant; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SSOTenantSettingsResponse { + private Tenant tenant; + private SSOSAMLSettingsResponse saml; + private SSOOIDCSettings oidc; +} diff --git a/src/main/java/com/descope/model/ssoapp/OIDCApplicationRequest.java b/src/main/java/com/descope/model/ssoapp/OIDCApplicationRequest.java new file mode 100644 index 00000000..898645ce --- /dev/null +++ b/src/main/java/com/descope/model/ssoapp/OIDCApplicationRequest.java @@ -0,0 +1,37 @@ +package com.descope.model.ssoapp; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class OIDCApplicationRequest { + /** + * Optional ID that if given must be unique per project. Will be generated if not given. + */ + private String id; + /** + * The sso application's name. Must be unique per project. + */ + private String name; + /** + * Optional sso application description. + */ + private String description; + /** + * Optional set the sso application as enabled or disabled. + */ + private Boolean enabled; + /** + * Optional sso application logo. + */ + private String logo; + /** + * The URL where login page is hosted. + */ + private String loginPageUrl; +} diff --git a/src/main/java/com/descope/model/ssoapp/SAMLApplicationRequest.java b/src/main/java/com/descope/model/ssoapp/SAMLApplicationRequest.java new file mode 100644 index 00000000..7f18bdb0 --- /dev/null +++ b/src/main/java/com/descope/model/ssoapp/SAMLApplicationRequest.java @@ -0,0 +1,82 @@ +package com.descope.model.ssoapp; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SAMLApplicationRequest { + /** + * Optional sso application ID. Will be auto-generated if not given. Must be unique per project. + */ + private String id; + /** + * The sso application's name. Must be unique per project. + */ + private String name; + /** + * Optional sso application description. + */ + private String description; + /** + * Optional set the sso application as enabled or disabled. + */ + private Boolean enabled; + /** + * Optional sso application logo. + */ + private String logo; + /** + * The URL where login page is hosted. + */ + private String loginPageUrl; + /** + * Optional determine if SP info should be automatically fetched from metadata_url + * or by specified it by the entity_id, acs_url, certificate parameters. + */ + private Boolean useMetadataInfo; + /** + * Optional (must be set if UseMetadataInfo is True) SP metadata url which include all the SP SAML info. + */ + private String metadataUrl; + /** + * Optional (must be set if UseMetadataInfo is False) SP entity id. + */ + private String entityId; + /** + * Optional (must be set if UseMetadataInfo is False) SP ACS (saml callback) url. + */ + private String acsUrl; + /** + * Optional (must be set if UseMetadataInfo is False)SP certificate, relevant only when SAML request must be signed. + */ + private String certificate; + /** + * Optional list of Descope (IdP) attributes to SP mapping. + */ + private List attributeMapping; + /** + * Optional list of Descope (IdP) roles that will be mapped to SP groups. + */ + private List groupsMapping; + /** + * Optional list of urls wildcards strings represents the allowed ACS urls that will be accepted while arriving on the + * SAML request as SP callback urls. + */ + private List acsAllowedCallbacks; + /** + * Optional define the SAML Assertion subject name type, leave empty for using Descope user-id or set to + * "email"/"phone". + */ + private String subjectNameIdType; + /** + * Optional define the SAML Assertion subject name format, leave empty for using + * "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified". + */ + private String subjectNameIdFormat; +} diff --git a/src/main/java/com/descope/model/ssoapp/SAMLIDPAttributeMappingInfo.java b/src/main/java/com/descope/model/ssoapp/SAMLIDPAttributeMappingInfo.java new file mode 100644 index 00000000..5a5fce89 --- /dev/null +++ b/src/main/java/com/descope/model/ssoapp/SAMLIDPAttributeMappingInfo.java @@ -0,0 +1,16 @@ +package com.descope.model.ssoapp; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SAMLIDPAttributeMappingInfo { + private String name; + private String type; + private String value; +} diff --git a/src/main/java/com/descope/model/ssoapp/SAMLIDPGroupsMappingInfo.java b/src/main/java/com/descope/model/ssoapp/SAMLIDPGroupsMappingInfo.java new file mode 100644 index 00000000..add86d3d --- /dev/null +++ b/src/main/java/com/descope/model/ssoapp/SAMLIDPGroupsMappingInfo.java @@ -0,0 +1,19 @@ +package com.descope.model.ssoapp; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SAMLIDPGroupsMappingInfo { + private String name; + private String type; + private String filterType; + private String value; + private List roles; +} diff --git a/src/main/java/com/descope/model/ssoapp/SAMLIDPRoleGroupMappingInfo.java b/src/main/java/com/descope/model/ssoapp/SAMLIDPRoleGroupMappingInfo.java new file mode 100644 index 00000000..9e02be7c --- /dev/null +++ b/src/main/java/com/descope/model/ssoapp/SAMLIDPRoleGroupMappingInfo.java @@ -0,0 +1,15 @@ +package com.descope.model.ssoapp; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SAMLIDPRoleGroupMappingInfo { + private String id; + private String name; +} diff --git a/src/main/java/com/descope/model/ssoapp/SSOApplication.java b/src/main/java/com/descope/model/ssoapp/SSOApplication.java new file mode 100644 index 00000000..5935b4bf --- /dev/null +++ b/src/main/java/com/descope/model/ssoapp/SSOApplication.java @@ -0,0 +1,21 @@ +package com.descope.model.ssoapp; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SSOApplication { + private String id; + private String name; + private String description; + private Boolean enabled; + private String logo; + private String appType; + private SSOApplicationSAMLSettings samlSettings; + private SSOApplicationOIDCSettings oidcSettings; +} diff --git a/src/main/java/com/descope/model/ssoapp/SSOApplicationOIDCSettings.java b/src/main/java/com/descope/model/ssoapp/SSOApplicationOIDCSettings.java new file mode 100644 index 00000000..dad840a9 --- /dev/null +++ b/src/main/java/com/descope/model/ssoapp/SSOApplicationOIDCSettings.java @@ -0,0 +1,16 @@ +package com.descope.model.ssoapp; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SSOApplicationOIDCSettings { + private String loginPageUrl; + private String issuer; + private String discoveryUrl; +} diff --git a/src/main/java/com/descope/model/ssoapp/SSOApplicationSAMLSettings.java b/src/main/java/com/descope/model/ssoapp/SSOApplicationSAMLSettings.java new file mode 100644 index 00000000..1b97d81f --- /dev/null +++ b/src/main/java/com/descope/model/ssoapp/SSOApplicationSAMLSettings.java @@ -0,0 +1,29 @@ +package com.descope.model.ssoapp; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SSOApplicationSAMLSettings { + private String loginPageUrl; + private String idpCert; + private Boolean useMetadataInfo; + private String metadataUrl; + private String entityId; + private String acsUrl; + private String certificate; + private List attributeMapping; + private List groupsMapping; + private String idpMetadataUrl; + private String idpEntityId; + private String idpSsoUrl; + private List acsAllowedCallbacks; + private String subjectNameIdType; + private String subjectNameIdFormat; +} diff --git a/src/main/java/com/descope/model/ssoapp/SSOApplications.java b/src/main/java/com/descope/model/ssoapp/SSOApplications.java new file mode 100644 index 00000000..c560bda4 --- /dev/null +++ b/src/main/java/com/descope/model/ssoapp/SSOApplications.java @@ -0,0 +1,15 @@ +package com.descope.model.ssoapp; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SSOApplications { + private List apps; +} diff --git a/src/main/java/com/descope/model/tenant/request/TenantSearchRequest.java b/src/main/java/com/descope/model/tenant/request/TenantSearchRequest.java index 9dcd7a61..3334b94b 100644 --- a/src/main/java/com/descope/model/tenant/request/TenantSearchRequest.java +++ b/src/main/java/com/descope/model/tenant/request/TenantSearchRequest.java @@ -20,4 +20,5 @@ public class TenantSearchRequest { Map customAttributes; @JsonProperty("tenantSelfProvisioningDomains") List selfProvisioningDomains; + String authType; } diff --git a/src/main/java/com/descope/model/user/request/UserRequest.java b/src/main/java/com/descope/model/user/request/UserRequest.java index d0073f80..18f4ecf3 100644 --- a/src/main/java/com/descope/model/user/request/UserRequest.java +++ b/src/main/java/com/descope/model/user/request/UserRequest.java @@ -31,6 +31,7 @@ public class UserRequest { String picture; Boolean test; List additionalLoginIds; + List ssoAppIds; public Map toMap() { Map m = new HashMap<>(); @@ -48,6 +49,7 @@ public Map toMap() { addIfNotNull(m, "picture", picture); addIfNotNull(m, "test", test); addIfNotNull(m, "additionalLoginIds", additionalLoginIds); + addIfNotNull(m, "ssoAppIDs", ssoAppIds); return m; } } diff --git a/src/main/java/com/descope/model/user/response/UserResponse.java b/src/main/java/com/descope/model/user/response/UserResponse.java index 304fae4b..e2530885 100644 --- a/src/main/java/com/descope/model/user/response/UserResponse.java +++ b/src/main/java/com/descope/model/user/response/UserResponse.java @@ -32,4 +32,5 @@ public class UserResponse { Boolean TOTP; Boolean SAML; Map oAuth; + List ssoAppIds; } diff --git a/src/main/java/com/descope/proxy/impl/AbstractProxyImpl.java b/src/main/java/com/descope/proxy/impl/AbstractProxyImpl.java index 3ed7d68f..278d442d 100644 --- a/src/main/java/com/descope/proxy/impl/AbstractProxyImpl.java +++ b/src/main/java/com/descope/proxy/impl/AbstractProxyImpl.java @@ -104,15 +104,19 @@ public R handleResponse(ClassicHttpResponse response) throws HttpException, IOEx bs.toString(), String.valueOf(res.getCode()), bs.toString()); } } - R r = returnClz != null - ? objectMapper.readValue(tee, returnClz) - : objectMapper.readValue(tee, typeReference); - if (log.isDebugEnabled()) { - String resStr = bs.toString(); - log.debug(String.format("Received response %s", - resStr.substring(0, resStr.length() > 10000 ? 10000 : resStr.length()))); + try { + R r = returnClz != null + ? objectMapper.readValue(tee, returnClz) + : objectMapper.readValue(tee, typeReference); + if (log.isDebugEnabled()) { + String resStr = bs.toString(); + log.debug(String.format("Received response %s", + resStr.substring(0, resStr.length() > 10000 ? 10000 : resStr.length()))); + } + return r; + } catch (Exception e) { + throw ServerCommonException.parseResponseError("Error parsing response", bs.toString(), e); } - return r; } } }); diff --git a/src/main/java/com/descope/sdk/auth/EnchantedLinkService.java b/src/main/java/com/descope/sdk/auth/EnchantedLinkService.java index fa6ce9d1..c33ed7ed 100644 --- a/src/main/java/com/descope/sdk/auth/EnchantedLinkService.java +++ b/src/main/java/com/descope/sdk/auth/EnchantedLinkService.java @@ -5,6 +5,7 @@ import com.descope.model.auth.UpdateOptions; import com.descope.model.enchantedlink.EnchantedLinkResponse; import com.descope.model.magiclink.LoginOptions; +import com.descope.model.magiclink.SignUpOptions; import com.descope.model.user.User; public interface EnchantedLinkService { @@ -37,6 +38,19 @@ EnchantedLinkResponse signIn( EnchantedLinkResponse signUp(String loginId, String uri, User user) throws DescopeException; + /** + * Use to create a new user based on the given loginID either email or a phone. + * + * @param loginId - User login ID + * @param uri - Base URI + * @param user - {@link User User} + * @param signupOptions - optional claims and template strings + * @return pendingRef, linkId and masked email + * @throws DescopeException - error upon failure + */ + EnchantedLinkResponse signUp(String loginId, String uri, User user, SignUpOptions signupOptions) + throws DescopeException; + /** * Use to login in using loginID, if user does not exist, a new user will be created. * diff --git a/src/main/java/com/descope/sdk/auth/MagicLinkService.java b/src/main/java/com/descope/sdk/auth/MagicLinkService.java index 08d90185..0029394f 100644 --- a/src/main/java/com/descope/sdk/auth/MagicLinkService.java +++ b/src/main/java/com/descope/sdk/auth/MagicLinkService.java @@ -5,6 +5,7 @@ import com.descope.model.auth.AuthenticationInfo; import com.descope.model.auth.UpdateOptions; import com.descope.model.magiclink.LoginOptions; +import com.descope.model.magiclink.SignUpOptions; import com.descope.model.user.User; public interface MagicLinkService { @@ -45,6 +46,21 @@ String signIn( String signUp(DeliveryMethod deliveryMethod, String loginId, String uri, User user) throws DescopeException; + /** + * Use to create a new user based on the given loginID either email, sms or whatsapp. Choose the + * selected delivery method for verification. + * + * @param deliveryMethod - {@link com.descope.enums.DeliveryMethod DeliveryMethod} + * @param loginId - User login ID + * @param uri - Base URI + * @param user - {@link User User} + * @param signupOptions - optional claims and template strings + * @return masked address where the link was sent (email, whatsapp or phone) + * @throws DescopeException - error upon failure + */ + String signUp(DeliveryMethod deliveryMethod, String loginId, String uri, User user, SignUpOptions signupOptions) + throws DescopeException; + /** * Verify - Use to verify a SignIn/SignUp request, based on the magic link token generated. if the * link was generated with crossDevice, the authentication info will be nil, and should returned diff --git a/src/main/java/com/descope/sdk/auth/OTPService.java b/src/main/java/com/descope/sdk/auth/OTPService.java index de57cb22..5220bc7a 100644 --- a/src/main/java/com/descope/sdk/auth/OTPService.java +++ b/src/main/java/com/descope/sdk/auth/OTPService.java @@ -5,6 +5,7 @@ import com.descope.model.auth.AuthenticationInfo; import com.descope.model.auth.UpdateOptions; import com.descope.model.magiclink.LoginOptions; +import com.descope.model.magiclink.SignUpOptions; import com.descope.model.user.User; public interface OTPService { @@ -21,6 +22,20 @@ public interface OTPService { String signIn(DeliveryMethod deliveryMethod, String loginId, LoginOptions loginOptions) throws DescopeException; + /** + * Use to login a user based on the given loginID either email or a phone and choose the selected + * delivery method for verification. + * + * @param deliveryMethod - {@link com.descope.enums.DeliveryMethod DeliveryMethod} + * @param loginId - User login ID + * @param loginOptions - {@link LoginOptions LoginOptions} + * @param refreshToken - if doing step-up or mfa refresh token is required + * @return - masked address where the link was sent (email or phone) + * @throws DescopeException - error upon failure + */ + String signIn(DeliveryMethod deliveryMethod, String loginId, LoginOptions loginOptions, String refreshToken) + throws DescopeException; + /** * Use to create a new user based on the given loginID either email or a phone. choose the * selected delivery method for verification. @@ -33,6 +48,20 @@ String signIn(DeliveryMethod deliveryMethod, String loginId, LoginOptions loginO */ String signUp(DeliveryMethod deliveryMethod, String loginId, User user) throws DescopeException; + /** + * Use to create a new user based on the given loginID either email or a phone. choose the + * selected delivery method for verification. + * + * @param deliveryMethod - {@link com.descope.enums.DeliveryMethod DeliveryMethod} + * @param loginId - User login ID + * @param user - {@link User User} + * @param signupOptions - optional claims and template strings + * @return masked address where the link was sent (email, whatsapp or phone) + * @throws DescopeException - error upon failure + */ + String signUp(DeliveryMethod deliveryMethod, String loginId, User user, SignUpOptions signupOptions) + throws DescopeException; + /** * Use to login in using loginID, if user does not exist, a new user will be created with the * given loginID. diff --git a/src/main/java/com/descope/sdk/auth/SSOServiceProvider.java b/src/main/java/com/descope/sdk/auth/SSOServiceProvider.java new file mode 100644 index 00000000..f65013af --- /dev/null +++ b/src/main/java/com/descope/sdk/auth/SSOServiceProvider.java @@ -0,0 +1,44 @@ +package com.descope.sdk.auth; + +import com.descope.exception.DescopeException; +import com.descope.model.auth.AuthenticationInfo; +import com.descope.model.magiclink.LoginOptions; + +public interface SSOServiceProvider { + /** + * Start will initiate an SSO login flow. + * + * @param tenant - tenant + * @param redirectUrl - URL to redirect user to overriding configuration + * @param prompt - Prompt to the user overriding configuration + * @param loginOptions - {@link LoginOptions loginOptions} + * @return will be the redirect URL that needs to return to client + * @throws DescopeException - error upon failure + */ + String start(String tenant, String redirectUrl, String prompt, LoginOptions loginOptions) throws DescopeException; + + /** + * Start will initiate an SSO login flow. + * + * @param tenant - tenant + * @param redirectUrl - URL to redirect user to overriding configuration + * @param prompt - Prompt to the user overriding configuration + * @param loginOptions - {@link LoginOptions loginOptions} + * @param refreshToken - if we are doing step-up or MFA, existing refresh token is required + * @return will be the redirect URL that needs to return to client + * @throws DescopeException - error upon failure + */ + String start(String tenant, String redirectUrl, String prompt, LoginOptions loginOptions, String refreshToken) + throws DescopeException; + + /** + * ExchangeToken - Finalize SAML authentication. + * + * @param code - Code to be validated + * @return Authentication info + * @throws DescopeException - error upon failure + */ + + AuthenticationInfo exchangeToken(String code) throws DescopeException; + +} diff --git a/src/main/java/com/descope/sdk/auth/impl/AuthenticationServiceBuilder.java b/src/main/java/com/descope/sdk/auth/impl/AuthenticationServiceBuilder.java index cc4d9868..f07b6eb1 100644 --- a/src/main/java/com/descope/sdk/auth/impl/AuthenticationServiceBuilder.java +++ b/src/main/java/com/descope/sdk/auth/impl/AuthenticationServiceBuilder.java @@ -11,6 +11,7 @@ public static AuthenticationServices buildServices(Client client) { .authService(new AuthenticationServiceImpl(client)) .otpService(new OTPServiceImpl(client)) .samlService(new SAMLServiceImpl(client)) + .ssoServiceProvider(new SSOServiceProviderImpl(client)) .totpService(new TOTPServiceImpl(client)) .oauthService(new OAuthServiceImpl(client)) .passwordService(new PasswordServiceImpl(client)) diff --git a/src/main/java/com/descope/sdk/auth/impl/EnchantedLinkServiceImpl.java b/src/main/java/com/descope/sdk/auth/impl/EnchantedLinkServiceImpl.java index 2d2b8220..4fc8128f 100644 --- a/src/main/java/com/descope/sdk/auth/impl/EnchantedLinkServiceImpl.java +++ b/src/main/java/com/descope/sdk/auth/impl/EnchantedLinkServiceImpl.java @@ -19,6 +19,7 @@ import com.descope.model.enchantedlink.EnchantedLinkSessionBody; import com.descope.model.jwt.response.JWTResponse; import com.descope.model.magiclink.LoginOptions; +import com.descope.model.magiclink.SignUpOptions; import com.descope.model.magiclink.request.SignInRequest; import com.descope.model.magiclink.request.SignUpRequest; import com.descope.model.magiclink.request.UpdateEmailRequest; @@ -59,6 +60,12 @@ public EnchantedLinkResponse signIn(String loginId, String uri, String token, Lo @Override public EnchantedLinkResponse signUp(String loginId, String uri, User user) throws DescopeException { + return signUp(loginId, uri, user, null); + } + + @Override + public EnchantedLinkResponse signUp(String loginId, String uri, User user, SignUpOptions signupOptions) + throws DescopeException { if (user == null) { user = new User(); } @@ -68,6 +75,9 @@ public EnchantedLinkResponse signUp(String loginId, String uri, User user) if (StringUtils.isBlank(user.getEmail())) { user.setEmail(loginId); } + if (signupOptions != null) { + signUpRequestBuilder.loginOptions(signupOptions); + } SignUpRequest signUpRequest = signUpRequestBuilder.user(user).build(); ApiProxy apiProxy = getApiProxy(); return apiProxy.post(enchantedLinkSignUpURL, signUpRequest, EnchantedLinkResponse.class); diff --git a/src/main/java/com/descope/sdk/auth/impl/MagicLinkServiceImpl.java b/src/main/java/com/descope/sdk/auth/impl/MagicLinkServiceImpl.java index c37a28fb..802e79dc 100644 --- a/src/main/java/com/descope/sdk/auth/impl/MagicLinkServiceImpl.java +++ b/src/main/java/com/descope/sdk/auth/impl/MagicLinkServiceImpl.java @@ -19,6 +19,7 @@ import com.descope.model.client.Client; import com.descope.model.jwt.response.JWTResponse; import com.descope.model.magiclink.LoginOptions; +import com.descope.model.magiclink.SignUpOptions; import com.descope.model.magiclink.request.SignInRequest; import com.descope.model.magiclink.request.SignUpRequest; import com.descope.model.magiclink.request.UpdateEmailRequest; @@ -67,8 +68,13 @@ public String signIn( } @Override - public String signUp(DeliveryMethod deliveryMethod, String loginId, String uri, User user) - throws DescopeException { + public String signUp(DeliveryMethod deliveryMethod, String loginId, String uri, User user) { + return signUp(deliveryMethod, loginId, uri, user, null); + } + + @Override + public String signUp(DeliveryMethod deliveryMethod, String loginId, String uri, User user, + SignUpOptions signupOptions) throws DescopeException { if (user == null) { user = new User(); } @@ -81,7 +87,9 @@ public String signUp(DeliveryMethod deliveryMethod, String loginId, String uri, if (EMAIL.equals(deliveryMethod)) { signUpRequestBuilder.email(user.getEmail()); } - + if (signupOptions != null) { + signUpRequestBuilder.loginOptions(signupOptions); + } SignUpRequest signUpRequest = signUpRequestBuilder.user(user).build(); ApiProxy apiProxy = getApiProxy(); Masked masked = apiProxy.post(magicLinkSignUpURL, signUpRequest, maskedClass); diff --git a/src/main/java/com/descope/sdk/auth/impl/OTPServiceImpl.java b/src/main/java/com/descope/sdk/auth/impl/OTPServiceImpl.java index cf2b7024..e2963e9d 100644 --- a/src/main/java/com/descope/sdk/auth/impl/OTPServiceImpl.java +++ b/src/main/java/com/descope/sdk/auth/impl/OTPServiceImpl.java @@ -20,6 +20,7 @@ import com.descope.model.client.Client; import com.descope.model.jwt.response.JWTResponse; import com.descope.model.magiclink.LoginOptions; +import com.descope.model.magiclink.SignUpOptions; import com.descope.model.magiclink.response.Masked; import com.descope.model.otp.AuthenticationVerifyRequestBody; import com.descope.model.otp.SignInRequest; @@ -40,7 +41,12 @@ class OTPServiceImpl extends AuthenticationServiceImpl implements OTPService { } @Override - public String signIn(DeliveryMethod deliveryMethod, String loginId, LoginOptions loginOptions) + public String signIn(DeliveryMethod deliveryMethod, String loginId, LoginOptions loginOptions) { + return signIn(deliveryMethod, loginId, loginOptions, null); + } + + @Override + public String signIn(DeliveryMethod deliveryMethod, String loginId, LoginOptions loginOptions, String refreshToken) throws DescopeException { if (StringUtils.isBlank(loginId)) { throw ServerCommonException.invalidArgument("Login ID"); @@ -50,8 +56,10 @@ public String signIn(DeliveryMethod deliveryMethod, String loginId, LoginOptions URI otpSignInURL = composeSignInURL(deliveryMethod); SignInRequest signInRequest = new SignInRequest(loginId, loginOptions); if (JwtUtils.isJWTRequired(loginOptions)) { - String pwd = ""; // getValidRefreshToken(request); - apiProxy = getApiProxy(pwd); + if (StringUtils.isBlank(refreshToken)) { + throw ServerCommonException.invalidArgument("refreshToken"); + } + apiProxy = getApiProxy(refreshToken); } else { apiProxy = getApiProxy(); } @@ -60,19 +68,25 @@ public String signIn(DeliveryMethod deliveryMethod, String loginId, LoginOptions } @Override - public String signUp(DeliveryMethod deliveryMethod, String loginId, User user) + public String signUp(DeliveryMethod deliveryMethod, String loginId, User user) { + return signUp(deliveryMethod, loginId, user, null); + } + + @Override + public String signUp(DeliveryMethod deliveryMethod, String loginId, User user, SignUpOptions signUpOptions) throws DescopeException { if (user == null) { user = new User(); } verifyDeliveryMethod(deliveryMethod, loginId, user); - Class maskedClass = getMaskedValue(deliveryMethod); - URI otpSignUpURL = composeSignUpURI(deliveryMethod); - SignUpRequest signUpRequest = newSignUpRequest(deliveryMethod, user); signUpRequest.setLoginId(loginId); signUpRequest.setUser(user); - + if (signUpOptions != null) { + signUpRequest.setLoginOptions(signUpOptions); + } + Class maskedClass = getMaskedValue(deliveryMethod); + URI otpSignUpURL = composeSignUpURI(deliveryMethod); ApiProxy apiProxy = getApiProxy(); Masked masked = apiProxy.post(otpSignUpURL, signUpRequest, maskedClass); return masked.getMasked(); diff --git a/src/main/java/com/descope/sdk/auth/impl/SSOServiceProviderImpl.java b/src/main/java/com/descope/sdk/auth/impl/SSOServiceProviderImpl.java new file mode 100644 index 00000000..6f705a13 --- /dev/null +++ b/src/main/java/com/descope/sdk/auth/impl/SSOServiceProviderImpl.java @@ -0,0 +1,59 @@ +package com.descope.sdk.auth.impl; + +import static com.descope.literals.Routes.AuthEndPoints.COMPOSE_SSO_START_LINK; +import static com.descope.literals.Routes.AuthEndPoints.EXCHANGE_SSO_LINK; +import static com.descope.utils.CollectionUtils.mapOf; + +import com.descope.exception.DescopeException; +import com.descope.exception.ServerCommonException; +import com.descope.model.auth.AuthenticationInfo; +import com.descope.model.auth.OAuthResponse; +import com.descope.model.client.Client; +import com.descope.model.magiclink.LoginOptions; +import com.descope.proxy.ApiProxy; +import com.descope.sdk.auth.SSOServiceProvider; +import com.descope.utils.JwtUtils; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; + +class SSOServiceProviderImpl extends AuthenticationServiceImpl implements SSOServiceProvider { + + SSOServiceProviderImpl(Client client) { + super(client); + } + + @Override + public String start(String tenant, String redirectUrl, String prompt, LoginOptions loginOptions) { + return start(tenant, redirectUrl, prompt, loginOptions, null); + } + + @Override + public String start(String tenant, String redirectUrl, String prompt, LoginOptions loginOptions, String refreshToken) + throws DescopeException { + if (StringUtils.isBlank(tenant)) { + throw ServerCommonException.invalidArgument("Tenant"); + } + Map request = mapOf("tenant", tenant); + if (StringUtils.isNotBlank(redirectUrl)) { + request.put("redirectURL", redirectUrl); + } + if (StringUtils.isNotBlank(prompt)) { + request.put("prompt", prompt); + } + ApiProxy apiProxy; + if (JwtUtils.isJWTRequired(loginOptions)) { + if (StringUtils.isBlank(refreshToken)) { + throw ServerCommonException.invalidArgument("refreshToken"); + } + apiProxy = getApiProxy(refreshToken); + } else { + apiProxy = getApiProxy(); + } + return apiProxy.post(getQueryParamUri(COMPOSE_SSO_START_LINK, request), loginOptions, OAuthResponse.class).getUrl(); + } + + @Override + public AuthenticationInfo exchangeToken(String code) throws DescopeException { + return exchangeToken(code, getUri(EXCHANGE_SSO_LINK)); + } +} diff --git a/src/main/java/com/descope/sdk/mgmt/SsoApplicationService.java b/src/main/java/com/descope/sdk/mgmt/SsoApplicationService.java new file mode 100644 index 00000000..821be464 --- /dev/null +++ b/src/main/java/com/descope/sdk/mgmt/SsoApplicationService.java @@ -0,0 +1,72 @@ +package com.descope.sdk.mgmt; + +import com.descope.exception.DescopeException; +import com.descope.model.ssoapp.OIDCApplicationRequest; +import com.descope.model.ssoapp.SAMLApplicationRequest; +import com.descope.model.ssoapp.SSOApplication; +import java.util.List; + +public interface SsoApplicationService { + + /** + * Create a new OIDC SSO application with the given name. + * + * @param appRequest the OIDC application details + * @return the new ID of the application + * @throws DescopeException If error, a subtype of this exception will be thrown + */ + String createOIDCApplication(OIDCApplicationRequest appRequest) throws DescopeException; + + /** + * Create a new SAML SSO application with the given name. + * + * @param appRequest the SAML application details + * @return the new ID of the application + * @throws DescopeException If error, a subtype of this exception will be thrown + */ + String createSAMLApplication(SAMLApplicationRequest appRequest) throws DescopeException; + + /** + * Update an existing OIDC sso application. + * IMPORTANT: All parameters are required and will override whatever value is currently + * + * @param appRequest the application details. + * @throws DescopeException If error, a subtype of this exception will be thrown + */ + void updateOIDCApplication(OIDCApplicationRequest appRequest) throws DescopeException; + + /** + * Update an existing SAML sso application. + * IMPORTANT: All parameters are required and will override whatever value is currently + * + * @param appRequest the application details. + * @throws DescopeException If error, a subtype of this exception will be thrown + */ + void updateSAMLApplication(SAMLApplicationRequest appRequest) throws DescopeException; + + /** + * Delete an existing sso application. + * IMPORTANT: This action is irreversible. Use carefully. + * + * @param id application ID to delete + * @throws DescopeException If error, a subtype of this exception will be thrown + */ + void delete(String id) throws DescopeException; + + /** + * Load project sso application by id. + * + * @param id ID of application to load + * @return {@link SSOApplication} details + * @throws DescopeException If error, a subtype of this exception will be thrown + */ + SSOApplication load(String id) throws DescopeException; + + /** + * Load all project sso applications. + * + * @return {@link SSOApplication} details + * @throws DescopeException If error, a subtype of this exception will be thrown + */ + List loadAll() throws DescopeException; +} diff --git a/src/main/java/com/descope/sdk/mgmt/SsoService.java b/src/main/java/com/descope/sdk/mgmt/SsoService.java index de167559..8b975d18 100644 --- a/src/main/java/com/descope/sdk/mgmt/SsoService.java +++ b/src/main/java/com/descope/sdk/mgmt/SsoService.java @@ -3,19 +3,77 @@ import com.descope.exception.DescopeException; import com.descope.model.sso.AttributeMapping; import com.descope.model.sso.RoleMapping; +import com.descope.model.sso.SSOOIDCSettings; +import com.descope.model.sso.SSOSAMLSettings; +import com.descope.model.sso.SSOSAMLSettingsByMetadata; import com.descope.model.sso.SSOSettingsResponse; +import com.descope.model.sso.SSOTenantSettingsResponse; import java.util.List; public interface SsoService { + /** + * Load all tenant SSO setting. + * + * @param tenantId the tenant ID we are loading settings for + * @return {@link SSOTenantSettingsResponse} all SSO settings for the tenant + * @throws DescopeException If error, a subtype of this exception will be thrown + */ + SSOTenantSettingsResponse loadSettings(String tenantId) throws DescopeException; + + /** + * Configure SSO SAML settings for a tenant manually. + * + * @param tenantId required tenant ID + * @param settings required settings with all fields set + * @param redirectUrl optional. If absent, must be specified when starting an SSO authentication via the request + * @param domains optional and is used to map users to this tenant when authenticating via SSO. + * @throws DescopeException If error, a subtype of this exception will be thrown + */ + void configureSAMLSettings(String tenantId, SSOSAMLSettings settings, String redirectUrl, List domains) + throws DescopeException; + + /** + * Configure SSO SAML settings for a tenant by fetching them from an IDP metadata URL. + * + * @param tenantId required tenant ID + * @param settings required settings with all fields set + * @param redirectUrl optional. If absent, must be specified when starting an SSO authentication via the request + * @param domains optional and is used to map users to this tenant when authenticating via SSO. + * @throws DescopeException If error, a subtype of this exception will be thrown + */ + void configureSAMLSettingsByMetadata(String tenantId, SSOSAMLSettingsByMetadata settings, String redirectUrl, + List domains) throws DescopeException; + + /** + * Configure SSO OIDC settings for a tenant manually. + * + * @param tenantId required tenant ID + * @param settings required settings + * @param domains optional and is used to map users to this tenant when authenticating via SSO. + * @throws DescopeException If error, a subtype of this exception will be thrown + */ + void configureOIDCSettings(String tenantId, SSOOIDCSettings settings, List domains) throws DescopeException; + + /** + * Delete the SSO settings for the given tenant. + * + * @param tenantId required tenant ID + * @throws DescopeException If error, a subtype of this exception will be thrown + */ + void deleteSettings(String tenantId) throws DescopeException; + + @Deprecated SSOSettingsResponse getSettings(String tenantID) throws DescopeException; - void deleteSettings(String tenantID) throws DescopeException; + @Deprecated void configureSettings(String tenantID, String idpURL, String idpCert, String entityID, String redirectURL, List domains) throws DescopeException; + @Deprecated void configureMetadata(String tenantID, String idpMetadataURL) throws DescopeException; + @Deprecated void configureMapping(String tenantID, List roleMapping, AttributeMapping attributeMapping) throws DescopeException; } diff --git a/src/main/java/com/descope/sdk/mgmt/UserService.java b/src/main/java/com/descope/sdk/mgmt/UserService.java index 5888341c..7fd5efba 100644 --- a/src/main/java/com/descope/sdk/mgmt/UserService.java +++ b/src/main/java/com/descope/sdk/mgmt/UserService.java @@ -317,6 +317,39 @@ UserResponseDetails updateCustomAttributes(String loginId, String key, Object va */ UserResponseDetails removeRoles(String loginId, List roles) throws DescopeException; + /** + * Set (associate) SSO applications for a user. + * + * @param loginId The loginID is required. + * @param ssoAppIds List of SSO apps to associate + * @return {@link UserResponseDetails UserResponseDetails} + * @throws DescopeException If there occurs any exception, a subtype of this exception will be + * thrown. + */ + UserResponseDetails addSsoApps(String loginId, List ssoAppIds) throws DescopeException; + + /** + * Remove SSO application association from a user. + * + * @param loginId The loginID is required. + * @param ssoAppIds List of SSO apps to associate + * @return {@link UserResponseDetails UserResponseDetails} + * @throws DescopeException If there occurs any exception, a subtype of this exception will be + * thrown. + */ + UserResponseDetails removeSsoApps(String loginId, List ssoAppIds) throws DescopeException; + + /** + * Associate SSO applications for a user. + * + * @param loginId The loginID is required. + * @param ssoAppIds List of SSO apps to associate + * @return {@link UserResponseDetails UserResponseDetails} + * @throws DescopeException If there occurs any exception, a subtype of this exception will be + * thrown. + */ + UserResponseDetails setSsoApps(String loginId, List ssoAppIds) throws DescopeException; + /** * Add a tenant association for an existing user. * diff --git a/src/main/java/com/descope/sdk/mgmt/impl/ManagementServiceBuilder.java b/src/main/java/com/descope/sdk/mgmt/impl/ManagementServiceBuilder.java index cdab40d8..098cb870 100644 --- a/src/main/java/com/descope/sdk/mgmt/impl/ManagementServiceBuilder.java +++ b/src/main/java/com/descope/sdk/mgmt/impl/ManagementServiceBuilder.java @@ -21,6 +21,7 @@ public static ManagementServices buildServices(Client client) { .authzService(new AuthzServiceImpl(client)) .projectService(new ProjectServiceImpl(client)) .passwordSettingsService(new PasswordSettingsServiceImpl(client)) + .ssoApplicationService(new SsoApplicationServiceImpl(client)) .build(); } } diff --git a/src/main/java/com/descope/sdk/mgmt/impl/SsoApplicationServiceImpl.java b/src/main/java/com/descope/sdk/mgmt/impl/SsoApplicationServiceImpl.java new file mode 100644 index 00000000..a2496a37 --- /dev/null +++ b/src/main/java/com/descope/sdk/mgmt/impl/SsoApplicationServiceImpl.java @@ -0,0 +1,110 @@ +package com.descope.sdk.mgmt.impl; + +import static com.descope.literals.Routes.ManagementEndPoints.SSO_APPLICATION_DELETE_LINK; +import static com.descope.literals.Routes.ManagementEndPoints.SSO_APPLICATION_LOAD_ALL_LINK; +import static com.descope.literals.Routes.ManagementEndPoints.SSO_APPLICATION_LOAD_LINK; +import static com.descope.literals.Routes.ManagementEndPoints.SSO_APPLICATION_OIDC_CREATE_LINK; +import static com.descope.literals.Routes.ManagementEndPoints.SSO_APPLICATION_OIDC_UPDATE_LINK; +import static com.descope.literals.Routes.ManagementEndPoints.SSO_APPLICATION_SAML_CREATE_LINK; +import static com.descope.literals.Routes.ManagementEndPoints.SSO_APPLICATION_SAML_UPDATE_LINK; +import static com.descope.utils.CollectionUtils.mapOf; + +import com.descope.exception.DescopeException; +import com.descope.exception.ServerCommonException; +import com.descope.model.client.Client; +import com.descope.model.mgmt.IDResponse; +import com.descope.model.ssoapp.OIDCApplicationRequest; +import com.descope.model.ssoapp.SAMLApplicationRequest; +import com.descope.model.ssoapp.SSOApplication; +import com.descope.model.ssoapp.SSOApplications; +import com.descope.proxy.ApiProxy; +import com.descope.sdk.mgmt.SsoApplicationService; +import java.util.List; +import org.apache.commons.lang3.StringUtils; + +class SsoApplicationServiceImpl extends ManagementsBase implements SsoApplicationService { + SsoApplicationServiceImpl(Client client) { + super(client); + } + + @Override + public String createOIDCApplication(OIDCApplicationRequest appRequest) throws DescopeException { + if (appRequest == null) { + throw ServerCommonException.invalidArgument("appRequest"); + } + if (StringUtils.isBlank(appRequest.getName())) { + throw ServerCommonException.invalidArgument("appRequest.Name"); + } + ApiProxy apiProxy = getApiProxy(); + IDResponse id = apiProxy.post(getUri(SSO_APPLICATION_OIDC_CREATE_LINK), appRequest, IDResponse.class); + return id.getId(); + } + + @Override + public String createSAMLApplication(SAMLApplicationRequest appRequest) throws DescopeException { + if (appRequest == null) { + throw ServerCommonException.invalidArgument("appRequest"); + } + if (StringUtils.isBlank(appRequest.getName())) { + throw ServerCommonException.invalidArgument("appRequest.Name"); + } + ApiProxy apiProxy = getApiProxy(); + IDResponse id = apiProxy.post(getUri(SSO_APPLICATION_SAML_CREATE_LINK), appRequest, IDResponse.class); + return id.getId(); + } + + @Override + public void updateOIDCApplication(OIDCApplicationRequest appRequest) throws DescopeException { + if (appRequest == null) { + throw ServerCommonException.invalidArgument("appRequest"); + } + if (StringUtils.isBlank(appRequest.getId())) { + throw ServerCommonException.invalidArgument("appRequest.Id"); + } + if (StringUtils.isBlank(appRequest.getName())) { + throw ServerCommonException.invalidArgument("appRequest.Name"); + } + ApiProxy apiProxy = getApiProxy(); + apiProxy.post(getUri(SSO_APPLICATION_OIDC_UPDATE_LINK), appRequest, Void.class); + } + + @Override + public void updateSAMLApplication(SAMLApplicationRequest appRequest) throws DescopeException { + if (appRequest == null) { + throw ServerCommonException.invalidArgument("appRequest"); + } + if (StringUtils.isBlank(appRequest.getId())) { + throw ServerCommonException.invalidArgument("appRequest.Id"); + } + if (StringUtils.isBlank(appRequest.getName())) { + throw ServerCommonException.invalidArgument("appRequest.Name"); + } + ApiProxy apiProxy = getApiProxy(); + apiProxy.post(getUri(SSO_APPLICATION_SAML_UPDATE_LINK), appRequest, Void.class); + } + + @Override + public void delete(String id) throws DescopeException { + if (StringUtils.isBlank(id)) { + throw ServerCommonException.invalidArgument("id"); + } + ApiProxy apiProxy = getApiProxy(); + apiProxy.post(getUri(SSO_APPLICATION_DELETE_LINK), mapOf("id", id), Void.class); + } + + @Override + public SSOApplication load(String id) throws DescopeException { + if (StringUtils.isBlank(id)) { + throw ServerCommonException.invalidArgument("id"); + } + ApiProxy apiProxy = getApiProxy(); + return apiProxy.get(getQueryParamUri(SSO_APPLICATION_LOAD_LINK, mapOf("id", id)), SSOApplication.class); + } + + @Override + public List loadAll() throws DescopeException { + ApiProxy apiProxy = getApiProxy(); + SSOApplications apps = apiProxy.get(getUri(SSO_APPLICATION_LOAD_ALL_LINK), SSOApplications.class); + return apps.getApps(); + } +} diff --git a/src/main/java/com/descope/sdk/mgmt/impl/SsoServiceImpl.java b/src/main/java/com/descope/sdk/mgmt/impl/SsoServiceImpl.java index 2a093f90..67c09f66 100644 --- a/src/main/java/com/descope/sdk/mgmt/impl/SsoServiceImpl.java +++ b/src/main/java/com/descope/sdk/mgmt/impl/SsoServiceImpl.java @@ -2,9 +2,13 @@ import static com.descope.literals.Routes.ManagementEndPoints.SSO_CONFIGURE_MAPPING_LINK; import static com.descope.literals.Routes.ManagementEndPoints.SSO_CONFIGURE_METADATA_LINK; +import static com.descope.literals.Routes.ManagementEndPoints.SSO_CONFIGURE_OIDC_SETTINGS_LINK; +import static com.descope.literals.Routes.ManagementEndPoints.SSO_CONFIGURE_SAML_SETTINGS_BY_MD_LINK; +import static com.descope.literals.Routes.ManagementEndPoints.SSO_CONFIGURE_SAML_SETTINGS_LINK; import static com.descope.literals.Routes.ManagementEndPoints.SSO_CONFIGURE_SETTINGS_LINK; import static com.descope.literals.Routes.ManagementEndPoints.SSO_DELETE_SETTINGS_LINK; import static com.descope.literals.Routes.ManagementEndPoints.SSO_GET_SETTINGS_LINK; +import static com.descope.literals.Routes.ManagementEndPoints.SSO_GET_SETTINGS_V2_LINK; import static com.descope.utils.CollectionUtils.mapOf; import com.descope.exception.DescopeException; @@ -12,7 +16,11 @@ import com.descope.model.client.Client; import com.descope.model.sso.AttributeMapping; import com.descope.model.sso.RoleMapping; +import com.descope.model.sso.SSOOIDCSettings; +import com.descope.model.sso.SSOSAMLSettings; +import com.descope.model.sso.SSOSAMLSettingsByMetadata; import com.descope.model.sso.SSOSettingsResponse; +import com.descope.model.sso.SSOTenantSettingsResponse; import com.descope.proxy.ApiProxy; import com.descope.sdk.mgmt.SsoService; import java.util.List; @@ -25,23 +33,91 @@ class SsoServiceImpl extends ManagementsBase implements SsoService { } @Override - public SSOSettingsResponse getSettings(String tenantID) throws DescopeException { - if (StringUtils.isBlank(tenantID)) { + public SSOTenantSettingsResponse loadSettings(String tenantId) throws DescopeException { + if (StringUtils.isBlank(tenantId)) { throw ServerCommonException.invalidArgument("TenantId"); } - Map params = mapOf("tenantId", tenantID); + Map params = mapOf("tenantId", tenantId); ApiProxy apiProxy = getApiProxy(); - return apiProxy.get(getQueryParamUri(SSO_GET_SETTINGS_LINK, params), SSOSettingsResponse.class); + return apiProxy.get(getQueryParamUri(SSO_GET_SETTINGS_V2_LINK, params), SSOTenantSettingsResponse.class); } @Override - public void deleteSettings(String tenantID) throws DescopeException { + public void configureSAMLSettings(String tenantId, SSOSAMLSettings settings, String redirectUrl, List domains) + throws DescopeException { + if (StringUtils.isBlank(tenantId)) { + throw ServerCommonException.invalidArgument("TenantId"); + } + if (settings == null) { + throw ServerCommonException.invalidArgument("settings"); + } + if (StringUtils.isBlank(settings.getIdpUrl())) { + throw ServerCommonException.invalidArgument("idpUrl"); + } + if (StringUtils.isBlank(settings.getIdpCert())) { + throw ServerCommonException.invalidArgument("idpCert"); + } + if (StringUtils.isBlank(settings.getEntityId())) { + throw ServerCommonException.invalidArgument("entityId"); + } + Map req = mapOf("tenantId", tenantId, "redirectUrl", redirectUrl, "domains", domains, + "settings", settings); + ApiProxy apiProxy = getApiProxy(); + apiProxy.post(getUri(SSO_CONFIGURE_SAML_SETTINGS_LINK), req, Void.class); + } + + @Override + public void configureSAMLSettingsByMetadata(String tenantId, SSOSAMLSettingsByMetadata settings, String redirectUrl, + List domains) throws DescopeException { + if (StringUtils.isBlank(tenantId)) { + throw ServerCommonException.invalidArgument("TenantId"); + } + if (settings == null) { + throw ServerCommonException.invalidArgument("settings"); + } + if (StringUtils.isBlank(settings.getIdpMetadataUrl())) { + throw ServerCommonException.invalidArgument("idpMetadataURL"); + } + Map req = mapOf("tenantId", tenantId, "redirectUrl", redirectUrl, "domains", domains, + "settings", settings); + ApiProxy apiProxy = getApiProxy(); + apiProxy.post(getUri(SSO_CONFIGURE_SAML_SETTINGS_BY_MD_LINK), req, Void.class); + } + + @Override + public void configureOIDCSettings(String tenantId, SSOOIDCSettings settings, List domains) + throws DescopeException { + if (StringUtils.isBlank(tenantId)) { + throw ServerCommonException.invalidArgument("TenantId"); + } + if (settings == null) { + throw ServerCommonException.invalidArgument("settings"); + } + Map req = mapOf("tenantId", tenantId, "domains", domains, "settings", settings); + ApiProxy apiProxy = getApiProxy(); + apiProxy.post(getUri(SSO_CONFIGURE_OIDC_SETTINGS_LINK), req, Void.class); + } + + @Override + public void deleteSettings(String tenantId) throws DescopeException { + if (StringUtils.isBlank(tenantId)) { + throw ServerCommonException.invalidArgument("TenantId"); + } + Map request = mapOf("tenantId", tenantId); + ApiProxy apiProxy = getApiProxy(); + apiProxy.delete(getQueryParamUri(SSO_DELETE_SETTINGS_LINK, request), null, Void.class); + } + + // DEPRECATED METHODS + + @Override + public SSOSettingsResponse getSettings(String tenantID) throws DescopeException { if (StringUtils.isBlank(tenantID)) { throw ServerCommonException.invalidArgument("TenantId"); } - Map request = mapOf("tenantId", tenantID); + Map params = mapOf("tenantId", tenantID); ApiProxy apiProxy = getApiProxy(); - apiProxy.delete(getUri(SSO_DELETE_SETTINGS_LINK), request, Void.class); + return apiProxy.get(getQueryParamUri(SSO_GET_SETTINGS_LINK, params), SSOSettingsResponse.class); } @Override diff --git a/src/main/java/com/descope/sdk/mgmt/impl/UserServiceImpl.java b/src/main/java/com/descope/sdk/mgmt/impl/UserServiceImpl.java index 58df910b..1712f4de 100644 --- a/src/main/java/com/descope/sdk/mgmt/impl/UserServiceImpl.java +++ b/src/main/java/com/descope/sdk/mgmt/impl/UserServiceImpl.java @@ -16,6 +16,7 @@ import static com.descope.literals.Routes.ManagementEndPoints.UPDATE_USER_LOGIN_ID_LINK; import static com.descope.literals.Routes.ManagementEndPoints.UPDATE_USER_NAME_LINK; import static com.descope.literals.Routes.ManagementEndPoints.USER_ADD_ROLES_LINK; +import static com.descope.literals.Routes.ManagementEndPoints.USER_ADD_SSO_APPS_LINK; import static com.descope.literals.Routes.ManagementEndPoints.USER_ADD_TENANT_LINK; import static com.descope.literals.Routes.ManagementEndPoints.USER_CREATE_EMBEDDED_LINK; import static com.descope.literals.Routes.ManagementEndPoints.USER_EXPIRE_PASSWORD_LINK; @@ -25,6 +26,7 @@ import static com.descope.literals.Routes.ManagementEndPoints.USER_SEARCH_ALL_LINK; import static com.descope.literals.Routes.ManagementEndPoints.USER_SET_PASSWORD_LINK; import static com.descope.literals.Routes.ManagementEndPoints.USER_SET_ROLES_LINK; +import static com.descope.literals.Routes.ManagementEndPoints.USER_SET_SSO_APPS_LINK; import static com.descope.literals.Routes.ManagementEndPoints.USER_UPDATE_EMAIL_LINK; import static com.descope.literals.Routes.ManagementEndPoints.USER_UPDATE_PHONE_LINK; import static com.descope.literals.Routes.ManagementEndPoints.USER_UPDATE_STATUS_LINK; @@ -378,6 +380,36 @@ public UserResponseDetails removeRoles(String loginId, List roles) return apiProxy.post(removeRolesUri, request, UserResponseDetails.class); } + @Override + public UserResponseDetails addSsoApps(String loginId, List ssoAppIds) throws DescopeException { + if (StringUtils.isBlank(loginId)) { + throw ServerCommonException.invalidArgument("Login ID"); + } + Map request = mapOf("loginId", loginId, "ssoAppIds", ssoAppIds); + ApiProxy apiProxy = getApiProxy(); + return apiProxy.post(getUri(USER_ADD_SSO_APPS_LINK), request, UserResponseDetails.class); + } + + @Override + public UserResponseDetails setSsoApps(String loginId, List ssoAppIds) throws DescopeException { + if (StringUtils.isBlank(loginId)) { + throw ServerCommonException.invalidArgument("Login ID"); + } + Map request = mapOf("loginId", loginId, "ssoAppIds", ssoAppIds); + ApiProxy apiProxy = getApiProxy(); + return apiProxy.post(getUri(USER_SET_SSO_APPS_LINK), request, UserResponseDetails.class); + } + + @Override + public UserResponseDetails removeSsoApps(String loginId, List ssoAppIds) throws DescopeException { + if (StringUtils.isBlank(loginId)) { + throw ServerCommonException.invalidArgument("Login ID"); + } + Map request = mapOf("loginId", loginId, "ssoAppIds", ssoAppIds); + ApiProxy apiProxy = getApiProxy(); + return apiProxy.post(getUri(USER_SET_SSO_APPS_LINK), request, UserResponseDetails.class); + } + @Override public UserResponseDetails addTenant(String loginId, String tenantId) throws DescopeException { if (StringUtils.isBlank(loginId)) { diff --git a/src/test/java/com/descope/sdk/TestUtils.java b/src/test/java/com/descope/sdk/TestUtils.java index b2f3bc4f..1f16a61d 100644 --- a/src/test/java/com/descope/sdk/TestUtils.java +++ b/src/test/java/com/descope/sdk/TestUtils.java @@ -52,7 +52,8 @@ public class TestUtils { Collections.emptyMap(), false, false, - Collections.emptyMap()); + Collections.emptyMap(), + Collections.emptyList()); public static final JWTResponse MOCK_JWT_RESPONSE = new JWTResponse( "someSessionJwt", "someRefreshJwt", diff --git a/src/test/java/com/descope/sdk/mgmt/impl/SsoApplicationServiceImplTest.java b/src/test/java/com/descope/sdk/mgmt/impl/SsoApplicationServiceImplTest.java new file mode 100644 index 00000000..e33b676c --- /dev/null +++ b/src/test/java/com/descope/sdk/mgmt/impl/SsoApplicationServiceImplTest.java @@ -0,0 +1,94 @@ +package com.descope.sdk.mgmt.impl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.descope.exception.RateLimitExceededException; +import com.descope.model.client.Client; +import com.descope.model.ssoapp.OIDCApplicationRequest; +import com.descope.model.ssoapp.SAMLApplicationRequest; +import com.descope.model.ssoapp.SSOApplication; +import com.descope.sdk.TestUtils; +import com.descope.sdk.mgmt.SsoApplicationService; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junitpioneer.jupiter.RetryingTest; + +public class SsoApplicationServiceImplTest { + + private SsoApplicationService ssoApplicationService; + + @BeforeEach + void setUp() { + Client client = TestUtils.getClient(); + this.ssoApplicationService = ManagementServiceBuilder.buildServices(client).getSsoApplicationService(); + } + + @RetryingTest(value = 3, suspendForMs = 30000, onExceptions = RateLimitExceededException.class) + void testFunctionalFullCycleOIDC() { + String name = TestUtils.getRandomName("a-"); + String id = ssoApplicationService.createOIDCApplication(OIDCApplicationRequest.builder() + .name(name) + .description("test") + .enabled(true) + .build()); + assertThat(id).isNotBlank(); + SSOApplication app = ssoApplicationService.load(id); + assertEquals(name, app.getName()); + assertEquals("test", app.getDescription()); + assertEquals("oidc", app.getAppType()); + ssoApplicationService.updateOIDCApplication(OIDCApplicationRequest.builder() + .name(name + "1") + .description("test1") + .enabled(false) + .id(id) + .build()); + List apps = ssoApplicationService.loadAll(); + boolean found = false; + for (SSOApplication a : apps) { + if (a.getId().equals(id)) { + found = true; + assertEquals("test1", a.getDescription()); + assertEquals(name + "1", a.getName()); + assertFalse(a.getEnabled()); + } + } + assertTrue(found); + ssoApplicationService.delete(id); + } + + @RetryingTest(value = 3, suspendForMs = 30000, onExceptions = RateLimitExceededException.class) + void testFunctionalFullCycleSAML() { + String name = TestUtils.getRandomName("a-"); + String id = ssoApplicationService.createSAMLApplication(SAMLApplicationRequest.builder() + .name(name) + .description("test") + .enabled(true) + .build()); + assertThat(id).isNotBlank(); + SSOApplication app = ssoApplicationService.load(id); + assertEquals(name, app.getName()); + assertEquals("test", app.getDescription()); + assertEquals("saml", app.getAppType()); + ssoApplicationService.updateSAMLApplication(SAMLApplicationRequest.builder() + .name(name + "1") + .description("test1") + .enabled(false) + .id(id) + .build()); + List apps = ssoApplicationService.loadAll(); + boolean found = false; + for (SSOApplication a : apps) { + if (a.getId().equals(id)) { + found = true; + assertEquals("test1", a.getDescription()); + assertEquals(name + "1", a.getName()); + assertFalse(a.getEnabled()); + } + } + assertTrue(found); + ssoApplicationService.delete(id); + } +} diff --git a/src/test/java/com/descope/sdk/mgmt/impl/SsoServiceImplTest.java b/src/test/java/com/descope/sdk/mgmt/impl/SsoServiceImplTest.java index 006d14ec..39489510 100644 --- a/src/test/java/com/descope/sdk/mgmt/impl/SsoServiceImplTest.java +++ b/src/test/java/com/descope/sdk/mgmt/impl/SsoServiceImplTest.java @@ -1,5 +1,6 @@ package com.descope.sdk.mgmt.impl; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -10,30 +11,43 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import com.descope.exception.RateLimitExceededException; import com.descope.exception.ServerCommonException; import com.descope.model.client.Client; +import com.descope.model.mgmt.ManagementServices; import com.descope.model.sso.AttributeMapping; +import com.descope.model.sso.OIDCAttributeMapping; import com.descope.model.sso.RoleMapping; +import com.descope.model.sso.SSOOIDCSettings; +import com.descope.model.sso.SSOSAMLSettings; import com.descope.model.sso.SSOSettingsResponse; +import com.descope.model.sso.SSOTenantSettingsResponse; import com.descope.proxy.ApiProxy; import com.descope.proxy.impl.ApiProxyBuilder; import com.descope.sdk.TestUtils; import com.descope.sdk.mgmt.SsoService; +import com.descope.sdk.mgmt.TenantService; import java.util.Arrays; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.RetryingTest; import org.mockito.MockedStatic; import org.mockito.Mockito; +@SuppressWarnings("deprecation") class SsoServiceImplTest { private SsoService ssoService; + private TenantService tenantService; + @BeforeEach void setUp() { Client client = TestUtils.getClient(); - this.ssoService = ManagementServiceBuilder.buildServices(client).getSsoService(); + ManagementServices mgmt = ManagementServiceBuilder.buildServices(client); + this.ssoService = mgmt.getSsoService(); + this.tenantService = mgmt.getTenantService(); } @Test @@ -205,4 +219,72 @@ void testConfigureMappingForSuccess() { verify(apiProxy, times(1)).post(any(), any(), any()); } } + + @RetryingTest(value = 3, suspendForMs = 30000, onExceptions = RateLimitExceededException.class) + void testFunctionalFullCycleOIDC() { + String name = TestUtils.getRandomName("t-"); + String tenantId = tenantService.create(name, Arrays.asList(name + ".com", name + "1.com")); + assertThat(tenantId).isNotBlank(); + ssoService.configureOIDCSettings(tenantId, SSOOIDCSettings.builder() + .jwksUrl("https://" + name + ".com/jwks") + .authUrl("https://" + name + ".com/auth") + .callbackDomain(name + ".com") + .clientId("xxx") + .clientSecret("yyy") + .grantType("implicit") + .issuer("i") + .name(name) + .redirectUrl("https://" + name + ".com/r") + .tokenUrl("https://" + name + ".com/t") + .userDataUrl("https://" + name + ".com/ud") + .userAttrMapping(OIDCAttributeMapping.builder() + .loginId("loginId") + .email("email") + .username("username") + .name("name") + .build()) + .build(), null); + SSOTenantSettingsResponse resp = ssoService.loadSettings(tenantId); + assertEquals(tenantId, resp.getTenant().getId()); + assertThat(Arrays.asList(name + ".com", name + "1.com")).containsExactly(name + ".com", name + "1.com"); + assertEquals(name, resp.getTenant().getName()); + assertEquals("https://" + name + ".com/jwks", resp.getOidc().getJwksUrl()); + assertEquals("https://" + name + ".com/auth", resp.getOidc().getAuthUrl()); + assertEquals(name + ".com", resp.getOidc().getCallbackDomain()); + assertEquals("xxx", resp.getOidc().getClientId()); + assertEquals("implicit", resp.getOidc().getGrantType()); + assertEquals("i", resp.getOidc().getIssuer()); + assertEquals(name, resp.getOidc().getName()); + assertEquals("https://" + name + ".com/r", resp.getOidc().getRedirectUrl()); + assertEquals("https://" + name + ".com/t", resp.getOidc().getTokenUrl()); + assertEquals("https://" + name + ".com/ud", resp.getOidc().getUserDataUrl()); + ssoService.deleteSettings(tenantId); + tenantService.delete(tenantId); + } + + @RetryingTest(value = 3, suspendForMs = 30000, onExceptions = RateLimitExceededException.class) + void testFunctionalFullCycleSAML() { + String name = TestUtils.getRandomName("t-"); + String tenantId = tenantService.create(name, Arrays.asList(name + ".com", name + "1.com")); + assertThat(tenantId).isNotBlank(); + ssoService.configureSAMLSettings(tenantId, SSOSAMLSettings.builder() + .attributeMapping(AttributeMapping.builder() + .email("email") + .name("name") + .build()) + .entityId("entityId") + .idpCert("idpCert") + .idpUrl("https://" + name + ".com") + .build(), "https://" + name + ".com", null); + SSOTenantSettingsResponse resp = ssoService.loadSettings(tenantId); + assertEquals(tenantId, resp.getTenant().getId()); + assertThat(Arrays.asList(name + ".com", name + "1.com")).containsExactly(name + ".com", name + "1.com"); + assertEquals(name, resp.getTenant().getName()); + assertEquals("entityId", resp.getSaml().getIdpEntityId()); + assertEquals("idpCert", resp.getSaml().getIdpCertificate()); + assertEquals("https://" + name + ".com", resp.getSaml().getIdpSSOUrl()); + assertEquals("https://" + name + ".com", resp.getSaml().getRedirectUrl()); + ssoService.deleteSettings(tenantId); + tenantService.delete(tenantId); + } } From 632fe4c73ec725e98a4c1598461cd8cd628fd24a Mon Sep 17 00:00:00 2001 From: Slavik Markovich Date: Wed, 14 Feb 2024 15:20:16 -0800 Subject: [PATCH 2/3] fix typo --- .../java/com/descope/sdk/auth/impl/WebAuthnServiceImplTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/descope/sdk/auth/impl/WebAuthnServiceImplTest.java b/src/test/java/com/descope/sdk/auth/impl/WebAuthnServiceImplTest.java index 8886262e..e25b03a0 100644 --- a/src/test/java/com/descope/sdk/auth/impl/WebAuthnServiceImplTest.java +++ b/src/test/java/com/descope/sdk/auth/impl/WebAuthnServiceImplTest.java @@ -168,7 +168,7 @@ void testSignInStartEmptyOrigin() { void testSignInStartEmptyToken() { ServerCommonException thrown = assertThrows(ServerCommonException.class, - () -> webAuthnService.signInStart("x", "x", null, new LoginOptions(true, false, null))); + () -> webAuthnService.signInStart("x", "x", null, new LoginOptions(true, false, null, null))); assertNotNull(thrown); assertEquals("The Token argument is invalid", thrown.getMessage()); } From 1c26d217a9ee9c65b2631ee9be126b4218ca439c Mon Sep 17 00:00:00 2001 From: Slavik Markovich Date: Thu, 15 Feb 2024 10:17:41 -0800 Subject: [PATCH 3/3] fix comments --- .../model/sso/OIDCAttributeMapping.java | 2 +- .../descope/sdk/auth/SSOServiceProvider.java | 2 +- .../sdk/mgmt/impl/SsoServiceImplTest.java | 34 +++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/descope/model/sso/OIDCAttributeMapping.java b/src/main/java/com/descope/model/sso/OIDCAttributeMapping.java index 41899e62..ae992ad2 100644 --- a/src/main/java/com/descope/model/sso/OIDCAttributeMapping.java +++ b/src/main/java/com/descope/model/sso/OIDCAttributeMapping.java @@ -6,7 +6,7 @@ import lombok.NoArgsConstructor; /** - * Represents a SAML mapping between Descope and IDP user attributes. + * Represents a OIDC mapping between Descope and IDP user attributes. */ @Data @Builder diff --git a/src/main/java/com/descope/sdk/auth/SSOServiceProvider.java b/src/main/java/com/descope/sdk/auth/SSOServiceProvider.java index f65013af..8dab7f3f 100644 --- a/src/main/java/com/descope/sdk/auth/SSOServiceProvider.java +++ b/src/main/java/com/descope/sdk/auth/SSOServiceProvider.java @@ -32,7 +32,7 @@ String start(String tenant, String redirectUrl, String prompt, LoginOptions logi throws DescopeException; /** - * ExchangeToken - Finalize SAML authentication. + * ExchangeToken - Finalize SAML/OIDC SSO authentication. * * @param code - Code to be validated * @return Authentication info diff --git a/src/test/java/com/descope/sdk/mgmt/impl/SsoServiceImplTest.java b/src/test/java/com/descope/sdk/mgmt/impl/SsoServiceImplTest.java index 39489510..3fd8ec56 100644 --- a/src/test/java/com/descope/sdk/mgmt/impl/SsoServiceImplTest.java +++ b/src/test/java/com/descope/sdk/mgmt/impl/SsoServiceImplTest.java @@ -16,18 +16,22 @@ import com.descope.model.client.Client; import com.descope.model.mgmt.ManagementServices; import com.descope.model.sso.AttributeMapping; +import com.descope.model.sso.GroupsMapping; import com.descope.model.sso.OIDCAttributeMapping; import com.descope.model.sso.RoleMapping; import com.descope.model.sso.SSOOIDCSettings; import com.descope.model.sso.SSOSAMLSettings; +import com.descope.model.sso.SSOSAMLSettingsByMetadata; import com.descope.model.sso.SSOSettingsResponse; import com.descope.model.sso.SSOTenantSettingsResponse; import com.descope.proxy.ApiProxy; import com.descope.proxy.impl.ApiProxyBuilder; import com.descope.sdk.TestUtils; +import com.descope.sdk.mgmt.RolesService; import com.descope.sdk.mgmt.SsoService; import com.descope.sdk.mgmt.TenantService; import java.util.Arrays; +import java.util.List; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -40,6 +44,7 @@ class SsoServiceImplTest { private SsoService ssoService; private TenantService tenantService; + private RolesService rolesService; @BeforeEach @@ -48,6 +53,7 @@ void setUp() { ManagementServices mgmt = ManagementServiceBuilder.buildServices(client); this.ssoService = mgmt.getSsoService(); this.tenantService = mgmt.getTenantService(); + this.rolesService = mgmt.getRolesService(); } @Test @@ -287,4 +293,32 @@ void testFunctionalFullCycleSAML() { ssoService.deleteSettings(tenantId); tenantService.delete(tenantId); } + + @RetryingTest(value = 3, suspendForMs = 30000, onExceptions = RateLimitExceededException.class) + void testFunctionalFullCycleSAMLMetadata() { + String name = TestUtils.getRandomName("t-"); + String tenantId = tenantService.create(name, Arrays.asList(name + ".com", name + "1.com")); + assertThat(tenantId).isNotBlank(); + String roleName = TestUtils.getRandomName("rt-").substring(0, 20); + rolesService.create(roleName, tenantId, "ttt", null); + ssoService.configureSAMLSettingsByMetadata(tenantId, SSOSAMLSettingsByMetadata.builder() + .attributeMapping(AttributeMapping.builder() + .email("email") + .name("name") + .build()) + .idpMetadataUrl("https://" + name + ".com/md") + .roleMappings(Arrays.asList(RoleMapping.builder().groups(Arrays.asList("a", "b")).roleName(roleName).build())) + .build(), "https://" + name + ".com", null); + SSOTenantSettingsResponse resp = ssoService.loadSettings(tenantId); + assertEquals(tenantId, resp.getTenant().getId()); + assertThat(Arrays.asList(name + ".com", name + "1.com")).containsExactly(name + ".com", name + "1.com"); + assertEquals(name, resp.getTenant().getName()); + assertEquals("https://" + name + ".com/md", resp.getSaml().getIdpMetadataUrl()); + List groupsMapping = resp.getSaml().getGroupsMapping(); + assertNotNull(groupsMapping); + assertThat(groupsMapping).hasSize(1); + assertThat(groupsMapping.get(0).getRole().getId()).isNotBlank(); + ssoService.deleteSettings(tenantId); + tenantService.delete(tenantId); + } }