Skip to content

Commit

Permalink
DEVEXP-223: SMS / support service plan ID (#62)
Browse files Browse the repository at this point in the history
* refactor: Move domain's specific credentials onto dedicated classes
* refactor: create dedicated Numbers context
* refactor: create dedicated Verification context
* refactor: create dedicated Voice context
* refactor: create dedicated SMS context
* refactor: Stop consuming configuration across domains and use dedicated credentials/contexts
* refactor: Rename poorly named 'BearerAuthManager' class
* feature: Support SMS service plan ID
* refactor: Use 'uriUUID' against 'credentials.getProjectId()' onto API calls
  • Loading branch information
JPPortier authored Feb 23, 2024
1 parent 2b5e3c2 commit 3e601bb
Show file tree
Hide file tree
Showing 64 changed files with 2,221 additions and 997 deletions.
2 changes: 2 additions & 0 deletions client/resources/config-default.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ oauth-url=https://auth.sinch.com/oauth2/token
numbers-server=https://numbers.api.sinch.com
sms-region=us
sms-server=https://zt.%s.sms.api.sinch.com
sms-server-service-plan=https://%s.sms.api.sinch.com

verification-server=https://verification.api.sinch.com

voice-region=global
Expand Down
139 changes: 96 additions & 43 deletions client/src/main/com/sinch/sdk/SinchClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@
import com.sinch.sdk.domains.voice.VoiceService;
import com.sinch.sdk.http.HttpClientApache;
import com.sinch.sdk.models.Configuration;
import com.sinch.sdk.models.NumbersContext;
import com.sinch.sdk.models.SMSRegion;
import com.sinch.sdk.models.SmsContext;
import com.sinch.sdk.models.VerificationContext;
import com.sinch.sdk.models.VoiceContext;
import com.sinch.sdk.models.VoiceRegion;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import java.util.Properties;
import java.util.logging.Logger;
Expand All @@ -29,6 +33,7 @@ public class SinchClient {
private static final String NUMBERS_SERVER_KEY = "numbers-server";
private static final String SMS_REGION_KEY = "sms-region";
private static final String SMS_SERVER_KEY = "sms-server";
private static final String SMS_SERVER_SERVICE_PLAN_KEY = "sms-server-service-plan";

private static final String VOICE_REGION_KEY = "voice-region";
private static final String VOICE_APPLICATION_MANAGEMENT_SERVER_KEY =
Expand Down Expand Up @@ -83,60 +88,104 @@ public SinchClient(Configuration configuration) {
versionProperties = handlePropertiesFile(VERSION_PROPERTIES_FILE_NAME);
LOGGER.fine(
String.format(
"%s (%s) started with projectId '%s'",
"%s (%s) started",
versionProperties.getProperty(PROJECT_NAME_KEY),
versionProperties.getProperty(PROJECT_VERSION_KEY),
configuration.getProjectId()));
versionProperties.getProperty(PROJECT_VERSION_KEY)));
}

