From cd0633106ac147bb055a573617b7d8995e810c7b Mon Sep 17 00:00:00 2001 From: Slavik Markovich Date: Fri, 3 Apr 2026 13:23:14 -0700 Subject: [PATCH 1/4] Add timeout to generate embedded link and allow create link for signup --- pom.xml | 2 +- .../java/com/descope/literals/Routes.java | 1 + .../descope/model/magiclink/LoginOptions.java | 14 ++++ .../request/GenerateEmbeddedLinkRequest.java | 1 + .../com/descope/sdk/mgmt/UserService.java | 25 ++++++- .../sdk/mgmt/impl/UserServiceImpl.java | 30 +++++++- .../sdk/mgmt/impl/UserServiceImplTest.java | 75 +++++++++++++++++-- 7 files changed, 139 insertions(+), 9 deletions(-) diff --git a/pom.xml b/pom.xml index 6ceb8d9c..1b0e2f12 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.descope java-sdk 4.0.0 - 1.0.63 + 1.0.64 ${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/literals/Routes.java b/src/main/java/com/descope/literals/Routes.java index 10784061..b7514b42 100644 --- a/src/main/java/com/descope/literals/Routes.java +++ b/src/main/java/com/descope/literals/Routes.java @@ -119,6 +119,7 @@ public static class ManagementEndPoints { public static final String USER_SET_TEMPORARY_PASSWORD_LINK = "/v1/mgmt/user/password/set/temporary"; public static final String USER_EXPIRE_PASSWORD_LINK = "/v1/mgmt/user/password/expire"; public static final String USER_CREATE_EMBEDDED_LINK = "/v1/mgmt/user/signin/embeddedlink"; + public static final String USER_SIGNUP_EMBEDDED_LINK = "/v1/mgmt/user/signup/embeddedlink"; public static final String USER_HISTORY_LINK = "/v1/mgmt/user/history"; // Tenant diff --git a/src/main/java/com/descope/model/magiclink/LoginOptions.java b/src/main/java/com/descope/model/magiclink/LoginOptions.java index 4236a4c8..de3b9ac2 100644 --- a/src/main/java/com/descope/model/magiclink/LoginOptions.java +++ b/src/main/java/com/descope/model/magiclink/LoginOptions.java @@ -18,6 +18,8 @@ public class LoginOptions { private boolean revokeOtherSessions; private String[] revokeOtherSessionsTypes; private String jwt; + private String locale; + private String templateId; public LoginOptions(boolean stepup, boolean mfa, Map customClaims, Map templateOptions) { @@ -27,6 +29,18 @@ public LoginOptions(boolean stepup, boolean mfa, Map customClaim this.templateOptions = templateOptions; } + public LoginOptions(boolean stepup, boolean mfa, Map customClaims, + Map templateOptions, boolean revokeOtherSessions, + String[] revokeOtherSessionsTypes, String jwt) { + this.stepup = stepup; + this.mfa = mfa; + this.customClaims = customClaims; + this.templateOptions = templateOptions; + this.revokeOtherSessions = revokeOtherSessions; + this.revokeOtherSessionsTypes = revokeOtherSessionsTypes; + this.jwt = jwt; + } + public boolean isJWTRequired() { return this.isStepup() || this.isMfa(); } diff --git a/src/main/java/com/descope/model/user/request/GenerateEmbeddedLinkRequest.java b/src/main/java/com/descope/model/user/request/GenerateEmbeddedLinkRequest.java index db838402..4c9551b8 100644 --- a/src/main/java/com/descope/model/user/request/GenerateEmbeddedLinkRequest.java +++ b/src/main/java/com/descope/model/user/request/GenerateEmbeddedLinkRequest.java @@ -13,4 +13,5 @@ public class GenerateEmbeddedLinkRequest { private String loginId; private Map customClaims; + private int timeout; } diff --git a/src/main/java/com/descope/sdk/mgmt/UserService.java b/src/main/java/com/descope/sdk/mgmt/UserService.java index 566954c3..b722e596 100644 --- a/src/main/java/com/descope/sdk/mgmt/UserService.java +++ b/src/main/java/com/descope/sdk/mgmt/UserService.java @@ -3,6 +3,8 @@ import com.descope.enums.DeliveryMethod; import com.descope.exception.DescopeException; import com.descope.model.auth.InviteOptions; +import com.descope.model.magiclink.LoginOptions; +import com.descope.model.user.User; import com.descope.model.user.request.BatchUserRequest; import com.descope.model.user.request.PatchUserRequest; import com.descope.model.user.request.UserRequest; @@ -550,12 +552,32 @@ EnchantedLinkTestUserResponse generateEnchantedLinkForTestUser(String loginId, S * * @param loginId loginId The loginID is required. * @param customClaims additional claims to be added to the verified token JWT + * @param timeout The timeout in seconds for the embedded link token * @return It returns the token that can then be verified using the magic link 'verify' function, * either directly or through a flow. * @throws DescopeException If there occurs any exception, a subtype of this exception will be * thrown. */ - String generateEmbeddedLink(String loginId, Map customClaims) + String generateEmbeddedLink(String loginId, Map customClaims, int timeout) + throws DescopeException; + + /** + * Generate a sign-up embedded link token, later can be used to authenticate via magiclink + * verify method or via flow verify step. + * + * @param loginId loginId The loginID is required. + * @param user The user object containing user details. + * @param emailVerified Boolean indicating if the email is verified. + * @param phoneVerified Boolean indicating if the phone is verified. + * @param loginOptions Options for the login process. + * @param timeout The timeout in seconds for the embedded link token + * @return It returns the token that can then be verified using the magic link 'verify' function, + * either directly or through a flow. + * @throws DescopeException If there occurs any exception, a subtype of this exception will be + * thrown. + */ + String generateSignUpEmbeddedLink(String loginId, User user, Boolean emailVerified, + Boolean phoneVerified, LoginOptions loginOptions, int timeout) throws DescopeException; /** @@ -564,6 +586,7 @@ String generateEmbeddedLink(String loginId, Map customClaims) * @param userIds List of user IDs to retrieve the history for * @return {{@link List} of {@link UserHistoryResponse}} of all requested users login history * @throws DescopeException If there occurs any exception, a subtype of this exception will be + * thrown. */ List history(List userIds) throws DescopeException; } 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 f5651b80..d374cb2d 100644 --- a/src/main/java/com/descope/sdk/mgmt/impl/UserServiceImpl.java +++ b/src/main/java/com/descope/sdk/mgmt/impl/UserServiceImpl.java @@ -31,6 +31,7 @@ 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_SET_TEMPORARY_PASSWORD_LINK; +import static com.descope.literals.Routes.ManagementEndPoints.USER_SIGNUP_EMBEDDED_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; @@ -43,9 +44,12 @@ import com.descope.exception.ServerCommonException; import com.descope.model.auth.InviteOptions; import com.descope.model.client.Client; +import com.descope.model.magiclink.LoginOptions; +import com.descope.model.user.User; import com.descope.model.user.request.BatchUserRequest; import com.descope.model.user.request.EnchantedLinkTestUserRequest; import com.descope.model.user.request.GenerateEmbeddedLinkRequest; +import com.descope.model.user.request.GenerateSignUpEmbeddedLinkRequest; import com.descope.model.user.request.MagicLinkTestUserRequest; import com.descope.model.user.request.OTPTestUserRequest; import com.descope.model.user.request.PatchUserRequest; @@ -609,18 +613,36 @@ public List history(List userIds) throws DescopeExc }); } - public String generateEmbeddedLink(String loginId, Map customClaims) throws DescopeException { + @Override + public String generateEmbeddedLink(String loginId, Map customClaims, int timeout) + throws DescopeException { if (StringUtils.isBlank(loginId)) { throw ServerCommonException.invalidArgument("Login ID"); } URI generateEmbeddedLinkUri = composeGenerateEmbeddedLink(); - GenerateEmbeddedLinkRequest request = new GenerateEmbeddedLinkRequest(loginId, customClaims); + GenerateEmbeddedLinkRequest request = new GenerateEmbeddedLinkRequest(loginId, customClaims, timeout); ApiProxy apiProxy = getApiProxy(); GenerateEmbeddedLinkResponse response = apiProxy.post(generateEmbeddedLinkUri, request, GenerateEmbeddedLinkResponse.class); return response.getToken(); } + @Override + public String generateSignUpEmbeddedLink(String loginId, User user, Boolean emailVerified, + Boolean phoneVerified, LoginOptions loginOptions, int timeout) + throws DescopeException { + if (StringUtils.isBlank(loginId)) { + throw ServerCommonException.invalidArgument("Login ID"); + } + URI generateSignUpEmbeddedLinkUri = composeGenerateSignUpEmbeddedLink(); + GenerateSignUpEmbeddedLinkRequest request = new GenerateSignUpEmbeddedLinkRequest( + loginId, user, emailVerified, phoneVerified, loginOptions, timeout); + ApiProxy apiProxy = getApiProxy(); + GenerateEmbeddedLinkResponse response = apiProxy.post(generateSignUpEmbeddedLinkUri, request, + GenerateEmbeddedLinkResponse.class); + return response.getToken(); + } + private URI composeCreateUserUri() { return getUri(CREATE_USER_LINK); } @@ -753,4 +775,8 @@ private URI composeGenerateEmbeddedLink() { return getUri(USER_CREATE_EMBEDDED_LINK); } + private URI composeGenerateSignUpEmbeddedLink() { + return getUri(USER_SIGNUP_EMBEDDED_LINK); + } + } diff --git a/src/test/java/com/descope/sdk/mgmt/impl/UserServiceImplTest.java b/src/test/java/com/descope/sdk/mgmt/impl/UserServiceImplTest.java index 0debe85d..616d0d29 100644 --- a/src/test/java/com/descope/sdk/mgmt/impl/UserServiceImplTest.java +++ b/src/test/java/com/descope/sdk/mgmt/impl/UserServiceImplTest.java @@ -26,7 +26,9 @@ import com.descope.model.auth.AuthenticationServices; import com.descope.model.auth.InviteOptions; import com.descope.model.client.Client; +import com.descope.model.magiclink.LoginOptions; import com.descope.model.mgmt.ManagementServices; +import com.descope.model.user.User; import com.descope.model.user.request.BatchUserPasswordHashed; import com.descope.model.user.request.BatchUserPasswordPbkdf2; import com.descope.model.user.request.BatchUserPasswordSha; @@ -797,7 +799,7 @@ void testGenerateEnchantedLinkForTestUserForSuccess() { @Test void testGenerateEmbeddedLinkForEmptyLoginId() { ServerCommonException thrown = assertThrows(ServerCommonException.class, - () -> userService.generateEmbeddedLink("", null)); + () -> userService.generateEmbeddedLink("", null, 0)); assertNotNull(thrown); assertEquals("The Login ID argument is invalid", thrown.getMessage()); } @@ -809,7 +811,7 @@ void testGenerateEmbeddedLinkForSuccess() { doReturn(mockResponse).when(apiProxy).post(any(), any(), any()); try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { mockedApiProxyBuilder.when(() -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); - String response = userService.generateEmbeddedLink("someLoginId", null); + String response = userService.generateEmbeddedLink("someLoginId", null, 0); Assertions.assertThat(response).isEqualTo("someToken"); } } @@ -1068,11 +1070,11 @@ void testFunctionalGenerateEmbeddedLink() { UserResponse user = createResponse.getUser(); assertNotNull(user); Assertions.assertThat(user.getLoginIds()).contains(loginId); - String token = userService.generateEmbeddedLink(loginId, null); + String token = userService.generateEmbeddedLink(loginId, null, 0); AuthenticationInfo authInfo = magicLinkService.verify(token); assertNotNull(authInfo.getToken()); assertThat(authInfo.getToken().getJwt()).isNotBlank(); - token = userService.generateEmbeddedLink(loginId, mapOf("kuku", "kiki")); + token = userService.generateEmbeddedLink(loginId, mapOf("kuku", "kiki"), 0); final long now = System.currentTimeMillis(); authInfo = magicLinkService.verify(token); assertNotNull(authInfo.getToken()); @@ -1121,7 +1123,7 @@ void testFunctionalGenerateEmbeddedLinkWithPhoneAsID() { UserResponse user = createResponse.getUser(); assertNotNull(user); Assertions.assertThat(user.getLoginIds()).contains(cleanPhone); - String token = userService.generateEmbeddedLink(phone, null); + String token = userService.generateEmbeddedLink(phone, null, 0); AuthenticationInfo authInfo = magicLinkService.verify(token); assertNotNull(authInfo.getToken()); assertThat(authInfo.getToken().getJwt()).isNotBlank(); @@ -1131,6 +1133,69 @@ void testFunctionalGenerateEmbeddedLinkWithPhoneAsID() { userService.delete(phone); } + @Test + void testGenerateSignUpEmbeddedLinkForEmptyLoginId() { + ServerCommonException thrown = assertThrows(ServerCommonException.class, + () -> userService.generateSignUpEmbeddedLink("", null, false, false, null, 0)); + assertNotNull(thrown); + assertEquals("The Login ID argument is invalid", thrown.getMessage()); + } + + @Test + void testGenerateSignUpEmbeddedLinkForSuccess() { + GenerateEmbeddedLinkResponse mockResponse = new GenerateEmbeddedLinkResponse("someSignUpToken"); + ApiProxy apiProxy = mock(ApiProxy.class); + doReturn(mockResponse).when(apiProxy).post(any(), any(), any()); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when(() -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + User user = User.builder().name("Test User").email("test@example.com").build(); + LoginOptions loginOptions = LoginOptions.builder().stepup(false).mfa(false).build(); + String response = userService.generateSignUpEmbeddedLink("someLoginId", user, true, false, loginOptions, 300); + Assertions.assertThat(response).isEqualTo("someSignUpToken"); + } + } + + @RetryingTest(value = 3, suspendForMs = 30000, onExceptions = RateLimitExceededException.class) + void testFunctionalGenerateSignUpEmbeddedLink() { + String loginId = TestUtils.getRandomName("signup-"); + String email = TestUtils.getRandomName("test-") + "@descope.com"; + String phone = "+1-555-555-5555"; + + User user = User.builder() + .name("Test Signup User") + .email(email) + .phone(phone) + .build(); + + LoginOptions loginOptions = LoginOptions.builder() + .stepup(false) + .mfa(false) + .customClaims(mapOf("signup", true)) + .build(); + + // Test generateSignUpEmbeddedLink with basic parameters + String token = userService.generateSignUpEmbeddedLink(loginId, user, true, true, loginOptions, 300); + assertNotNull(token); + assertThat(token).isNotBlank(); + + // Verify the token by using magic link verify + AuthenticationInfo authInfo = magicLinkService.verify(token); + assertNotNull(authInfo.getToken()); + assertThat(authInfo.getToken().getJwt()).isNotBlank(); + + // Verify custom claims are present + Map claims = authInfo.getToken().getClaims(); + assertThat(claims).containsKey("signup"); + assertEquals(true, claims.get("signup")); + + // Clean up - delete the created user + try { + userService.delete(loginId); + } catch (DescopeException e) { + // User might not exist if sign-up didn't complete, ignore + } + } + @RetryingTest(value = 3, suspendForMs = 30000, onExceptions = RateLimitExceededException.class) void testFunctionalBatch() throws Exception { String loginId = TestUtils.getRandomName("u-"); From 51f418fe3eddaa40f46711c65b21ea9dff71c400 Mon Sep 17 00:00:00 2001 From: Slavik Markovich Date: Fri, 3 Apr 2026 13:29:09 -0700 Subject: [PATCH 2/4] Now with the missing file --- .../GenerateSignUpEmbeddedLinkRequest.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/main/java/com/descope/model/user/request/GenerateSignUpEmbeddedLinkRequest.java diff --git a/src/main/java/com/descope/model/user/request/GenerateSignUpEmbeddedLinkRequest.java b/src/main/java/com/descope/model/user/request/GenerateSignUpEmbeddedLinkRequest.java new file mode 100644 index 00000000..681bd5e4 --- /dev/null +++ b/src/main/java/com/descope/model/user/request/GenerateSignUpEmbeddedLinkRequest.java @@ -0,0 +1,21 @@ +package com.descope.model.user.request; + +import com.descope.model.magiclink.LoginOptions; +import com.descope.model.user.User; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GenerateSignUpEmbeddedLinkRequest { + private String loginId; + private User user; + private Boolean emailVerified; + private Boolean phoneVerified; + private LoginOptions loginOptions; + private int timeout; +} From 5b2f942f4fe908745d20442da19a25edb3d22ac1 Mon Sep 17 00:00:00 2001 From: Slavik Markovich Date: Fri, 3 Apr 2026 13:33:08 -0700 Subject: [PATCH 3/4] Added backward compatibility --- .../java/com/descope/sdk/mgmt/UserService.java | 14 ++++++++++++++ .../com/descope/sdk/mgmt/impl/UserServiceImpl.java | 6 ++++++ 2 files changed, 20 insertions(+) diff --git a/src/main/java/com/descope/sdk/mgmt/UserService.java b/src/main/java/com/descope/sdk/mgmt/UserService.java index b722e596..e38246a7 100644 --- a/src/main/java/com/descope/sdk/mgmt/UserService.java +++ b/src/main/java/com/descope/sdk/mgmt/UserService.java @@ -546,6 +546,20 @@ MagicLinkTestUserResponse generateMagicLinkForTestUser( EnchantedLinkTestUserResponse generateEnchantedLinkForTestUser(String loginId, String uri) throws DescopeException; + /** + * Generate an embedded link token, later can be used to authenticate via magiclink verify method + * or via flow verify step. + * + * @param loginId loginId The loginID is required. + * @param customClaims additional claims to be added to the verified token JWT + * @return It returns the token that can then be verified using the magic link 'verify' function, + * either directly or through a flow. + * @throws DescopeException If there occurs any exception, a subtype of this exception will be + * thrown. + */ + String generateEmbeddedLink(String loginId, Map customClaims) + throws DescopeException; + /** * Generate an embedded link token, later can be used to authenticate via magiclink verify method * or via flow verify step. 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 d374cb2d..e58e2619 100644 --- a/src/main/java/com/descope/sdk/mgmt/impl/UserServiceImpl.java +++ b/src/main/java/com/descope/sdk/mgmt/impl/UserServiceImpl.java @@ -613,6 +613,12 @@ public List history(List userIds) throws DescopeExc }); } + @Override + public String generateEmbeddedLink(String loginId, Map customClaims) + throws DescopeException { + return generateEmbeddedLink(loginId, customClaims, 0); + } + @Override public String generateEmbeddedLink(String loginId, Map customClaims, int timeout) throws DescopeException { From e333a7510ccbeaf8e57f0e8c91a183b25404ba7f Mon Sep 17 00:00:00 2001 From: Slavik Markovich Date: Fri, 3 Apr 2026 13:54:27 -0700 Subject: [PATCH 4/4] Fix javadoc and verify timeout is not negative --- src/main/java/com/descope/sdk/mgmt/UserService.java | 10 ++++++---- .../com/descope/sdk/mgmt/impl/UserServiceImpl.java | 6 ++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/descope/sdk/mgmt/UserService.java b/src/main/java/com/descope/sdk/mgmt/UserService.java index e38246a7..e2b02be3 100644 --- a/src/main/java/com/descope/sdk/mgmt/UserService.java +++ b/src/main/java/com/descope/sdk/mgmt/UserService.java @@ -564,9 +564,10 @@ String generateEmbeddedLink(String loginId, Map customClaims) * Generate an embedded link token, later can be used to authenticate via magiclink verify method * or via flow verify step. * - * @param loginId loginId The loginID is required. + * @param loginId The loginID is required. * @param customClaims additional claims to be added to the verified token JWT - * @param timeout The timeout in seconds for the embedded link token + * @param timeout The timeout in seconds for the embedded link token. + * If not provided or set to 0, a default timeout (10 minutes) will be used. * @return It returns the token that can then be verified using the magic link 'verify' function, * either directly or through a flow. * @throws DescopeException If there occurs any exception, a subtype of this exception will be @@ -579,12 +580,13 @@ String generateEmbeddedLink(String loginId, Map customClaims, in * Generate a sign-up embedded link token, later can be used to authenticate via magiclink * verify method or via flow verify step. * - * @param loginId loginId The loginID is required. + * @param loginId The loginID is required. * @param user The user object containing user details. * @param emailVerified Boolean indicating if the email is verified. * @param phoneVerified Boolean indicating if the phone is verified. * @param loginOptions Options for the login process. - * @param timeout The timeout in seconds for the embedded link token + * @param timeout The timeout in seconds for the embedded link token. + * If not provided or set to 0, a default timeout (10 minutes) will be used. * @return It returns the token that can then be verified using the magic link 'verify' function, * either directly or through a flow. * @throws DescopeException If there occurs any exception, a subtype of this exception will be 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 e58e2619..b57b19a3 100644 --- a/src/main/java/com/descope/sdk/mgmt/impl/UserServiceImpl.java +++ b/src/main/java/com/descope/sdk/mgmt/impl/UserServiceImpl.java @@ -625,6 +625,9 @@ public String generateEmbeddedLink(String loginId, Map customCla if (StringUtils.isBlank(loginId)) { throw ServerCommonException.invalidArgument("Login ID"); } + if (timeout < 0) { + throw ServerCommonException.invalidArgument("Timeout"); + } URI generateEmbeddedLinkUri = composeGenerateEmbeddedLink(); GenerateEmbeddedLinkRequest request = new GenerateEmbeddedLinkRequest(loginId, customClaims, timeout); ApiProxy apiProxy = getApiProxy(); @@ -640,6 +643,9 @@ public String generateSignUpEmbeddedLink(String loginId, User user, Boolean emai if (StringUtils.isBlank(loginId)) { throw ServerCommonException.invalidArgument("Login ID"); } + if (timeout < 0) { + throw ServerCommonException.invalidArgument("Timeout"); + } URI generateSignUpEmbeddedLinkUri = composeGenerateSignUpEmbeddedLink(); GenerateSignUpEmbeddedLinkRequest request = new GenerateSignUpEmbeddedLinkRequest( loginId, user, emailVerified, phoneVerified, loginOptions, timeout);