-
Notifications
You must be signed in to change notification settings - Fork 4
feat(jwt): add oauth 2.0 client assertion creation per rfc 7523 #294
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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); | |
| } |
| 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; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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; | ||||||||||||||||||||||||||
|
|
@@ -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; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
| import io.jsonwebtoken.SignatureAlgorithm; |
Copilot
AI
Feb 15, 2026
There was a problem hiding this comment.
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).
| 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
AI
Feb 15, 2026
There was a problem hiding this comment.
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
AI
Feb 15, 2026
There was a problem hiding this comment.
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().
| .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
AI
Feb 15, 2026
There was a problem hiding this comment.
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
AI
Feb 15, 2026
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.FileInputStreamandjava.io.InputStreamwhich are used in the example code. These should be added to the import section.