From 1fa789ef0f48e1e8d26975571169344474a86333 Mon Sep 17 00:00:00 2001 From: kridai Date: Wed, 16 Aug 2023 08:46:38 +0530 Subject: [PATCH 1/2] feat: add support for PS256, RS256 PKCV in validationClient --- .../com/twilio/example/ValidationExample.java | 15 ++-- .../com/twilio/http/ValidationClient.java | 69 ++++++++++++++++++- .../twilio/http/ValidationInterceptor.java | 23 ++++++- .../jwt/validation/ValidationToken.java | 45 +++++++++++- .../jwt/validation/ValidationTokenTest.java | 58 +++++++++++++++- 5 files changed, 200 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/twilio/example/ValidationExample.java b/src/main/java/com/twilio/example/ValidationExample.java index aae09834cd..62e92e45a5 100644 --- a/src/main/java/com/twilio/example/ValidationExample.java +++ b/src/main/java/com/twilio/example/ValidationExample.java @@ -13,6 +13,8 @@ import java.security.KeyPairGenerator; import java.util.Base64; +import io.jsonwebtoken.SignatureAlgorithm; + public class ValidationExample { public static final String ACCOUNT_SID = System.getenv("TWILIO_ACCOUNT_SID"); @@ -25,7 +27,7 @@ public class ValidationExample { * @throws TwiMLException if unable to generate TwiML */ public static void main(String[] args) throws Exception { - + //Twilio.setRegion("dev"); // Generate public/private key pair KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); keyGen.initialize(2048); @@ -35,7 +37,8 @@ public static void main(String[] args) throws Exception { // Use the default rest client TwilioRestClient client = new TwilioRestClient.Builder(ACCOUNT_SID, AUTH_TOKEN) - .build(); + .region("dev") + .build(); // Create a public key and signing key using the default client PublicKey key = PublicKey.creator( @@ -46,9 +49,11 @@ public static void main(String[] args) throws Exception { // Switch to validation client as the default client TwilioRestClient validationClient = new TwilioRestClient.Builder(signingKey.getSid(), signingKey.getSecret()) - .accountSid(ACCOUNT_SID) - .httpClient(new ValidationClient(ACCOUNT_SID, key.getSid(), signingKey.getSid(), pair.getPrivate())) - .build(); + .accountSid(ACCOUNT_SID) + .region("dev") + // Validation client supports RS256 or PS256 algorithm. Default is RS256. + .httpClient(new ValidationClient(ACCOUNT_SID, key.getSid(), signingKey.getSid(), pair.getPrivate(), SignatureAlgorithm.PS256)) + .build(); // Make REST API requests Iterable messages = Message.reader().read(validationClient); diff --git a/src/main/java/com/twilio/http/ValidationClient.java b/src/main/java/com/twilio/http/ValidationClient.java index 521983787e..6cdb01b915 100644 --- a/src/main/java/com/twilio/http/ValidationClient.java +++ b/src/main/java/com/twilio/http/ValidationClient.java @@ -19,6 +19,12 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Set; + +import io.jsonwebtoken.SignatureAlgorithm; + +import static io.jsonwebtoken.SignatureAlgorithm.PS256; +import static io.jsonwebtoken.SignatureAlgorithm.RS256; public class ValidationClient extends HttpClient { @@ -39,6 +45,23 @@ public ValidationClient(final String accountSid, this(accountSid, credentialSid, signingKey, privateKey, DEFAULT_REQUEST_CONFIG); } + /** + * Create a new ValidationClient. + * + * @param accountSid Twilio Account SID + * @param credentialSid Twilio Credential SID + * @param signingKey Twilio Signing key + * @param privateKey Private Key + * @param algorithm Client validation algorithm + */ + public ValidationClient(final String accountSid, + final String credentialSid, + final String signingKey, + final PrivateKey privateKey, + final SignatureAlgorithm algorithm) { + this(accountSid, credentialSid, signingKey, privateKey, DEFAULT_REQUEST_CONFIG, algorithm); + } + /** * Create a new ValidationClient. * @@ -53,7 +76,26 @@ public ValidationClient(final String accountSid, final String signingKey, final PrivateKey privateKey, final RequestConfig requestConfig) { - this(accountSid, credentialSid, signingKey, privateKey, requestConfig, DEFAULT_SOCKET_CONFIG); + this(accountSid, credentialSid, signingKey, privateKey, requestConfig, DEFAULT_SOCKET_CONFIG, RS256); + } + + /** + * Create a new ValidationClient. + * + * @param accountSid Twilio Account SID + * @param credentialSid Twilio Credential SID + * @param signingKey Twilio Signing key + * @param privateKey Private Key + * @param requestConfig HTTP Request Config + * @param algorithm Client validation algorithm + */ + public ValidationClient(final String accountSid, + final String credentialSid, + final String signingKey, + final PrivateKey privateKey, + final RequestConfig requestConfig, + final SignatureAlgorithm algorithm) { + this(accountSid, credentialSid, signingKey, privateKey, requestConfig, DEFAULT_SOCKET_CONFIG, algorithm); } /** @@ -72,6 +114,29 @@ public ValidationClient(final String accountSid, final PrivateKey privateKey, final RequestConfig requestConfig, final SocketConfig socketConfig) { + + this(accountSid, credentialSid, signingKey, privateKey, requestConfig, socketConfig, RS256); + } + + /** + * Create a new ValidationClient. + * + * @param accountSid Twilio Account SID + * @param credentialSid Twilio Credential SID + * @param signingKey Twilio Signing key + * @param privateKey Private Key + * @param requestConfig HTTP Request Config + * @param socketConfig HTTP Socket Config + * @param algorithm Client validation algorithm + */ + public ValidationClient(final String accountSid, + final String credentialSid, + final String signingKey, + final PrivateKey privateKey, + final RequestConfig requestConfig, + final SocketConfig socketConfig, + final SignatureAlgorithm algorithm) { + Collection headers = Arrays.asList( new BasicHeader("X-Twilio-Client", "java-" + Twilio.VERSION), new BasicHeader(HttpHeaders.ACCEPT, "application/json"), @@ -86,7 +151,7 @@ public ValidationClient(final String accountSid, .setDefaultRequestConfig(requestConfig) .setDefaultHeaders(headers) .setMaxConnPerRoute(10) - .addInterceptorLast(new ValidationInterceptor(accountSid, credentialSid, signingKey, privateKey)) + .addInterceptorLast(new ValidationInterceptor(accountSid, credentialSid, signingKey, privateKey, algorithm)) .setRedirectStrategy(this.getRedirectStrategy()) .build(); } diff --git a/src/main/java/com/twilio/http/ValidationInterceptor.java b/src/main/java/com/twilio/http/ValidationInterceptor.java index 4f8b5e2f59..c3c391e84c 100644 --- a/src/main/java/com/twilio/http/ValidationInterceptor.java +++ b/src/main/java/com/twilio/http/ValidationInterceptor.java @@ -12,6 +12,8 @@ import java.util.Arrays; import java.util.List; +import io.jsonwebtoken.SignatureAlgorithm; + public class ValidationInterceptor implements HttpRequestInterceptor { private static final List HEADERS = Arrays.asList("authorization", "host"); @@ -20,6 +22,7 @@ public class ValidationInterceptor implements HttpRequestInterceptor { private final String credentialSid; private final String signingKeySid; private final PrivateKey privateKey; + private final SignatureAlgorithm algorithm; /** * Create a new ValidationInterceptor. @@ -30,10 +33,27 @@ public class ValidationInterceptor implements HttpRequestInterceptor { * @param privateKey Private Key */ public ValidationInterceptor(String accountSid, String credentialSid, String signingKeySid, PrivateKey privateKey) { + + this(accountSid, credentialSid, signingKeySid, privateKey, SignatureAlgorithm.RS256); + } + + + /** + * Create a new ValidationInterceptor. + * + * @param accountSid Twilio Acocunt SID + * @param credentialSid Twilio Credential SID + * @param signingKeySid Twilio Signing Key + * @param privateKey Private Key + * @param algorithm Client validaiton algorithm + */ + public ValidationInterceptor(String accountSid, String credentialSid, String signingKeySid, PrivateKey privateKey, + SignatureAlgorithm algorithm) { this.accountSid = accountSid; this.credentialSid = credentialSid; this.signingKeySid = signingKeySid; this.privateKey = privateKey; + this.algorithm = algorithm; } @Override @@ -44,7 +64,8 @@ public void process(HttpRequest request, HttpContext context) throws HttpExcepti signingKeySid, privateKey, request, - HEADERS + HEADERS, + algorithm ); request.addHeader("Twilio-Client-Validation", jwt.toJwt()); } diff --git a/src/main/java/com/twilio/jwt/validation/ValidationToken.java b/src/main/java/com/twilio/jwt/validation/ValidationToken.java index dc8832afff..4a1f48dc60 100644 --- a/src/main/java/com/twilio/jwt/validation/ValidationToken.java +++ b/src/main/java/com/twilio/jwt/validation/ValidationToken.java @@ -8,13 +8,17 @@ import org.apache.http.HttpEntity; import org.apache.http.HttpEntityEnclosingRequest; import org.apache.http.HttpRequest; +import org.apache.http.impl.auth.UnsupportedDigestAlgorithmException; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; import java.security.PrivateKey; import java.util.*; import java.util.function.Function; +import static io.jsonwebtoken.SignatureAlgorithm.PS256; +import static io.jsonwebtoken.SignatureAlgorithm.RS256; public class ValidationToken extends Jwt { @@ -30,9 +34,12 @@ public class ValidationToken extends Jwt { private final List signedHeaders; private final String requestBody; + private static final Set supportedAlgorithms + = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(PS256, RS256))); + private ValidationToken(Builder b) { super( - SignatureAlgorithm.RS256, + b.algorithm, b.privateKey, b.credentialSid, new Date(new Date().getTime() + b.ttl * 1000) @@ -99,10 +106,37 @@ public static ValidationToken fromHttpRequest( HttpRequest request, List signedHeaders ) throws IOException { + + return fromHttpRequest(accountSid, credentialSid, signingKeySid, privateKey, request, signedHeaders, SignatureAlgorithm.RS256); + } + + /** + * Create a ValidationToken from an HTTP Request. + * + * @param accountSid Twilio Account SID + * @param credentialSid Twilio Credential SID + * @param signingKeySid Twilio Signing Key SID + * @param privateKey Private Key + * @param request HTTP Request + * @param signedHeaders Headers to sign + * @param algorithm Client validation algorithm + * @return The ValidationToken generated from the HttpRequest + * @throws IOException when unable to generate + */ + public static ValidationToken fromHttpRequest( + String accountSid, + String credentialSid, + String signingKeySid, + PrivateKey privateKey, + HttpRequest request, + List signedHeaders, + SignatureAlgorithm algorithm + ) throws IOException { Builder builder = new Builder(accountSid, credentialSid, signingKeySid, privateKey); String method = request.getRequestLine().getMethod(); builder.method(method); + builder.algorithm(algorithm); String uri = request.getRequestLine().getUri(); if (uri.contains("?")) { @@ -151,6 +185,8 @@ public static class Builder { private String requestBody = ""; private int ttl = 300; + private SignatureAlgorithm algorithm = SignatureAlgorithm.RS256; + /** * Create a new ValidationToken Builder. * @@ -206,6 +242,13 @@ public Builder ttl(int ttl) { return this; } + public Builder algorithm(SignatureAlgorithm algorithm) { + if (!supportedAlgorithms.contains(algorithm)) { + throw new IllegalArgumentException("Not supported!"); + } + this.algorithm = algorithm; + return this; + } public ValidationToken build() { return new ValidationToken(this); } diff --git a/src/test/java/com/twilio/jwt/validation/ValidationTokenTest.java b/src/test/java/com/twilio/jwt/validation/ValidationTokenTest.java index 9d8622e51b..c977fd0f3b 100644 --- a/src/test/java/com/twilio/jwt/validation/ValidationTokenTest.java +++ b/src/test/java/com/twilio/jwt/validation/ValidationTokenTest.java @@ -4,6 +4,8 @@ import com.twilio.jwt.Jwt; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.InvalidKeyException; import org.apache.http.*; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.message.BasicHeader; @@ -20,6 +22,7 @@ import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.PrivateKey; +import java.security.PublicKey; import java.util.*; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -28,6 +31,13 @@ import static org.mockito.Mockito.when; +import static io.jsonwebtoken.SignatureAlgorithm.PS256; +import static io.jsonwebtoken.SignatureAlgorithm.PS384; +import static io.jsonwebtoken.SignatureAlgorithm.PS512; +import static io.jsonwebtoken.SignatureAlgorithm.RS256; +import static io.jsonwebtoken.SignatureAlgorithm.RS384; +import static io.jsonwebtoken.SignatureAlgorithm.RS512; + public class ValidationTokenTest { private static final List SIGNED_HEADERS = Arrays.asList("host", "authorization"); @@ -38,6 +48,8 @@ public class ValidationTokenTest { private Header[] headers; private PrivateKey privateKey; + private PublicKey publicKey; + @Mock private HttpRequest request; @@ -61,6 +73,7 @@ public void setup() throws Exception { keyGen.initialize(2048); KeyPair pair = keyGen.generateKeyPair(); privateKey = pair.getPrivate(); + publicKey = pair.getPublic(); } @Test @@ -79,13 +92,56 @@ public void testTokenBuilder() { .setSigningKey(privateKey).build() .parseClaimsJws(jwt.toJwt()) .getBody(); - + jwt.getHeaders() this.validateToken(claims); Assert.assertEquals("authorization;host", claims.get("hrh")); Assert.assertEquals("4dc9b67bed579647914587b0e22a1c65c1641d8674797cd82de65e766cce5f80", claims.get("rqh")); } + @Test + public void testTokenValidAlgorithms() { + List validAlgorithms = Arrays.asList(RS256, PS256); + for (SignatureAlgorithm alg : validAlgorithms) { + Jwt jwt = new ValidationToken.Builder(ACCOUNT_SID, CREDENTIAL_SID, SIGNING_KEY_SID, privateKey) + .algorithm(alg) + .method("GET") + .uri("/Messages") + .queryString("PageSize=5&Limit=10") + .headers(headers) + .signedHeaders(SIGNED_HEADERS) + .requestBody("foobar") + .build(); + + Claims claims = + Jwts.parserBuilder().setSigningKey(publicKey).build() + .parseClaimsJws(jwt.toJwt()) + .getBody(); + validateToken(claims); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testTokenInvalidAlgorithms() { + List validAlgorithms = Arrays.asList(SignatureAlgorithm.HS256, SignatureAlgorithm.ES256, RS384, RS512, PS384, PS512); + for (SignatureAlgorithm alg : validAlgorithms) { + Jwt jwt = new ValidationToken.Builder(ACCOUNT_SID, CREDENTIAL_SID, SIGNING_KEY_SID, privateKey) + .algorithm(alg) + .method("GET") + .uri("/Messages") + .queryString("PageSize=5&Limit=10") + .headers(headers) + .signedHeaders(SIGNED_HEADERS) + .requestBody("foobar") + .build(); + + + Jwts.parserBuilder().setSigningKey(publicKey).build() + .parseClaimsJws(jwt.toJwt()) + .getBody(); + } + } + @Test public void testTokenFromHttpRequest() throws IOException { when(request.getRequestLine()).thenReturn(requestLine); From eb5f2ae8c0afdc5121031b14e833776398a71cef Mon Sep 17 00:00:00 2001 From: kridai Date: Thu, 17 Aug 2023 16:49:06 +0530 Subject: [PATCH 2/2] chore: fix build --- src/test/java/com/twilio/jwt/validation/ValidationTokenTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/com/twilio/jwt/validation/ValidationTokenTest.java b/src/test/java/com/twilio/jwt/validation/ValidationTokenTest.java index c977fd0f3b..a1dc059534 100644 --- a/src/test/java/com/twilio/jwt/validation/ValidationTokenTest.java +++ b/src/test/java/com/twilio/jwt/validation/ValidationTokenTest.java @@ -92,7 +92,6 @@ public void testTokenBuilder() { .setSigningKey(privateKey).build() .parseClaimsJws(jwt.toJwt()) .getBody(); - jwt.getHeaders() this.validateToken(claims); Assert.assertEquals("authorization;host", claims.get("hrh"));