Skip to content

Commit

Permalink
feat: Add verifySignature utility method to Voice and Messages (#491)
Browse files Browse the repository at this point in the history
* feat: Add verifySignature utility method

* Bump version: v7.10.0 → v7.11.0
  • Loading branch information
SMadani authored Oct 30, 2023
1 parent 4751026 commit 80a4cdc
Show file tree
Hide file tree
Showing 13 changed files with 113 additions and 21 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[bumpversion]
commit = True
tag = False
current_version = v7.10.0
current_version = v7.11.0
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\-(?P<release>[a-z]+)(?P<build>\d+))?
serialize =
{major}.{minor}.{patch}-{release}{build}
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

# [7.11.0] - 2023-11-??
- Added `verifySignature` utility method to Voice and Messages clients
- Added `applicationId(UUID)` overload to `VonageClient.Builder`
- Added direct call modification methods to `VoiceClient`
- Added `downloadRecordingRaw` and `saveRecording` methods to `VoiceClient`
- Deprecated `ModifyCallResponse` and `VoiceClient.modifyCall`
- Deprecated `VoiceClient.downloadRecording` and `Recording` class
- Internal refactoring of Voice API implementation
- Bumped `com.vonage:jwt` version to 1.1.0

# [7.10.0] - 2023-10-20
- Added more locales for Verify v2 and Meetings APIs
- Added `check_url` to `VerificationResponse` to support synchronous Silent Authentication
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ See all of our SDKs and integrations on the [Vonage Developer portal](https://de

## Installation

Releases are published to [Maven Central](https://central.sonatype.com/artifact/com.vonage/client/7.10.0/snippets).
Releases are published to [Maven Central](https://central.sonatype.com/artifact/com.vonage/client/7.11.0/snippets).
Instructions for your build system can be found in the snippets section.
They're also available from [here](https://mvnrepository.com/artifact/com.vonage/client/7.10.0).
They're also available from [here](https://mvnrepository.com/artifact/com.vonage/client/7.11.0).
Release notes can be found in the [changelog](CHANGELOG.md).

### Build It Yourself
Expand Down
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ plugins {

group = "com.vonage"
archivesBaseName = "client"
version = "7.10.0"
version = "7.11.0"
sourceCompatibility = "1.8"
targetCompatibility = "1.8"

Expand All @@ -32,7 +32,7 @@ dependencies {
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.3'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.3'
implementation 'io.openapitools.jackson.dataformat:jackson-dataformat-hal:1.0.9'
implementation 'com.vonage:jwt:1.0.2'
implementation 'com.vonage:jwt:1.1.0'

testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.0'
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/vonage/client/HttpWrapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
*/
public class HttpWrapper {
private static final String CLIENT_NAME = "vonage-java-sdk";
private static final String CLIENT_VERSION = "7.10.0";
private static final String CLIENT_VERSION = "7.11.0";
private static final String JAVA_VERSION = System.getProperty("java.version");
private static final String USER_AGENT = String.format("%s/%s java/%s", CLIENT_NAME, CLIENT_VERSION, JAVA_VERSION);

Expand Down
15 changes: 15 additions & 0 deletions src/main/java/com/vonage/client/VonageClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;

/**
* Top-level Vonage API client object.
Expand Down Expand Up @@ -250,6 +251,20 @@ public Builder httpClient(HttpClient httpClient) {
return this;
}

/**
* Set the application ID for this client. This will be used alongside your private key
* (se via {@link #privateKeyContents}) for authenticating requests.
*
* @param applicationId The application UUID.
*
* @return This builder.
*
* @since 7.11.0
*/
public Builder applicationId(UUID applicationId) {
return applicationId(applicationId.toString());
}

/**
* When setting an applicationId, it is also expected that the {@link #privateKeyContents} will also be set.
*
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/com/vonage/client/messages/MessagesClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.vonage.client.auth.JWTAuthMethod;
import com.vonage.client.auth.TokenAuthMethod;
import com.vonage.client.common.HttpMethod;
import com.vonage.jwt.Jwt;
import java.util.function.Function;

public class MessagesClient {
Expand Down Expand Up @@ -99,4 +100,20 @@ public MessagesClient useRegularEndpoint() {
sandbox = false;
return this;
}

/**
* Utility method for verifying whether a token was signed by a secret.
* This is mostly useful when using signed callbacks to ensure that the inbound
* data came from Vonage servers. The signature is performed using the SHA-256 HMAC algorithm.
*
* @param jwt The JSON Web Token to verify.
* @param secret The symmetric secret key (HS256) to use for decrypting the token's signature.
*
* @return {@code true} if the token was signed by the secret, {@code false} otherwise.
*
* @since 7.11.0
*/
public static boolean verifySignature(String jwt, String secret) {
return Jwt.verifySignature(jwt, secret);
}
}
17 changes: 17 additions & 0 deletions src/main/java/com/vonage/client/voice/VoiceClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.vonage.client.auth.JWTAuthMethod;
import com.vonage.client.common.HttpMethod;
import com.vonage.client.voice.ncco.Ncco;
import com.vonage.jwt.Jwt;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
Expand Down Expand Up @@ -615,4 +616,20 @@ public void saveRecording(String recordingUrl, Path destination) throws IOExcept
}
Files.write(path, binary);
}

/**
* Utility method for verifying whether a token was signed by a secret.
* This is mostly useful when using signed callbacks to ensure that the inbound
* data came from Vonage servers. The signature is performed using the SHA-256 HMAC algorithm.
*
* @param jwt The JSON Web Token to verify.
* @param secret The symmetric secret key (HS256) to use for decrypting the token's signature.
*
* @return {@code true} if the token was signed by the secret, {@code false} otherwise.
*
* @since 7.11.0
*/
public static boolean verifySignature(String jwt, String secret) {
return Jwt.verifySignature(jwt, secret);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public String getUri() {

/**
*
* @return Thge content type.
* @return The content type.
*/
@JsonProperty("content-type")
public String getContentType() {
Expand Down
21 changes: 11 additions & 10 deletions src/test/java/com/vonage/client/VonageClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.message.BasicNameValuePair;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
Expand All @@ -43,8 +43,10 @@
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.List;
import java.util.UUID;

public class VonageClientTest {
private static final UUID APPLICATION_ID = UUID.randomUUID();
private final TestUtils testUtils = new TestUtils();

private HttpClient stubHttpClient(int statusCode, String content) throws Exception {
Expand Down Expand Up @@ -107,9 +109,8 @@ public void testConstructVonageClientWithDifferentHash() throws Exception {
public void testGenerateJwt() throws Exception {
byte[] privateKeyBytes = testUtils.loadKey("test/keys/application_key");
VonageClient client = VonageClient.builder()
.applicationId("application-id")
.privateKeyContents(privateKeyBytes)
.build();
.applicationId(APPLICATION_ID).build();

String constructedToken = client.generateJwt();

Expand All @@ -118,9 +119,9 @@ public void testGenerateJwt() throws Exception {
KeyFactory kf = KeyFactory.getInstance("RSA");
PublicKey key = kf.generatePublic(spec);

Claims claims = Jwts.parser().setSigningKey(key).parseClaimsJws(constructedToken).getBody();
Claims claims = Jwts.parser().verifyWith(key).build().parseSignedClaims(constructedToken).getPayload();

assertEquals("application-id", claims.get("application_id"));
assertEquals(APPLICATION_ID.toString(), claims.get("application_id"));
}

@Test
Expand All @@ -147,7 +148,7 @@ public void testSoloSignatureSecret() {
@Test
public void testSoloApplicationId() {
assertThrows(VonageClientCreationException.class, () ->
VonageClient.builder().applicationId("app-id").build()
VonageClient.builder().applicationId(APPLICATION_ID).build()
);
}

Expand Down Expand Up @@ -252,7 +253,7 @@ public void testApplicationIdWithCertContentsAsBytes() throws Exception {
byte[] keyBytes = testUtils.loadKey("test/keys/application_key");

VonageClient vonageClient = VonageClient.builder()
.applicationId("app-id")
.applicationId(APPLICATION_ID)
.privateKeyContents(keyBytes)
.build();
AuthCollection authCollection = vonageClient.getHttpWrapper().getAuthCollection();
Expand All @@ -269,7 +270,7 @@ public void testApplicationIdWithCertContentsAsString() throws Exception {
TestUtils testUtils = new TestUtils();
String key = new String(testUtils.loadKey("test/keys/application_key"));

VonageClient vonageClient = VonageClient.builder().applicationId("app-id").privateKeyContents(key).build();
VonageClient vonageClient = VonageClient.builder().applicationId(APPLICATION_ID).privateKeyContents(key).build();
AuthCollection authCollection = vonageClient.getHttpWrapper().getAuthCollection();

RequestBuilder requestBuilder = RequestBuilder.get();
Expand All @@ -282,7 +283,7 @@ public void testApplicationIdWithCertContentsAsString() throws Exception {
@Test
public void testApplicationIdWithCertPath() throws Exception {
VonageClient vonageClient = VonageClient.builder()
.applicationId("app-id")
.applicationId(APPLICATION_ID)
.privateKeyPath(Paths.get(getClass().getResource("test/keys/application_key").toURI()))
.build();
AuthCollection authCollection = vonageClient.getHttpWrapper().getAuthCollection();
Expand All @@ -297,7 +298,7 @@ public void testApplicationIdWithCertPath() throws Exception {
@Test
public void testApplicationIdWithCertPathAsString() throws Exception {
VonageClient vonageClient = VonageClient.builder()
.applicationId("app-id")
.applicationId(APPLICATION_ID)
.privateKeyPath(Paths.get(getClass().getResource("test/keys/application_key").toURI()).toString())
.build();
AuthCollection authCollection = vonageClient.getHttpWrapper().getAuthCollection();
Expand Down
21 changes: 19 additions & 2 deletions src/test/java/com/vonage/client/messages/MessagesClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
*/
package com.vonage.client.messages;

import com.vonage.client.*;
import com.vonage.client.ClientTest;
import com.vonage.client.DynamicEndpointTestSpec;
import com.vonage.client.RestEndpoint;
import com.vonage.client.VonageApiResponseException;
import com.vonage.client.auth.AuthMethod;
import com.vonage.client.auth.JWTAuthMethod;
import com.vonage.client.auth.TokenAuthMethod;
Expand All @@ -30,8 +33,8 @@
import com.vonage.client.messages.viber.ViberTextRequest;
import com.vonage.client.messages.viber.ViberVideoRequest;
import com.vonage.client.messages.whatsapp.*;
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.*;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
Expand Down Expand Up @@ -73,6 +76,20 @@ void assertException(int statusCode, String json) throws Exception {
);
}

@Test
public void testVerifySignature() {
String secret = "XsA09z2MhUxYcdbXaUX3aTT7TzGmnCLfkdILf0NIyC9hN9criTEUdlI3OZ5hRjR";
String header = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
String payload = "eyJzdWIiOiJTaW5hIiwibmFtZSI6IkphdmFfU0RLIiwiaWF0IjoxNjk4NjgwMzkyfQ";
String signature = "4qJpi46NSYURiLI1xoLIfGRygA8IUI2QSG9P2Kus1Oo";
String token = header + '.' + payload + '.' + signature;
assertTrue(MessagesClient.verifySignature(token, secret));
String badToken = header + '.' + payload + '.' + "XsaXHXqxe2kfIbPy-JH2J6hfbHnEv8jdWsOhEuvzU98";
assertFalse(MessagesClient.verifySignature(badToken, secret));
assertThrows(NullPointerException.class, () -> MessagesClient.verifySignature(null, secret));
assertThrows(NullPointerException.class, () -> MessagesClient.verifySignature(token, null));
}

@Test
public void test429Response() throws Exception {
assertException(429, "{\n" +
Expand Down
5 changes: 3 additions & 2 deletions src/test/java/com/vonage/client/redact/RedactClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,16 @@ public void testSuccessfulResponse() throws Exception {
stubResponseAndRun(204, () -> client.redactTransaction(redactRequest));
stubResponse(204);
client.redactTransaction(redactRequest.getId(), redactRequest.getProduct(), redactRequest.getType());
stubResponseAndRun(204, () -> client.redactTransaction("test-id", RedactRequest.Product.VERIFY));
}

@Test
public void testInvalidRedactRequests() {
assertThrows(IllegalArgumentException.class, () -> client.redactTransaction(
new RedactRequest("test-id", RedactRequest.Product.SMS)
"test-id", RedactRequest.Product.SMS
));
assertThrows(IllegalArgumentException.class, () -> client.redactTransaction(
new RedactRequest("test-id", null)
"test-id", null
));
assertThrows(IllegalArgumentException.class, () -> client.redactTransaction(
new RedactRequest(null, RedactRequest.Product.SMS)
Expand Down
14 changes: 14 additions & 0 deletions src/test/java/com/vonage/client/voice/VoiceClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ public VoiceClientTest() {
client = new VoiceClient(wrapper);
}

@Test
public void testVerifySignature() {
String secret = "XsA09z2MhUxYcdbXaUX3aTT7TzGmnCLfkdILf0NIyC9hN9criTEUdlI3OZ5hRjR";
String header = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
String payload = "eyJzdWIiOiJTaW5hIiwibmFtZSI6IkphdmFfU0RLIiwiaWF0IjoxNjk4NjgwMzkyfQ";
String signature = "4qJpi46NSYURiLI1xoLIfGRygA8IUI2QSG9P2Kus1Oo";
String token = header + '.' + payload + '.' + signature;
assertTrue(VoiceClient.verifySignature(token, secret));
String badToken = header + '.' + payload + '.' + "XsaXHXqxe2kfIbPy-JH2J6hfbHnEv8jdWsOhEuvzU98";
assertFalse(VoiceClient.verifySignature(badToken, secret));
assertThrows(NullPointerException.class, () -> VoiceClient.verifySignature(null, secret));
assertThrows(NullPointerException.class, () -> VoiceClient.verifySignature(token, null));
}

@Test
public void testCreateCall() throws Exception {
stubResponse(200,
Expand Down

0 comments on commit 80a4cdc

Please sign in to comment.