Skip to content

Commit a7c3bc0

Browse files
authored
Add timeout to generate embedded link and allow create link for signup (#310)
* Add timeout to generate embedded link and allow create link for signup * Now with the missing file * Added backward compatibility
1 parent 22c72d8 commit a7c3bc0

8 files changed

Lines changed: 187 additions & 8 deletions

File tree

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<groupId>com.descope</groupId>
66
<artifactId>java-sdk</artifactId>
77
<modelVersion>4.0.0</modelVersion>
8-
<version>1.0.63</version>
8+
<version>1.0.64</version>
99
<name>${project.groupId}:${project.artifactId}</name>
1010
<description>Java library used to integrate with Descope.</description>
1111
<url>https://github.com/descope/descope-java</url>

src/main/java/com/descope/literals/Routes.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ public static class ManagementEndPoints {
119119
public static final String USER_SET_TEMPORARY_PASSWORD_LINK = "/v1/mgmt/user/password/set/temporary";
120120
public static final String USER_EXPIRE_PASSWORD_LINK = "/v1/mgmt/user/password/expire";
121121
public static final String USER_CREATE_EMBEDDED_LINK = "/v1/mgmt/user/signin/embeddedlink";
122+
public static final String USER_SIGNUP_EMBEDDED_LINK = "/v1/mgmt/user/signup/embeddedlink";
122123
public static final String USER_HISTORY_LINK = "/v1/mgmt/user/history";
123124

124125
// Tenant

src/main/java/com/descope/model/magiclink/LoginOptions.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ public class LoginOptions {
1818
private boolean revokeOtherSessions;
1919
private String[] revokeOtherSessionsTypes;
2020
private String jwt;
21+
private String locale;
22+
private String templateId;
2123

2224
public LoginOptions(boolean stepup, boolean mfa, Map<String, Object> customClaims,
2325
Map<String, String> templateOptions) {
@@ -27,6 +29,18 @@ public LoginOptions(boolean stepup, boolean mfa, Map<String, Object> customClaim
2729
this.templateOptions = templateOptions;
2830
}
2931

32+
public LoginOptions(boolean stepup, boolean mfa, Map<String, Object> customClaims,
33+
Map<String, String> templateOptions, boolean revokeOtherSessions,
34+
String[] revokeOtherSessionsTypes, String jwt) {
35+
this.stepup = stepup;
36+
this.mfa = mfa;
37+
this.customClaims = customClaims;
38+
this.templateOptions = templateOptions;
39+
this.revokeOtherSessions = revokeOtherSessions;
40+
this.revokeOtherSessionsTypes = revokeOtherSessionsTypes;
41+
this.jwt = jwt;
42+
}
43+
3044
public boolean isJWTRequired() {
3145
return this.isStepup() || this.isMfa();
3246
}

src/main/java/com/descope/model/user/request/GenerateEmbeddedLinkRequest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@
1313
public class GenerateEmbeddedLinkRequest {
1414
private String loginId;
1515
private Map<String, Object> customClaims;
16+
private int timeout;
1617
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.descope.model.user.request;
2+
3+
import com.descope.model.magiclink.LoginOptions;
4+
import com.descope.model.user.User;
5+
import lombok.AllArgsConstructor;
6+
import lombok.Builder;
7+
import lombok.Data;
8+
import lombok.NoArgsConstructor;
9+
10+
@Data
11+
@Builder
12+
@NoArgsConstructor
13+
@AllArgsConstructor
14+
public class GenerateSignUpEmbeddedLinkRequest {
15+
private String loginId;
16+
private User user;
17+
private Boolean emailVerified;
18+
private Boolean phoneVerified;
19+
private LoginOptions loginOptions;
20+
private int timeout;
21+
}

src/main/java/com/descope/sdk/mgmt/UserService.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import com.descope.enums.DeliveryMethod;
44
import com.descope.exception.DescopeException;
55
import com.descope.model.auth.InviteOptions;
6+
import com.descope.model.magiclink.LoginOptions;
7+
import com.descope.model.user.User;
68
import com.descope.model.user.request.BatchUserRequest;
79
import com.descope.model.user.request.PatchUserRequest;
810
import com.descope.model.user.request.UserRequest;
@@ -558,12 +560,49 @@ EnchantedLinkTestUserResponse generateEnchantedLinkForTestUser(String loginId, S
558560
String generateEmbeddedLink(String loginId, Map<String, Object> customClaims)
559561
throws DescopeException;
560562

563+
/**
564+
* Generate an embedded link token, later can be used to authenticate via magiclink verify method
565+
* or via flow verify step.
566+
*
567+
* @param loginId The loginID is required.
568+
* @param customClaims additional claims to be added to the verified token JWT
569+
* @param timeout The timeout in seconds for the embedded link token.
570+
* If not provided or set to 0, a default timeout (10 minutes) will be used.
571+
* @return It returns the token that can then be verified using the magic link 'verify' function,
572+
* either directly or through a flow.
573+
* @throws DescopeException If there occurs any exception, a subtype of this exception will be
574+
* thrown.
575+
*/
576+
String generateEmbeddedLink(String loginId, Map<String, Object> customClaims, int timeout)
577+
throws DescopeException;
578+
579+
/**
580+
* Generate a sign-up embedded link token, later can be used to authenticate via magiclink
581+
* verify method or via flow verify step.
582+
*
583+
* @param loginId The loginID is required.
584+
* @param user The user object containing user details.
585+
* @param emailVerified Boolean indicating if the email is verified.
586+
* @param phoneVerified Boolean indicating if the phone is verified.
587+
* @param loginOptions Options for the login process.
588+
* @param timeout The timeout in seconds for the embedded link token.
589+
* If not provided or set to 0, a default timeout (10 minutes) will be used.
590+
* @return It returns the token that can then be verified using the magic link 'verify' function,
591+
* either directly or through a flow.
592+
* @throws DescopeException If there occurs any exception, a subtype of this exception will be
593+
* thrown.
594+
*/
595+
String generateSignUpEmbeddedLink(String loginId, User user, Boolean emailVerified,
596+
Boolean phoneVerified, LoginOptions loginOptions, int timeout)
597+
throws DescopeException;
598+
561599
/**
562600
* Use to retrieve users' authentication history, by the given user's ids.
563601
*
564602
* @param userIds List of user IDs to retrieve the history for
565603
* @return {{@link List} of {@link UserHistoryResponse}} of all requested users login history
566604
* @throws DescopeException If there occurs any exception, a subtype of this exception will be
605+
* thrown.
567606
*/
568607
List<UserHistoryResponse> history(List<String> userIds) throws DescopeException;
569608
}

src/main/java/com/descope/sdk/mgmt/impl/UserServiceImpl.java

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import static com.descope.literals.Routes.ManagementEndPoints.USER_SET_ROLES_LINK;
3232
import static com.descope.literals.Routes.ManagementEndPoints.USER_SET_SSO_APPS_LINK;
3333
import static com.descope.literals.Routes.ManagementEndPoints.USER_SET_TEMPORARY_PASSWORD_LINK;
34+
import static com.descope.literals.Routes.ManagementEndPoints.USER_SIGNUP_EMBEDDED_LINK;
3435
import static com.descope.literals.Routes.ManagementEndPoints.USER_UPDATE_EMAIL_LINK;
3536
import static com.descope.literals.Routes.ManagementEndPoints.USER_UPDATE_PHONE_LINK;
3637
import static com.descope.literals.Routes.ManagementEndPoints.USER_UPDATE_STATUS_LINK;
@@ -43,9 +44,12 @@
4344
import com.descope.exception.ServerCommonException;
4445
import com.descope.model.auth.InviteOptions;
4546
import com.descope.model.client.Client;
47+
import com.descope.model.magiclink.LoginOptions;
48+
import com.descope.model.user.User;
4649
import com.descope.model.user.request.BatchUserRequest;
4750
import com.descope.model.user.request.EnchantedLinkTestUserRequest;
4851
import com.descope.model.user.request.GenerateEmbeddedLinkRequest;
52+
import com.descope.model.user.request.GenerateSignUpEmbeddedLinkRequest;
4953
import com.descope.model.user.request.MagicLinkTestUserRequest;
5054
import com.descope.model.user.request.OTPTestUserRequest;
5155
import com.descope.model.user.request.PatchUserRequest;
@@ -609,18 +613,48 @@ public List<UserHistoryResponse> history(List<String> userIds) throws DescopeExc
609613
});
610614
}
611615

612-
public String generateEmbeddedLink(String loginId, Map<String, Object> customClaims) throws DescopeException {
616+
@Override
617+
public String generateEmbeddedLink(String loginId, Map<String, Object> customClaims)
618+
throws DescopeException {
619+
return generateEmbeddedLink(loginId, customClaims, 0);
620+
}
621+
622+
@Override
623+
public String generateEmbeddedLink(String loginId, Map<String, Object> customClaims, int timeout)
624+
throws DescopeException {
613625
if (StringUtils.isBlank(loginId)) {
614626
throw ServerCommonException.invalidArgument("Login ID");
615627
}
628+
if (timeout < 0) {
629+
throw ServerCommonException.invalidArgument("Timeout");
630+
}
616631
URI generateEmbeddedLinkUri = composeGenerateEmbeddedLink();
617-
GenerateEmbeddedLinkRequest request = new GenerateEmbeddedLinkRequest(loginId, customClaims);
632+
GenerateEmbeddedLinkRequest request = new GenerateEmbeddedLinkRequest(loginId, customClaims, timeout);
618633
ApiProxy apiProxy = getApiProxy();
619634
GenerateEmbeddedLinkResponse response = apiProxy.post(generateEmbeddedLinkUri, request,
620635
GenerateEmbeddedLinkResponse.class);
621636
return response.getToken();
622637
}
623638

639+
@Override
640+
public String generateSignUpEmbeddedLink(String loginId, User user, Boolean emailVerified,
641+
Boolean phoneVerified, LoginOptions loginOptions, int timeout)
642+
throws DescopeException {
643+
if (StringUtils.isBlank(loginId)) {
644+
throw ServerCommonException.invalidArgument("Login ID");
645+
}
646+
if (timeout < 0) {
647+
throw ServerCommonException.invalidArgument("Timeout");
648+
}
649+
URI generateSignUpEmbeddedLinkUri = composeGenerateSignUpEmbeddedLink();
650+
GenerateSignUpEmbeddedLinkRequest request = new GenerateSignUpEmbeddedLinkRequest(
651+
loginId, user, emailVerified, phoneVerified, loginOptions, timeout);
652+
ApiProxy apiProxy = getApiProxy();
653+
GenerateEmbeddedLinkResponse response = apiProxy.post(generateSignUpEmbeddedLinkUri, request,
654+
GenerateEmbeddedLinkResponse.class);
655+
return response.getToken();
656+
}
657+
624658
private URI composeCreateUserUri() {
625659
return getUri(CREATE_USER_LINK);
626660
}
@@ -753,4 +787,8 @@ private URI composeGenerateEmbeddedLink() {
753787
return getUri(USER_CREATE_EMBEDDED_LINK);
754788
}
755789

790+
private URI composeGenerateSignUpEmbeddedLink() {
791+
return getUri(USER_SIGNUP_EMBEDDED_LINK);
792+
}
793+
756794
}

src/test/java/com/descope/sdk/mgmt/impl/UserServiceImplTest.java

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
import com.descope.model.auth.AuthenticationServices;
2727
import com.descope.model.auth.InviteOptions;
2828
import com.descope.model.client.Client;
29+
import com.descope.model.magiclink.LoginOptions;
2930
import com.descope.model.mgmt.ManagementServices;
31+
import com.descope.model.user.User;
3032
import com.descope.model.user.request.BatchUserPasswordHashed;
3133
import com.descope.model.user.request.BatchUserPasswordPbkdf2;
3234
import com.descope.model.user.request.BatchUserPasswordSha;
@@ -797,7 +799,7 @@ void testGenerateEnchantedLinkForTestUserForSuccess() {
797799
@Test
798800
void testGenerateEmbeddedLinkForEmptyLoginId() {
799801
ServerCommonException thrown = assertThrows(ServerCommonException.class,
800-
() -> userService.generateEmbeddedLink("", null));
802+
() -> userService.generateEmbeddedLink("", null, 0));
801803
assertNotNull(thrown);
802804
assertEquals("The Login ID argument is invalid", thrown.getMessage());
803805
}
@@ -809,7 +811,7 @@ void testGenerateEmbeddedLinkForSuccess() {
809811
doReturn(mockResponse).when(apiProxy).post(any(), any(), any());
810812
try (MockedStatic<ApiProxyBuilder> mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) {
811813
mockedApiProxyBuilder.when(() -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy);
812-
String response = userService.generateEmbeddedLink("someLoginId", null);
814+
String response = userService.generateEmbeddedLink("someLoginId", null, 0);
813815
Assertions.assertThat(response).isEqualTo("someToken");
814816
}
815817
}
@@ -1068,11 +1070,11 @@ void testFunctionalGenerateEmbeddedLink() {
10681070
UserResponse user = createResponse.getUser();
10691071
assertNotNull(user);
10701072
Assertions.assertThat(user.getLoginIds()).contains(loginId);
1071-
String token = userService.generateEmbeddedLink(loginId, null);
1073+
String token = userService.generateEmbeddedLink(loginId, null, 0);
10721074
AuthenticationInfo authInfo = magicLinkService.verify(token);
10731075
assertNotNull(authInfo.getToken());
10741076
assertThat(authInfo.getToken().getJwt()).isNotBlank();
1075-
token = userService.generateEmbeddedLink(loginId, mapOf("kuku", "kiki"));
1077+
token = userService.generateEmbeddedLink(loginId, mapOf("kuku", "kiki"), 0);
10761078
final long now = System.currentTimeMillis();
10771079
authInfo = magicLinkService.verify(token);
10781080
assertNotNull(authInfo.getToken());
@@ -1121,7 +1123,7 @@ void testFunctionalGenerateEmbeddedLinkWithPhoneAsID() {
11211123
UserResponse user = createResponse.getUser();
11221124
assertNotNull(user);
11231125
Assertions.assertThat(user.getLoginIds()).contains(cleanPhone);
1124-
String token = userService.generateEmbeddedLink(phone, null);
1126+
String token = userService.generateEmbeddedLink(phone, null, 0);
11251127
AuthenticationInfo authInfo = magicLinkService.verify(token);
11261128
assertNotNull(authInfo.getToken());
11271129
assertThat(authInfo.getToken().getJwt()).isNotBlank();
@@ -1131,6 +1133,69 @@ void testFunctionalGenerateEmbeddedLinkWithPhoneAsID() {
11311133
userService.delete(phone);
11321134
}
11331135

1136+
@Test
1137+
void testGenerateSignUpEmbeddedLinkForEmptyLoginId() {
1138+
ServerCommonException thrown = assertThrows(ServerCommonException.class,
1139+
() -> userService.generateSignUpEmbeddedLink("", null, false, false, null, 0));
1140+
assertNotNull(thrown);
1141+
assertEquals("The Login ID argument is invalid", thrown.getMessage());
1142+
}
1143+
1144+
@Test
1145+
void testGenerateSignUpEmbeddedLinkForSuccess() {
1146+
GenerateEmbeddedLinkResponse mockResponse = new GenerateEmbeddedLinkResponse("someSignUpToken");
1147+
ApiProxy apiProxy = mock(ApiProxy.class);
1148+
doReturn(mockResponse).when(apiProxy).post(any(), any(), any());
1149+
try (MockedStatic<ApiProxyBuilder> mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) {
1150+
mockedApiProxyBuilder.when(() -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy);
1151+
User user = User.builder().name("Test User").email("test@example.com").build();
1152+
LoginOptions loginOptions = LoginOptions.builder().stepup(false).mfa(false).build();
1153+
String response = userService.generateSignUpEmbeddedLink("someLoginId", user, true, false, loginOptions, 300);
1154+
Assertions.assertThat(response).isEqualTo("someSignUpToken");
1155+
}
1156+
}
1157+
1158+
@RetryingTest(value = 3, suspendForMs = 30000, onExceptions = RateLimitExceededException.class)
1159+
void testFunctionalGenerateSignUpEmbeddedLink() {
1160+
String loginId = TestUtils.getRandomName("signup-");
1161+
String email = TestUtils.getRandomName("test-") + "@descope.com";
1162+
String phone = "+1-555-555-5555";
1163+
1164+
User user = User.builder()
1165+
.name("Test Signup User")
1166+
.email(email)
1167+
.phone(phone)
1168+
.build();
1169+
1170+
LoginOptions loginOptions = LoginOptions.builder()
1171+
.stepup(false)
1172+
.mfa(false)
1173+
.customClaims(mapOf("signup", true))
1174+
.build();
1175+
1176+
// Test generateSignUpEmbeddedLink with basic parameters
1177+
String token = userService.generateSignUpEmbeddedLink(loginId, user, true, true, loginOptions, 300);
1178+
assertNotNull(token);
1179+
assertThat(token).isNotBlank();
1180+
1181+
// Verify the token by using magic link verify
1182+
AuthenticationInfo authInfo = magicLinkService.verify(token);
1183+
assertNotNull(authInfo.getToken());
1184+
assertThat(authInfo.getToken().getJwt()).isNotBlank();
1185+
1186+
// Verify custom claims are present
1187+
Map<String, Object> claims = authInfo.getToken().getClaims();
1188+
assertThat(claims).containsKey("signup");
1189+
assertEquals(true, claims.get("signup"));
1190+
1191+
// Clean up - delete the created user
1192+
try {
1193+
userService.delete(loginId);
1194+
} catch (DescopeException e) {
1195+
// User might not exist if sign-up didn't complete, ignore
1196+
}
1197+
}
1198+
11341199
@RetryingTest(value = 3, suspendForMs = 30000, onExceptions = RateLimitExceededException.class)
11351200
void testFunctionalBatch() throws Exception {
11361201
String loginId = TestUtils.getRandomName("u-");

0 commit comments

Comments
 (0)