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

#### OAuth 2.0 Client Assertion JWT

You can create client assertion JWTs for OAuth 2.0 client authentication per [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523). This is useful when authenticating to OAuth token endpoints that support JWT bearer client assertions.

```java
import com.descope.model.jwt.request.ClientAssertionRequest;
import java.security.KeyStore;
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import statement is missing java.io.FileInputStream and java.io.InputStream which are used in the example code. These should be added to the import section.

Suggested change
import java.security.KeyStore;
import java.security.KeyStore;
import java.io.FileInputStream;
import java.io.InputStream;

Copilot uses AI. Check for mistakes.
import java.security.interfaces.RSAPrivateKey;

JwtService jwtService = descopeClient.getManagementServices().getJwtService();

// Load your private key (example using keystore)
KeyStore keyStore = KeyStore.getInstance("PKCS12");
try (InputStream is = new FileInputStream("/path/to/keystore.p12")) {
keyStore.load(is, "keystore-password".toCharArray());
}
RSAPrivateKey privateKey = (RSAPrivateKey) keyStore.getKey("key-alias", "key-password".toCharArray());
Comment on lines +1256 to +1266
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The keystore loading code (lines 1262-1266) can throw multiple checked exceptions (KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException) that are not handled. This code should be wrapped in a try-catch block or the exceptions should be declared in the method signature of the enclosing method.

Suggested change
import java.security.KeyStore;
import java.security.interfaces.RSAPrivateKey;
JwtService jwtService = descopeClient.getManagementServices().getJwtService();
// Load your private key (example using keystore)
KeyStore keyStore = KeyStore.getInstance("PKCS12");
try (InputStream is = new FileInputStream("/path/to/keystore.p12")) {
keyStore.load(is, "keystore-password".toCharArray());
}
RSAPrivateKey privateKey = (RSAPrivateKey) keyStore.getKey("key-alias", "key-password".toCharArray());
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.interfaces.RSAPrivateKey;
JwtService jwtService = descopeClient.getManagementServices().getJwtService();
// Load your private key (example using keystore)
KeyStore keyStore;
RSAPrivateKey privateKey;
try {
keyStore = KeyStore.getInstance("PKCS12");
try (InputStream is = new FileInputStream("/path/to/keystore.p12")) {
keyStore.load(is, "keystore-password".toCharArray());
}
privateKey = (RSAPrivateKey) keyStore.getKey("key-alias", "key-password".toCharArray());
} catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException | UnrecoverableKeyException e) {
throw new RuntimeException("Failed to load private key from keystore", e);
}

Copilot uses AI. Check for mistakes.

// Create the client assertion JWT
ClientAssertionRequest request = ClientAssertionRequest.builder()
.clientId("your-client-id")
.tokenEndpoint("https://auth.example.com/oauth/token")
.privateKey(privateKey)
.algorithm("RS256") // Optional, defaults to RS256. Also supports ES256, etc.
.expirationSeconds(300) // Optional, defaults to 300 (5 minutes)
.build();

try {
String clientAssertion = jwtService.createClientAssertion(request);

// Use the client assertion in your OAuth token request
// POST to token endpoint with:
// grant_type=client_credentials
// client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
// client_assertion=<clientAssertion>
} catch (DescopeException de) {
// Handle the error
}
```

The generated JWT contains the following claims per RFC 7523:
- `iss` (issuer): Your client ID
- `sub` (subject): Your client ID
- `aud` (audience): The token endpoint URL
- `exp` (expiration): Current time + expiration seconds
- `iat` (issued at): Current time
- `jti` (JWT ID): Unique identifier to prevent replay attacks

### 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.descope.model.jwt.request;

import java.security.PrivateKey;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
* Request object for creating OAuth 2.0 client assertion JWT.
*
* <p>This is used for client authentication using JWT bearer tokens as per RFC 7523.
* The generated JWT can be used with OAuth token endpoints that support
* client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ClientAssertionRequest {
/**
* The client ID (issuer and subject of the JWT).
*/
private String clientId;

/**
* The token endpoint URL (audience of the JWT).
*/
private String tokenEndpoint;

/**
* The private key used to sign the JWT.
* Typically an RSA or ECDSA private key.
*/
private PrivateKey privateKey;

/**
* The signing algorithm to use (e.g., "RS256", "ES256").
* Defaults to "RS256" if not specified.
*/
@Builder.Default
private String algorithm = "RS256";

/**
* JWT expiration time in seconds.
* Defaults to 300 seconds (5 minutes) if not specified.
*/
@Builder.Default
private long expirationSeconds = 300;
}
11 changes: 11 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,6 +5,7 @@
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.magiclink.LoginOptions;
import java.util.Map;

Expand All @@ -31,4 +32,14 @@ AuthenticationInfo signUpOrIn(String loginId, MgmtSignUpUser signUpUserDetails)
AuthenticationInfo signIn(String loginId, LoginOptions loginOptions) throws DescopeException;

AuthenticationInfo anonymous(AnonymousUserRequest request) throws DescopeException;

/**
* Create an OAuth 2.0 client assertion JWT for client authentication.
* This JWT can be used with OAuth token endpoints that support RFC 7523.
*
* @param request - ClientAssertionRequest containing clientId, tokenEndpoint, privateKey, and signing algorithm
* @return - The signed JWT string that can be used as client_assertion parameter
* @throws DescopeException if JWT creation fails
*/
String createClientAssertion(ClientAssertionRequest request) throws DescopeException;
}
56 changes: 56 additions & 0 deletions src/main/java/com/descope/sdk/mgmt/impl/JwtServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
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;
Expand All @@ -22,8 +23,13 @@
import com.descope.model.magiclink.LoginOptions;
import com.descope.proxy.ApiProxy;
import com.descope.sdk.mgmt.JwtService;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SignatureAlgorithm import is deprecated in JJWT 0.12.0+. Replace with io.jsonwebtoken.security.SecureDigestAlgorithm and use Jwts.SIG.* constants for specific algorithms.

