Skip to content

Commit

Permalink
Merge pull request #26 from sinch/DEVEXP-201-verification-webhooks
Browse files Browse the repository at this point in the history
DEVEXP-201: Verification webhooks
  • Loading branch information
JPPortier authored Dec 12, 2023
2 parents 5968fbd + b122d13 commit 0b791b3
Show file tree
Hide file tree
Showing 63 changed files with 1,976 additions and 206 deletions.
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)
[![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) {

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(
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

0 comments on commit 0b791b3

Please sign in to comment.