Skip to content
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

DEVEXP-201: Verification webhooks #26

Merged
merged 9 commits into from
Dec 12, 2023
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Java CI with Maven
name: Samples Compilation

on: [push]

Expand All @@ -15,10 +15,12 @@ jobs:
java-version: '21'
distribution: 'temurin'
cache: maven
- name: Build Services samples with Maven
run: cd sample-app; mvn -B clean package
- name: Build Webhooks samples with Maven
run: cd sample-app; mvn -B -f pom-webhooks.xml clean package
- name: Building
run: |
mvn clean verify install -DskipTests=true -Dspotless.apply.skip=true
cd sample-app
mvn -B clean package
mvn -B -f pom-webhooks.xml clean package
# Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive
- name: Update dependency graph
uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Java SDK
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://github.com/sinch/sinch-sdk-python/blob/main/LICENSE)


[![Python 3.8](https://img.shields.io/badge/Java-8-blue.svg)](https://docs.oracle.com/javase/8)
JPPortier marked this conversation as resolved.
Show resolved Hide resolved
[![Java 8](https://img.shields.io/badge/Java-8-blue.svg)](https://docs.oracle.com/javase/8)


</h1>
Expand All @@ -34,7 +34,6 @@ For more in depth version of the Sinch APIs, please refer to the official develo

- JDK 8 or later
- [Maven](https://maven.apache.org/)
- [Maven Central](https://mvnrepository.com/artifact/com.sinch.sdk/sinch-java-sdk)
- [Sinch account](https://dashboard.sinch.com)

## Installation
Expand Down
3 changes: 2 additions & 1 deletion client/src/main/com/sinch/sdk/SinchClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
public class SinchClient {

private static final String DEFAULT_PROPERTIES_FILE_NAME = "/config-default.properties";
private static final String VERSION_PROPERTIES_FILE_NAME = "/version.properties";
private static final String VERSION_PROPERTIES_FILE_NAME = "/version.properties";

private static final String OAUTH_URL_KEY = "oauth-url";
private static final String NUMBERS_SERVER_KEY = "numbers-server";
Expand Down Expand Up @@ -224,4 +224,5 @@ private String formatAuxiliaryFlag(String auxiliaryFlag) {
}
return String.join(",", values);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public void resetToken() {

@Override
public Collection<Pair<String, String>> getAuthorizationHeaders(
String method, String httpContentType, String path, String body) {
String timestamp, String method, String httpContentType, String path, String body) {
String key = keyId == null ? "" : keyId;
String secret = keySecret == null ? "" : keySecret;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public void resetToken() {

@Override
public Collection<Pair<String, String>> getAuthorizationHeaders(
String method, String httpContentType, String path, String body) {
String timestamp, String method, String httpContentType, String path, String body) {

if (token == null) {
refreshToken();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
Expand Down Expand Up @@ -42,7 +41,7 @@ public void resetToken() {

@Override
public Collection<Pair<String, String>> getAuthorizationHeaders(
String method, String httpContentType, String path, String body) {
String timestamp, String method, String httpContentType, String path, String body) {

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks
Fixed


String decodePath;
try {
Expand All @@ -52,15 +51,14 @@ public Collection<Pair<String, String>> getAuthorizationHeaders(
}
// see
// https://developers.sinch.com/docs/verification/api-reference/authentication/signed-request/
Instant timestamp = Instant.now();
String bodyMD5Hash = getBodyMD5Hash(body);
String stringToSign = getSignature(method, bodyMD5Hash, httpContentType, timestamp, decodePath);
String encoded = encode(stringToSign);
String key = this.key == null ? "" : this.key;

return Arrays.asList(
new Pair<>("Authorization", AUTH_KEYWORD + " " + key + ":" + encoded),
new Pair<>(XTIMESTAMP_HEADER, timestamp.toString()));
new Pair<>(XTIMESTAMP_HEADER, timestamp));
}

private String getBodyMD5Hash(String body) {
Expand All @@ -78,13 +76,13 @@ private String getBodyMD5Hash(String body) {
}

private String getSignature(
String method, String bodyMD5Hash, String httpContentType, Instant timestamp, String path) {
String method, String bodyMD5Hash, String httpContentType, String timestamp, String path) {
return String.join(
"\n",
method,
bodyMD5Hash,
null != httpContentType ? httpContentType : "",
XTIMESTAMP_HEADER + ":" + timestamp.toString(),
XTIMESTAMP_HEADER + ":" + timestamp,
path);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,26 @@
public interface VerificationService {

/**
* Use application secret in place of unified configuration for authentication (see Sinch
* dashboard for details) These credentials are related to Verification Apps
* Verifications Service instance
*
* @param key see <a href="https://dashboard.sinch.com/verification/apps">dashboard</a>
* @param secret see <a href="https://dashboard.sinch.com/verification/apps">dashboard</a>
* @return service instance for project
* @since 1.0
*/
VerificationService setApplicationCredentials(String key, String secret);
VerificationsService verifications();

/**
* Verifications Service instance
* Status Service instance
*
* @return service instance for project
* @since 1.0
*/
VerificationsService verifications();
StatusService status();

/**
* Status Service instance
* Webhooks helpers instance
*
* @return service instance for project
* @return instance service related to webhooks helpers
* @since 1.0
*/
StatusService status();
WebHooksService webhooks();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.sinch.sdk.domains.verification;

import com.sinch.sdk.core.exceptions.ApiMappingException;
import com.sinch.sdk.domains.verification.models.webhooks.VerificationEvent;
import com.sinch.sdk.domains.verification.models.webhooks.VerificationResponse;
import java.util.Map;

/**
* Webhooks service
* <p>
* Callback events are used to authorize and manage your verification requests and return
* verification results.
* <p>
* see <a
* href="https://developers.sinch.com/docs/verification/api-reference/verification/tag/Verification-callbacks/#tag/Verification-callbacks/paths/VerificationRequestEvent/post">https://developers.sinch.com/docs/verification/api-reference/verification/tag/Verification-callbacks/#tag/Verification-callbacks/paths/VerificationRequestEvent/post</a>
*
* @since 1.0
*/
public interface WebHooksService {

/**
* The Sinch Platform can initiate callback requests to a URL you define (Callback URL) on request
* and result events. All callback requests are signed using your Application key and secret pair
* found on your dashboard. The signature is included in the Authorization header of the request
* <p>By using following function, you can ensure authentication according to received payload
* from your backend</p>
*
* @param method The HTTP method used ot handle the callback
* @param path The path to you backend endpoint used for callback
* @param headers Received headers
* @param jsonPayload Received payload
* @return Is authentication is validated (true) or not (false)
*
* see <a href="https://developers.sinch.com/docs/verification/api-reference/authentication/callback-signed-request">https://developers.sinch.com/docs/verification/api-reference/authentication/callback-signed-request</a>
* @since 1.0
*/
boolean checkAuthentication(

Choose a reason for hiding this comment

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

I'm wondering if it makes sense to have this method in this service, knowing that the voice domain will define the same

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right.
When Voice will arise, it seems refactoring will have sense (something synch with what we have to do related to Application spreading across Voice/Verification)

String method, String path, Map<String, String> headers, String jsonPayload);

/**
* This function can be called to deserialize received payload onto callback onto proper java
* verification event class
*
* @param jsonPayload Received payload to be deserialized
* @return The verification event instance class
*
* see <a href="https://developers.sinch.com/docs/verification/api-reference/verification/tag/Verification-callbacks/">https://developers.sinch.com/docs/verification/api-reference/verification/tag/Verification-callbacks/</a>
* @since 1.0
*/
VerificationEvent unserializeVerificationEvent(String jsonPayload) throws ApiMappingException;

/**
* This function can be called to serialize a verification response to be send as JSON
*
* @param response The response to be serialized
* @return The JSON string to be sent
*
* see <a href="https://developers.sinch.com/docs/verification/api-reference/verification/tag/Verification-callbacks/">https://developers.sinch.com/docs/verification/api-reference/verification/tag/Verification-callbacks/</a>
* @since 1.0
*/
String serializeVerificationResponse(VerificationResponse response) throws ApiMappingException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,26 @@
import com.sinch.sdk.domains.verification.models.VerificationReport;
import com.sinch.sdk.models.Configuration;
import java.util.Map;
import java.util.function.Supplier;

public class StatusService implements com.sinch.sdk.domains.verification.StatusService {

private final Configuration configuration;
private final HttpClient httpClient;
private final Supplier<Map<String, AuthManager>> authManagerSupplier;
private final QueryVerificationsApi api;

public StatusService(
Configuration configuration,
HttpClient httpClient,
Supplier<Map<String, AuthManager>> authManagerSupplier) {
this.configuration = configuration;
this.httpClient = httpClient;
this.authManagerSupplier = authManagerSupplier;
}

protected QueryVerificationsApi getApi() {
return new QueryVerificationsApi(
Map<String, AuthManager> authManagers) {
this.api = new QueryVerificationsApi(
httpClient,
configuration.getVerificationServer(),
authManagerSupplier.get(),
authManagers,
new HttpMapper());
}

protected QueryVerificationsApi getApi() {
return this.api;
}

public VerificationReport get(Identity identity, VerificationMethodType method) {
if (!(identity instanceof NumberIdentity)) {
throw new ApiException("Unexpected entity: " + identity);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,52 +7,70 @@
import com.sinch.sdk.core.http.HttpClient;
import com.sinch.sdk.domains.verification.StatusService;
import com.sinch.sdk.domains.verification.VerificationsService;
import com.sinch.sdk.domains.verification.WebHooksService;
import com.sinch.sdk.models.Configuration;
import java.util.AbstractMap;
import java.util.Map;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.Objects;
import java.util.TreeMap;

public class VerificationService implements com.sinch.sdk.domains.verification.VerificationService {

// FIXME: Verification OAS file claim it support "Basic" but miss the "Application" definition
// trick to adapt the mapping of "Basic" keyword to the dedicated "Application" auth manager
private static final String SECURITY_SCHEME_KEYWORD_VERIFICATION = "Basic";
private static final String BASIC_SECURITY_SCHEME_KEYWORD_VERIFICATION = "Basic";

private static final String APPLICATION_SECURITY_SCHEME_KEYWORD_VERIFICATION = "Application";

private final Configuration configuration;
private final HttpClient httpClient;
private VerificationsService verifications;
private StatusService status;
private Map<String, AuthManager> authManagers;
private final Supplier<Map<String, AuthManager>> authManagersSupplier = () -> authManagers;
private WebHooksService webhooks;
private Map<String, AuthManager> clientAuthManagers;
private Map<String, AuthManager> webhooksAuthManagers;

public VerificationService(Configuration configuration, HttpClient httpClient) {

// Currently, we are not supporting unified credentials: ensure application credentials are defined
Objects.requireNonNull(configuration.getApplicationKey(), "'applicationKey' cannot be null");
Objects.requireNonNull(configuration.getApplicationSecret(),
"'applicationSecret' cannot be null");

this.configuration = configuration;
this.httpClient = httpClient;
setApplicationCredentials(configuration.getApplicationKey(),configuration.getApplicationSecret() );
}

public VerificationService setApplicationCredentials(String key, String secret) {
private void setApplicationCredentials(String key, String secret) {

AuthManager basicAuthManager = new BasicAuthManager(key, secret);
AuthManager applicationAuthManager = new VerificationApplicationAuthManager(key, secret);

AuthManager authManager;
boolean useApplicationAuth = true;
if (useApplicationAuth) {
authManager = new VerificationApplicationAuthManager(key, secret);
} else {
authManager = new BasicAuthManager(key, secret);
}
authManagers =
Stream.of(new AbstractMap.SimpleEntry<>(SECURITY_SCHEME_KEYWORD_VERIFICATION, authManager))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
return this;
// to handle request from client we can only have "Basic" keyword behind the auth managers
// because of the OAS file only contains it; so we need to trick the application auth manager
// hidden behind the "Basic" keyword
// we need both auth manager to handle webhooks because of customer will choose from his
// dashboard which scheme to be used
clientAuthManagers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
clientAuthManagers.put(
BASIC_SECURITY_SCHEME_KEYWORD_VERIFICATION,
useApplicationAuth ? applicationAuthManager : basicAuthManager);

// here we need both auth managers to handle webhooks because we are receiving an Authorization
// header with "Application" keyword
webhooksAuthManagers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
webhooksAuthManagers.put(BASIC_SECURITY_SCHEME_KEYWORD_VERIFICATION, basicAuthManager);
webhooksAuthManagers.put(
APPLICATION_SECURITY_SCHEME_KEYWORD_VERIFICATION, applicationAuthManager);
}

public VerificationsService verifications() {
if (null == this.verifications) {
checkCredentials();
this.verifications =
new com.sinch.sdk.domains.verification.adapters.VerificationsService(
configuration, httpClient, authManagersSupplier);
configuration, httpClient, clientAuthManagers);
}
return this.verifications;
}
Expand All @@ -62,13 +80,23 @@ public StatusService status() {
checkCredentials();
this.status =
new com.sinch.sdk.domains.verification.adapters.StatusService(
configuration, httpClient, authManagersSupplier);
configuration, httpClient, clientAuthManagers);
}
return this.status;
}

public WebHooksService webhooks() {
checkCredentials();
if (null == this.webhooks) {
this.webhooks =
new com.sinch.sdk.domains.verification.adapters.WebHooksService(
webhooksAuthManagers);
}
return this.webhooks;
}

private void checkCredentials() throws ApiAuthException {
if (null == authManagers || authManagers.isEmpty()) {
if (null == clientAuthManagers || clientAuthManagers.isEmpty()) {
throw new ApiAuthException(
String.format(
"Service '%s' cannot be called without defined credentials",
Expand Down
Loading
Loading