private void handleDefaultNumbersSettings(
Configuration configuration, Properties props, Configuration.Builder builder) {

if (null == configuration.getNumbersUrl() && props.containsKey(NUMBERS_SERVER_KEY)) {
builder.setNumbersUrl(props.getProperty(NUMBERS_SERVER_KEY));
String url = configuration.getNumbersContext().map(NumbersContext::getNumbersUrl).orElse(null);

if (null == url && props.containsKey(NUMBERS_SERVER_KEY)) {
builder.setNumbersContext(
NumbersContext.builder().setNumbersUrl(props.getProperty(NUMBERS_SERVER_KEY)).build());
}
}

private void handleDefaultSmsSettings(
Configuration configuration, Properties props, Configuration.Builder builder) {

if (null == configuration.getSmsUrl() && props.containsKey(SMS_SERVER_KEY)) {
builder.setSmsUrl(props.getProperty(SMS_SERVER_KEY));
String smsUrl = configuration.getSmsContext().map(SmsContext::getSmsUrl).orElse(null);

SMSRegion smsRegion = configuration.getSmsContext().map(SmsContext::getSmsRegion).orElse(null);

// service plan ID activated: use dedicated server
String serverKey =
configuration
.getSmsServicePlanCredentials()
.map(unused -> SMS_SERVER_SERVICE_PLAN_KEY)
.orElse(SMS_SERVER_KEY);
if (null == smsUrl && props.containsKey(serverKey)) {
smsUrl = props.getProperty(serverKey);
}
if (null == configuration.getSmsRegion() && props.containsKey(SMS_REGION_KEY)) {
builder.setSmsRegion(SMSRegion.from(props.getProperty(SMS_REGION_KEY)));

if (null == smsRegion && props.containsKey(SMS_REGION_KEY)) {
smsRegion = SMSRegion.from(props.getProperty(SMS_REGION_KEY));
}

if (null != smsUrl || null != smsRegion) {
builder.setSmsContext(SmsContext.builder().setSmsRegion(smsRegion).setSmsUrl(smsUrl).build());
}
}

private void handleDefaultVerificationSettings(
Configuration configuration, Properties props, Configuration.Builder builder) {

if (null == configuration.getVerificationUrl() && props.containsKey(VERIFICATION_SERVER_KEY)) {
builder.setVerificationUrl(props.getProperty(VERIFICATION_SERVER_KEY));
String url =
configuration
.getVerificationContext()
.map(VerificationContext::getVerificationUrl)
.orElse(null);

if (null == url && props.containsKey(VERIFICATION_SERVER_KEY)) {
builder.setVerificationContext(
VerificationContext.builder()
.setVerificationUrl(props.getProperty(VERIFICATION_SERVER_KEY))
.build());
}
}

private void handleDefaultVoiceSettings(
Configuration configuration, Properties props, Configuration.Builder builder) {
if (null == configuration.getVoiceRegion() && props.containsKey(VOICE_REGION_KEY)) {
builder.setVoiceRegion(VoiceRegion.from(props.getProperty(VOICE_REGION_KEY)));

VoiceRegion region =
configuration.getVoiceContext().map(VoiceContext::getVoiceRegion).orElse(null);

String voiceUrl = configuration.getVoiceContext().map(VoiceContext::getVoiceUrl).orElse(null);

String voiceApplicationManagementUrl =
configuration
.getVoiceContext()
.map(VoiceContext::getVoiceApplicationManagementUrl)
.orElse(null);

// default region to be used ?
if (null == region && props.containsKey(VOICE_REGION_KEY)) {
region = VoiceRegion.from(props.getProperty(VOICE_REGION_KEY));
}

// server is not defined: use the region to set to an existing one and use "global" as a default
// fallback
if (StringUtil.isEmpty(builder.getVoiceUrl())) {
VoiceRegion region =
StringUtil.isEmpty(builder.getVoiceRegion().value())
? VoiceRegion.GLOBAL
: builder.getVoiceRegion();
builder.setVoiceUrl(props.getProperty(String.format("voice-server-%s", region.value())));
if (StringUtil.isEmpty(voiceUrl)) {
VoiceRegion regionForFormat = null == region ? VoiceRegion.GLOBAL : region;
voiceUrl = props.getProperty(String.format("voice-server-%s", regionForFormat.value()));
}

// application management server
if (null == configuration.getVoiceApplicationManagementUrl()
if (StringUtil.isEmpty(voiceApplicationManagementUrl)
&& props.containsKey(VOICE_APPLICATION_MANAGEMENT_SERVER_KEY)) {
builder.setVoiceApplicationMngmtUrl(
props.getProperty(VOICE_APPLICATION_MANAGEMENT_SERVER_KEY));
voiceApplicationManagementUrl = props.getProperty(VOICE_APPLICATION_MANAGEMENT_SERVER_KEY);
}

if (null != region || null != voiceUrl || null != voiceApplicationManagementUrl) {
builder.setVoiceContext(
VoiceContext.builder()
.setVoiceRegion(region)
.setVoiceUrl(voiceUrl)
.setVoiceApplicationMngmtUrl(voiceApplicationManagementUrl)
.build());
}
}

Expand Down Expand Up @@ -212,40 +261,44 @@ public VoiceService voice() {

private void checkConfiguration(Configuration configuration) throws NullPointerException {
Objects.requireNonNull(configuration.getOAuthUrl(), "'oauthUrl' cannot be null");
Objects.requireNonNull(configuration.getNumbersUrl(), "'numbersUrl' cannot be null");
Objects.requireNonNull(configuration.getSmsUrl(), "'smsUrl' cannot be null");
Objects.requireNonNull(configuration.getVerificationUrl(), "'verificationUrl' cannot be null");
}

private NumbersService numbersInit() {
LOGGER.fine(
"Activate numbers API with server='"
+ getConfiguration().getNumbersServer().getUrl()
+ "'");
return new com.sinch.sdk.domains.numbers.adapters.NumbersService(
getConfiguration(), getHttpClient());
getConfiguration().getUnifiedCredentials().orElse(null),
configuration.getNumbersContext().orElse(null),
getHttpClient());
}

private SMSService smsInit() {
LOGGER.fine(
"Activate SMS API with server='" + getConfiguration().getSmsServer().getUrl() + "'");
return new com.sinch.sdk.domains.sms.adapters.SMSService(getConfiguration(), getHttpClient());

return getConfiguration()
.getSmsServicePlanCredentials()
.map(
f ->
new com.sinch.sdk.domains.sms.adapters.SMSService(
f, getConfiguration().getSmsContext().orElse(null), getHttpClient()))
.orElseGet(
() ->
new com.sinch.sdk.domains.sms.adapters.SMSService(
getConfiguration().getUnifiedCredentials().orElse(null),
getConfiguration().getSmsContext().orElse(null),
configuration.getOAuthServer(),
getHttpClient()));
}

private VerificationService verificationInit() {
LOGGER.fine(
"Activate verification API with server='"
+ getConfiguration().getVerificationServer().getUrl()
+ "'");
return new com.sinch.sdk.domains.verification.adapters.VerificationService(
getConfiguration(), getHttpClient());
getConfiguration().getApplicationCredentials().orElse(null),
getConfiguration().getVerificationContext().orElse(null),
getHttpClient());
}

private VoiceService voiceInit() {
LOGGER.fine(
"Activate voice API with server='" + getConfiguration().getVoiceServer().getUrl() + "'");
return new com.sinch.sdk.domains.voice.adapters.VoiceService(
getConfiguration(), getHttpClient());
getConfiguration().getApplicationCredentials().orElse(null),
getConfiguration().getVoiceContext().orElse(null),
getHttpClient());
}

private Properties handlePropertiesFile(String fileName) {
Expand Down Expand Up @@ -289,7 +342,7 @@ private String formatSdkUserAgentHeader(Properties versionProperties) {

private String formatAuxiliaryFlag(String auxiliaryFlag) {

Collection<String> values = Arrays.asList(System.getProperty("java.vendor"));
Collection<String> values = Collections.singletonList(System.getProperty("java.vendor"));

if (!StringUtil.isEmpty(auxiliaryFlag)) {
values.add(auxiliaryFlag);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import com.sinch.sdk.core.http.AuthManager;
import com.sinch.sdk.core.utils.Pair;
import com.sinch.sdk.models.Configuration;
import com.sinch.sdk.models.UnifiedCredentials;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Collection;
Expand All @@ -18,8 +18,8 @@ public class BasicAuthManager implements AuthManager {
private final String keyId;
private final String keySecret;

public BasicAuthManager(Configuration configuration) {
this(configuration.getKeyId(), configuration.getKeySecret());
public BasicAuthManager(UnifiedCredentials credentials) {
this(credentials.getKeyId(), credentials.getKeySecret());
}

public BasicAuthManager(String keyId, String keySecret) {
Expand Down
86 changes: 5 additions & 81 deletions client/src/main/com/sinch/sdk/auth/adapters/BearerAuthManager.java
Original file line number Diff line number Diff line change
@@ -1,57 +1,21 @@
package com.sinch.sdk.auth.adapters;

import com.fasterxml.jackson.core.type.TypeReference;
import com.sinch.sdk.auth.models.BearerAuthResponse;
import com.sinch.sdk.core.exceptions.ApiAuthException;
import com.sinch.sdk.core.http.AuthManager;
import com.sinch.sdk.core.http.HttpClient;
import com.sinch.sdk.core.http.HttpMapper;
import com.sinch.sdk.core.http.HttpMethod;
import com.sinch.sdk.core.http.HttpRequest;
import com.sinch.sdk.core.http.HttpResponse;
import com.sinch.sdk.core.models.ServerConfiguration;
import com.sinch.sdk.core.utils.Pair;
import com.sinch.sdk.models.Configuration;
import java.util.AbstractMap;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class BearerAuthManager implements AuthManager {

public static final String BEARER_EXPIRED_KEYWORD = "expired";
public static final String BEARER_AUTHENTICATE_RESPONSE_HEADER_KEYWORD = "www-authenticate";
private static final Logger LOGGER = Logger.getLogger(BearerAuthManager.class.getName());
private static final String AUTH_KEYWORD = "Bearer";
private static final int maxRefreshAttempt = 5;
private final ServerConfiguration oAuthServer;
private final HttpMapper mapper;
private final HttpClient httpClient;
private final Map<String, AuthManager> authManagers;
private String token;

public BearerAuthManager(Configuration configuration, HttpMapper mapper, HttpClient httpClient) {
this(configuration.getKeyId(), configuration.getKeySecret(), configuration, mapper, httpClient);
}

public BearerAuthManager(
String keyId,
String keySecret,
Configuration configuration,
HttpMapper mapper,
HttpClient httpClient) {
this.oAuthServer = configuration.getOAuthServer();
this.mapper = mapper;
this.httpClient = httpClient;
private static final String AUTH_KEYWORD = "Bearer";
private final String token;

AuthManager basicAuthManager = new BasicAuthManager(keyId, keySecret);
authManagers =
Stream.of(new AbstractMap.SimpleEntry<>(SCHEMA_KEYWORD_BASIC, basicAuthManager))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
public BearerAuthManager(String token) {
this.token = token;
}

public String getSchema() {
Expand All @@ -60,56 +24,16 @@ public String getSchema() {

@Override
public void resetToken() {
token = null;
// no op
}

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

if (token == null) {
refreshToken();
}
return Collections.singletonList(new Pair<>("Authorization", AUTH_KEYWORD + " " + token));
}

private void refreshToken() {

int attempt = 0;
while (attempt < maxRefreshAttempt) {
Optional<String> newValue = getNewToken();
if (newValue.isPresent()) {
token = newValue.get();
return;
}
attempt++;
}
throw new ApiAuthException("Unable to get new token");
}

private Optional<String> getNewToken() {

LOGGER.fine("Refreshing OAuth token");
HttpRequest request =
new HttpRequest(
null,
HttpMethod.POST,
Collections.emptyList(),
"grant_type=client_credentials",
null,
null,
Collections.singletonList("application/x-www-form-urlencoded"),
Collections.singletonList(SCHEMA_KEYWORD_BASIC));
try {
HttpResponse httpResponse = httpClient.invokeAPI(oAuthServer, authManagers, request);
BearerAuthResponse authResponse =
mapper.deserialize(httpResponse, new TypeReference<BearerAuthResponse>() {});
return Optional.ofNullable(authResponse.getAccessToken());
} catch (Exception e) {
return Optional.empty();
}
}

public boolean validateAuthenticatedRequest(
String method, String path, Map<String, String> headers, String jsonPayload) {
LOGGER.severe("checkAuthentication not implemented");
Expand Down
Loading

0 comments on commit 3e601bb

Please sign in to comment.