Suggested change
import io.jsonwebtoken.SignatureAlgorithm;

Copilot uses AI. Check for mistakes.
import java.net.URI;
import java.security.PrivateKey;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;

class JwtServiceImpl extends ManagementsBase implements JwtService {
Expand Down Expand Up @@ -137,6 +143,56 @@ private AuthenticationInfo validateAndCreateAuthInfo(JWTResponse jwtResponse) th
return new AuthenticationInfo(sessionToken, refreshToken, jwtResponse.getUser(), jwtResponse.getFirstSeen());
}

@Override
public String createClientAssertion(ClientAssertionRequest request) throws DescopeException {
if (request == null) {
throw ServerCommonException.invalidArgument("ClientAssertionRequest");
}
if (StringUtils.isBlank(request.getClientId())) {
throw ServerCommonException.invalidArgument("clientId");
}
if (StringUtils.isBlank(request.getTokenEndpoint())) {
throw ServerCommonException.invalidArgument("tokenEndpoint");
}
if (request.getPrivateKey() == null) {
throw ServerCommonException.invalidArgument("privateKey");
}

try {
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
Date expiration = new Date(nowMillis + (request.getExpirationSeconds() * 1000));

Comment on lines +164 to +165
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no validation for expirationSeconds to ensure it's positive and within reasonable bounds. A negative value would create a JWT that's already expired, and an extremely large value could cause integer overflow when multiplied by 1000 (line 164). Consider adding validation to ensure expirationSeconds is positive and within a reasonable range (e.g., 1-3600 seconds).

Suggested change
Date expiration = new Date(nowMillis + (request.getExpirationSeconds() * 1000));
long expirationSeconds = request.getExpirationSeconds();
// Ensure expiration is positive and within a reasonable bound to avoid overflow and invalid JWTs
if (expirationSeconds <= 0 || expirationSeconds > 3600L) {
throw ServerCommonException.invalidArgument("expirationSeconds");
}
long expirationMillis = nowMillis + (expirationSeconds * 1000L);
Date expiration = new Date(expirationMillis);

Copilot uses AI. Check for mistakes.
SignatureAlgorithm algorithm = getSignatureAlgorithm(request.getAlgorithm());
PrivateKey privateKey = request.getPrivateKey();
Comment on lines +166 to +167
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The local variable algorithm at line 166 and privateKey at line 167 are unnecessary intermediary assignments that don't improve readability. The values can be used directly in the JWT builder for cleaner code.

Copilot uses AI. Check for mistakes.

return Jwts.builder()
.setIssuer(request.getClientId())
.setSubject(request.getClientId())
.setAudience(request.getTokenEndpoint())
.setIssuedAt(now)
.setExpiration(expiration)
.setId(UUID.randomUUID().toString())
Comment on lines +170 to +175
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The setter methods like setIssuer(), setSubject(), setAudience(), setIssuedAt(), setExpiration(), and setId() are deprecated in JJWT 0.12.0+. Use the non-setter methods instead: issuer(), subject(), audience(), issuedAt(), expiration(), and id().

Suggested change
.setIssuer(request.getClientId())
.setSubject(request.getClientId())
.setAudience(request.getTokenEndpoint())
.setIssuedAt(now)
.setExpiration(expiration)
.setId(UUID.randomUUID().toString())
.issuer(request.getClientId())
.subject(request.getClientId())
.audience(request.getTokenEndpoint())
.issuedAt(now)
.expiration(expiration)
.id(UUID.randomUUID().toString())

Copilot uses AI. Check for mistakes.
.signWith(algorithm, privateKey)
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The signWith(SignatureAlgorithm, privateKey) method is deprecated in JJWT 0.12.0+. Use signWith(privateKey) instead, which automatically determines the appropriate algorithm based on the key type. If you need to explicitly specify the algorithm, use the newer Jwts.SIG.* constants (e.g., Jwts.SIG.RS256) with the signWith(Key, SecureDigestAlgorithm) method.

Copilot uses AI. Check for mistakes.
.compact();
} catch (Exception e) {
String errorMessage = "Failed to create client assertion JWT: " + e.getMessage();
throw ServerCommonException.parseResponseError(errorMessage, null, e);
}
}

private SignatureAlgorithm getSignatureAlgorithm(String algorithm) {
if (StringUtils.isBlank(algorithm)) {
return SignatureAlgorithm.RS256;
}

try {
return SignatureAlgorithm.forName(algorithm);
} catch (IllegalArgumentException e) {
throw ServerCommonException.invalidArgument("algorithm - unsupported: " + algorithm);
}
}
Comment on lines +184 to +194
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SignatureAlgorithm class is deprecated in JJWT 0.12.0+. Use the SecureDigestAlgorithm interface and Jwts.SIG.* constants instead (e.g., Jwts.SIG.RS256, Jwts.SIG.ES256). This also means the getSignatureAlgorithm() helper method should return a SecureDigestAlgorithm<PrivateKey, PublicKey> type instead.

Copilot uses AI. Check for mistakes.

private URI composeUpdateJwtUri() {
return getUri(UPDATE_JWT_LINK);
}
Expand Down
Loading