Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<groupId>com.descope</groupId>
<artifactId>java-sdk</artifactId>
<modelVersion>4.0.0</modelVersion>
<version>1.0.63</version>
<version>1.0.64</version>
<name>${project.groupId}:${project.artifactId}</name>
<description>Java library used to integrate with Descope.</description>
<url>https://github.com/descope/descope-java</url>
Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/descope/literals/Routes.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/com/descope/model/magiclink/LoginOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Object> customClaims,
Map<String, String> templateOptions) {
Expand All @@ -27,6 +29,18 @@ public LoginOptions(boolean stepup, boolean mfa, Map<String, Object> customClaim
this.templateOptions = templateOptions;
}

public LoginOptions(boolean stepup, boolean mfa, Map<String, Object> customClaims,
Map<String, String> 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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@
public class GenerateEmbeddedLinkRequest {
private String loginId;
private Map<String, Object> customClaims;
private int timeout;
}
Original file line number Diff line number Diff line change
@@ -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;
}
39 changes: 39 additions & 0 deletions src/main/java/com/descope/sdk/mgmt/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -558,12 +560,49 @@ EnchantedLinkTestUserResponse generateEnchantedLinkForTestUser(String loginId, S
String generateEmbeddedLink(String loginId, Map<String, Object> customClaims)
throws DescopeException;

/**
* Generate an embedded link token, later can be used to authenticate via magiclink verify method
* or via flow verify step.
*
* @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.
* 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
* thrown.
*/
String generateEmbeddedLink(String loginId, Map<String, Object> 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 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.
* 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
* thrown.
*/
String generateSignUpEmbeddedLink(String loginId, User user, Boolean emailVerified,
Boolean phoneVerified, LoginOptions loginOptions, int timeout)
throws DescopeException;

/**
* Use to retrieve users' authentication history, by the given user's ids.
*
* @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<UserHistoryResponse> history(List<String> userIds) throws DescopeException;
}
42 changes: 40 additions & 2 deletions src/main/java/com/descope/sdk/mgmt/impl/UserServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -609,18 +613,48 @@ public List<UserHistoryResponse> history(List<String> userIds) throws DescopeExc
});
}

public String generateEmbeddedLink(String loginId, Map<String, Object> customClaims) throws DescopeException {
@Override
public String generateEmbeddedLink(String loginId, Map<String, Object> customClaims)
throws DescopeException {
return generateEmbeddedLink(loginId, customClaims, 0);
}

@Override
public String generateEmbeddedLink(String loginId, Map<String, Object> customClaims, int timeout)
throws DescopeException {
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);
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");
}
if (timeout < 0) {
throw ServerCommonException.invalidArgument("Timeout");
}
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);
}
Expand Down Expand Up @@ -753,4 +787,8 @@ private URI composeGenerateEmbeddedLink() {
return getUri(USER_CREATE_EMBEDDED_LINK);
}

private URI composeGenerateSignUpEmbeddedLink() {
return getUri(USER_SIGNUP_EMBEDDED_LINK);
}

}
75 changes: 70 additions & 5 deletions src/test/java/com/descope/sdk/mgmt/impl/UserServiceImplTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}
Expand All @@ -809,7 +811,7 @@ void testGenerateEmbeddedLinkForSuccess() {
doReturn(mockResponse).when(apiProxy).post(any(), any(), any());
try (MockedStatic<ApiProxyBuilder> 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");
}
}
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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();
Expand All @@ -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<ApiProxyBuilder> 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<String, Object> 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-");
Expand Down
Loading