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
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1247,6 +1247,25 @@ mgmtSignUpUser.setCustomClaims(new HashMap<String, Object>() {{
AuthenticationInfo res = jwtService.signUpOrIn("Dummy", mgmtSignUpUser);
```

Generate a client assertion JWT for OAuth 2.0 client credentials flow.

```java
JwtService jwtService = descopeClient.getManagementServices().getJwtService();
try {
ClientAssertionResponse response = jwtService.generateClientAssertionJwt(
"client-id",
"client-id",
Arrays.asList("https://auth.example.com/token"),
3600,
false,
"RS256"
);
String jwt = response.getJwt();
} catch (DescopeException de) {
// Handle the error
}
```

### Audit

You can perform an audit search for either specific values or full-text across the fields. Audit search is limited to the last 30 days.
Expand Down
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.60</version>
<version>1.0.61</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 @@ -166,6 +166,7 @@ public static class ManagementEndPoints {

// JWT
public static final String UPDATE_JWT_LINK = "/v1/mgmt/jwt/update";
public static final String CLIENT_ASSERTION = "/v1/mgmt/token/clientassertion";
public static final String MANAGEMENT_SIGN_IN = "/v1/mgmt/auth/signin";
public static final String MANAGEMENT_SIGN_UP = "/v1/mgmt/auth/signup";
public static final String MANAGEMENT_SIGN_UP_OR_IN = "/v1/mgmt/auth/signup-in";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.descope.model.jwt.request;

import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ClientAssertionRequest {
String issuer;
String subject;
List<String> audience;
Integer expiresIn;
Boolean flattenAudience;
String algorithm;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.descope.model.jwt.response;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ClientAssertionResponse {
private String jwt;
}
19 changes: 19 additions & 0 deletions src/main/java/com/descope/sdk/mgmt/JwtService.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
import com.descope.model.jwt.MgmtSignUpUser;
import com.descope.model.jwt.Token;
import com.descope.model.jwt.request.AnonymousUserRequest;
import com.descope.model.jwt.response.ClientAssertionResponse;
import com.descope.model.magiclink.LoginOptions;
import java.util.List;
import java.util.Map;

/** Provide functions for manipulating valid JWT. */
Expand All @@ -31,4 +33,21 @@ AuthenticationInfo signUpOrIn(String loginId, MgmtSignUpUser signUpUserDetails)
AuthenticationInfo signIn(String loginId, LoginOptions loginOptions) throws DescopeException;

AuthenticationInfo anonymous(AnonymousUserRequest request) throws DescopeException;

/**
* Generate a client assertion JWT for OAuth 2.0 client credentials flow.
* This JWT can be used as a client authentication method when requesting access tokens.
*
* @param issuer - The issuer of the JWT (typically the client ID)
* @param subject - The subject of the JWT (typically the client ID)
* @param audience - The audience of the JWT (typically the authorization server)
* @param expiresIn - Expiration time in seconds
* @param flattenAudience - Whether to flatten the audience array to a single string (optional)
* @param algorithm - The signing algorithm to use: RS256, RS384, or ES384 (optional)
* @return ClientAssertionResponse containing the generated JWT
* @throws DescopeException if validation fails or the request cannot be completed
*/
ClientAssertionResponse generateClientAssertionJwt(String issuer, String subject,
List<String> audience, Integer expiresIn, Boolean flattenAudience, String algorithm)
throws DescopeException;
}
36 changes: 36 additions & 0 deletions src/main/java/com/descope/sdk/mgmt/impl/JwtServiceImpl.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.descope.sdk.mgmt.impl;

import static com.descope.literals.Routes.ManagementEndPoints.CLIENT_ASSERTION;
import static com.descope.literals.Routes.ManagementEndPoints.MANAGEMENT_ANONYMOUS_USER;
import static com.descope.literals.Routes.ManagementEndPoints.MANAGEMENT_SIGN_IN;
import static com.descope.literals.Routes.ManagementEndPoints.MANAGEMENT_SIGN_UP;
Expand All @@ -14,15 +15,18 @@
import com.descope.model.jwt.MgmtSignUpUser;
import com.descope.model.jwt.Token;
import com.descope.model.jwt.request.AnonymousUserRequest;
import com.descope.model.jwt.request.ClientAssertionRequest;
import com.descope.model.jwt.request.ManagementSignInRequest;
import com.descope.model.jwt.request.ManagementSignUpRequest;
import com.descope.model.jwt.request.UpdateJwtRequest;
import com.descope.model.jwt.response.ClientAssertionResponse;
import com.descope.model.jwt.response.JWTResponse;
import com.descope.model.jwt.response.UpdateJwtResponse;
import com.descope.model.magiclink.LoginOptions;
import com.descope.proxy.ApiProxy;
import com.descope.sdk.mgmt.JwtService;
import java.net.URI;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;

Expand Down Expand Up @@ -140,4 +144,36 @@ private AuthenticationInfo validateAndCreateAuthInfo(JWTResponse jwtResponse) th
private URI composeUpdateJwtUri() {
return getUri(UPDATE_JWT_LINK);
}

@Override
public ClientAssertionResponse generateClientAssertionJwt(String issuer, String subject,
List<String> audience, Integer expiresIn, Boolean flattenAudience, String algorithm)
throws DescopeException {
if (StringUtils.isBlank(issuer)) {
throw ServerCommonException.invalidArgument("issuer");
}
if (StringUtils.isBlank(subject)) {
throw ServerCommonException.invalidArgument("subject");
}
if (audience == null || audience.isEmpty()) {
throw ServerCommonException.invalidArgument("audience");
}
if (expiresIn == null || expiresIn <= 0) {
throw ServerCommonException.invalidArgument("expiresIn");
}

ClientAssertionRequest request = ClientAssertionRequest.builder()
.issuer(issuer)
.subject(subject)
.audience(audience)
.expiresIn(expiresIn)
.flattenAudience(flattenAudience)
.algorithm(algorithm)
.build();

URI uri = getUri(CLIENT_ASSERTION);
ApiProxy apiProxy = getApiProxy();
ClientAssertionResponse response = apiProxy.post(uri, request, ClientAssertionResponse.class);
return response;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
package com.descope.sdk.mgmt.impl;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;

import com.descope.exception.ServerCommonException;
import com.descope.model.client.Client;
import com.descope.model.jwt.response.ClientAssertionResponse;
import com.descope.proxy.ApiProxy;
import com.descope.proxy.impl.ApiProxyBuilder;
import com.descope.sdk.mgmt.JwtService;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;

public class JwtServiceImplClientAssertionTest {

@Test
void testGenerateClientAssertionJwtSuccess() {
ApiProxy apiProxy = mock(ApiProxy.class);
ClientAssertionResponse mockResponse = new ClientAssertionResponse("mock.jwt.token");
doReturn(mockResponse).when(apiProxy).post(any(), any(), eq(ClientAssertionResponse.class));

try (MockedStatic<ApiProxyBuilder> mockedBuilder = mockStatic(ApiProxyBuilder.class)) {
mockedBuilder.when(() -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy);

Client client = Client.builder()
.projectId("test-project")
.managementKey("test-key")
.build();
JwtService jwtService = new JwtServiceImpl(client);

List<String> audience = Arrays.asList("https://auth.example.com/token");
ClientAssertionResponse response = jwtService.generateClientAssertionJwt(
"client-id",
"client-id",
audience,
3600,
null,
null
);

assertNotNull(response);
assertEquals("mock.jwt.token", response.getJwt());
}
}

@Test
void testGenerateClientAssertionJwtWithOptionalParams() {
ApiProxy apiProxy = mock(ApiProxy.class);
ClientAssertionResponse mockResponse = new ClientAssertionResponse("mock.jwt.token");
doReturn(mockResponse).when(apiProxy).post(any(), any(), eq(ClientAssertionResponse.class));

try (MockedStatic<ApiProxyBuilder> mockedBuilder = mockStatic(ApiProxyBuilder.class)) {
mockedBuilder.when(() -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy);

Client client = Client.builder()
.projectId("test-project")
.managementKey("test-key")
.build();
JwtService jwtService = new JwtServiceImpl(client);

List<String> audience = Arrays.asList("https://auth.example.com/token");
ClientAssertionResponse response = jwtService.generateClientAssertionJwt(
"client-id",
"client-id",
audience,
3600,
true,
"RS256"
);

assertNotNull(response);
assertEquals("mock.jwt.token", response.getJwt());
}
}

@Test
void testGenerateClientAssertionJwtEmptyIssuer() {
Client client = Client.builder()
.projectId("test-project")
.managementKey("test-key")
.build();
JwtService jwtService = new JwtServiceImpl(client);

List<String> audience = Arrays.asList("https://auth.example.com/token");
ServerCommonException thrown = assertThrows(
ServerCommonException.class,
() -> jwtService.generateClientAssertionJwt("", "client-id", audience, 3600, null, null)
);
assertNotNull(thrown);
assertEquals("The issuer argument is invalid", thrown.getMessage());
}

@Test
void testGenerateClientAssertionJwtEmptySubject() {
Client client = Client.builder()
.projectId("test-project")
.managementKey("test-key")
.build();
JwtService jwtService = new JwtServiceImpl(client);

List<String> audience = Arrays.asList("https://auth.example.com/token");
ServerCommonException thrown = assertThrows(
ServerCommonException.class,
() -> jwtService.generateClientAssertionJwt("client-id", "", audience, 3600, null, null)
);
assertNotNull(thrown);
assertEquals("The subject argument is invalid", thrown.getMessage());
}

@Test
void testGenerateClientAssertionJwtNullAudience() {
Client client = Client.builder()
.projectId("test-project")
.managementKey("test-key")
.build();
JwtService jwtService = new JwtServiceImpl(client);

ServerCommonException thrown = assertThrows(
ServerCommonException.class,
() -> jwtService.generateClientAssertionJwt("client-id", "client-id", null, 3600, null, null)
);
assertNotNull(thrown);
assertEquals("The audience argument is invalid", thrown.getMessage());
}

@Test
void testGenerateClientAssertionJwtEmptyAudience() {
Client client = Client.builder()
.projectId("test-project")
.managementKey("test-key")
.build();
JwtService jwtService = new JwtServiceImpl(client);

List<String> audience = Collections.emptyList();
ServerCommonException thrown = assertThrows(
ServerCommonException.class,
() -> jwtService.generateClientAssertionJwt("client-id", "client-id", audience, 3600, null, null)
);
assertNotNull(thrown);
assertEquals("The audience argument is invalid", thrown.getMessage());
}

@Test
void testGenerateClientAssertionJwtNullExpiresIn() {
Client client = Client.builder()
.projectId("test-project")
.managementKey("test-key")
.build();
JwtService jwtService = new JwtServiceImpl(client);

List<String> audience = Arrays.asList("https://auth.example.com/token");
ServerCommonException thrown = assertThrows(
ServerCommonException.class,
() -> jwtService.generateClientAssertionJwt("client-id", "client-id", audience, null, null, null)
);
assertNotNull(thrown);
assertEquals("The expiresIn argument is invalid", thrown.getMessage());
}

@Test
void testGenerateClientAssertionJwtZeroExpiresIn() {
Client client = Client.builder()
.projectId("test-project")
.managementKey("test-key")
.build();
JwtService jwtService = new JwtServiceImpl(client);

List<String> audience = Arrays.asList("https://auth.example.com/token");
ServerCommonException thrown = assertThrows(
ServerCommonException.class,
() -> jwtService.generateClientAssertionJwt("client-id", "client-id", audience, 0, null, null)
);
assertNotNull(thrown);
assertEquals("The expiresIn argument is invalid", thrown.getMessage());
}

@Test
void testGenerateClientAssertionJwtNegativeExpiresIn() {
Client client = Client.builder()
.projectId("test-project")
.managementKey("test-key")
.build();
JwtService jwtService = new JwtServiceImpl(client);

List<String> audience = Arrays.asList("https://auth.example.com/token");
ServerCommonException thrown = assertThrows(
ServerCommonException.class,
() -> jwtService.generateClientAssertionJwt("client-id", "client-id", audience, -1, null, null)
);
assertNotNull(thrown);
assertEquals("The expiresIn argument is invalid", thrown.getMessage());
}
}