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

feat: Add Network Authentication & refactor AuthMethod #530

Merged
merged 14 commits into from
May 24, 2024
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/).

# [8.8.0] - 2024-0?-??
- Refactored auth
- Split `TokenAuthMethod` into `ApiKeyQueryParamsAuthMethod` and `ApiKeyHeaderAuthMethod`
- `AuthMethod#apply(RequestBuilder)` removed to decouple from Apache HttpClient
- Refactored `RequestSigning`
- Deprecated methods that use `NameValuePair` and modify the input collection
- Improved testing of auth method application
- Added Vonage Network Auth API (intended for internal use only)
- Added CAMARA SIM Swap API

# [8.7.0] - 2024-05-16
- Added missing supported languages to `TextToSpeechLanguage` enum
- Added `ttl` field to outbound MMS messages
Expand Down
6 changes: 3 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ repositories {
dependencies {
def jacksonVersion = '2.17.1'
def httpclientVersion = '4.5.14'
def junitVersion = '5.11.0-M1'
def junitVersion = '5.11.0-M2'

implementation 'commons-codec:commons-codec:1.17.0'
implementation 'org.apache.commons:commons-lang3:3.14.0'
Expand All @@ -33,8 +33,8 @@ dependencies {

testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
testImplementation "org.mockito:mockito-core:5.12.0"
testImplementation "jakarta.servlet:jakarta.servlet-api:4.0.4"
testImplementation 'org.mockito:mockito-core:5.12.0'
testImplementation 'jakarta.servlet:jakarta.servlet-api:4.0.4'
}

java {
Expand Down
60 changes: 25 additions & 35 deletions src/main/java/com/vonage/client/AbstractMethod.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@
*/
package com.vonage.client;

import com.vonage.client.auth.AuthMethod;
import com.vonage.client.auth.JWTAuthMethod;
import com.vonage.client.auth.SignatureAuthMethod;
import com.vonage.client.auth.TokenAuthMethod;
import com.vonage.client.auth.*;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
Expand All @@ -27,7 +24,7 @@
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.AbstractMap;
import java.util.Set;
import java.util.stream.Collectors;

Expand All @@ -50,9 +47,7 @@ public abstract class AbstractMethod<RequestT, ResultT> implements RestEndpoint<
}

protected static final BasicResponseHandler basicResponseHandler = new BasicResponseHandler();

protected final HttpWrapper httpWrapper;
private Set<Class<? extends AuthMethod>> acceptable;

public AbstractMethod(HttpWrapper httpWrapper) {
this.httpWrapper = httpWrapper;
Expand Down Expand Up @@ -112,39 +107,31 @@ public ResultT execute(RequestT request) throws VonageResponseParseException, Vo
* @throws VonageClientException If no appropriate {@link AuthMethod} is available
*/
protected RequestBuilder applyAuth(RequestBuilder request) throws VonageClientException {
return getAuthMethod(getAcceptableAuthMethods()).apply(request);
}

/**
* Utility method for obtaining an appropriate {@link AuthMethod} for this call.
*
* @param acceptableAuthMethods an array of classes, representing authentication methods that are acceptable for
* this endpoint
*
* @return An AuthMethod created from one of the provided acceptableAuthMethods.
*
* @throws VonageClientException If no AuthMethod is available from the provided array of acceptableAuthMethods.
*/
@SuppressWarnings("unchecked")
protected AuthMethod getAuthMethod(Class<?>[] acceptableAuthMethods) throws VonageClientException {
if (acceptable == null) {
acceptable = Arrays.stream(acceptableAuthMethods)
.filter(AuthMethod.class::isAssignableFrom)
.map(c -> (Class<? extends AuthMethod>) c)
.collect(Collectors.toSet());
AuthMethod am = getAuthMethod();
if (am instanceof HeaderAuthMethod) {
request.setHeader("Authorization", ((HeaderAuthMethod) am).getHeaderValue());
}
else if (am instanceof QueryParamsAuthMethod) {
RequestQueryParams qp = am instanceof ApiKeyQueryParamsAuthMethod ? null : normalRequestParams(request);
((QueryParamsAuthMethod) am).getAuthParams(qp).forEach(request::addParameter);
}
return request;
}

return httpWrapper.getAuthCollection().getAcceptableAuthMethod(acceptable);
static RequestQueryParams normalRequestParams(RequestBuilder request) {
return request.getParameters().stream()
.map(nvp -> new AbstractMap.SimpleEntry<>(nvp.getName(), nvp.getValue()))
.collect(Collectors.toCollection(RequestQueryParams::new));
}

/**
* Call {@linkplain #getAuthMethod(Class[])} with {@linkplain #getAcceptableAuthMethods()}.
* Gets the highest priority available authentication method according to its sort key.
*
* @return An AuthMethod created from the accepted auth methods.
* @throws VonageUnexpectedException If no AuthMethod is available.
*/
protected AuthMethod getAuthMethod() throws VonageUnexpectedException {
return getAuthMethod(getAcceptableAuthMethods());
return httpWrapper.getAuthCollection().getAcceptableAuthMethod(getAcceptableAuthMethods());
}

/**
Expand All @@ -159,16 +146,19 @@ public String getApplicationIdOrApiKey() throws VonageUnexpectedException {
if (am instanceof JWTAuthMethod) {
return ((JWTAuthMethod) am).getApplicationId();
}
if (am instanceof TokenAuthMethod) {
return ((TokenAuthMethod) am).getApiKey();
if (am instanceof ApiKeyHeaderAuthMethod) {
return ((ApiKeyHeaderAuthMethod) am).getApiKey();
}
if (am instanceof ApiKeyQueryParamsAuthMethod) {
return ((ApiKeyQueryParamsAuthMethod) am).getApiKey();
}
if (am instanceof SignatureAuthMethod) {
return ((SignatureAuthMethod) am).getApiKey();
}
throw new IllegalStateException(am.getClass().getSimpleName() + " does not have API key.");
}

protected abstract Class<?>[] getAcceptableAuthMethods();
protected abstract Set<Class<? extends AuthMethod>> getAcceptableAuthMethods();

/**
* Construct and return a RequestBuilder instance from the provided request.
Expand All @@ -179,7 +169,7 @@ public String getApplicationIdOrApiKey() throws VonageUnexpectedException {
*
* @throws UnsupportedEncodingException if UTF-8 encoding is not supported by the JVM
*/
public abstract RequestBuilder makeRequest(RequestT request) throws UnsupportedEncodingException;
protected abstract RequestBuilder makeRequest(RequestT request) throws UnsupportedEncodingException;

/**
* Construct a ResultT representing the contents of the HTTP response returned from the Vonage Voice API.
Expand All @@ -190,5 +180,5 @@ public String getApplicationIdOrApiKey() throws VonageUnexpectedException {
*
* @throws IOException if a problem occurs parsing the response
*/
public abstract ResultT parseResponse(HttpResponse response) throws IOException;
protected abstract ResultT parseResponse(HttpResponse response) throws IOException;
}
64 changes: 22 additions & 42 deletions src/main/java/com/vonage/client/DynamicEndpoint.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Consumer;

Expand All @@ -41,8 +38,7 @@
*/
@SuppressWarnings("unchecked")
public class DynamicEndpoint<T, R> extends AbstractMethod<T, R> {
protected boolean applyBasicAuth;
protected Collection<Class<? extends AuthMethod>> authMethods;
protected Set<Class<? extends AuthMethod>> authMethods;
protected String contentType, accept;
protected HttpMethod requestMethod;
protected BiFunction<DynamicEndpoint<T, R>, ? super T, String> pathGetter;
Expand All @@ -52,7 +48,6 @@ public class DynamicEndpoint<T, R> extends AbstractMethod<T, R> {

protected DynamicEndpoint(Builder<T, R> builder) {
super(builder.wrapper);
applyBasicAuth = builder.applyBasicAuth;
authMethods = builder.authMethods;
requestMethod = builder.requestMethod;
pathGetter = builder.pathGetter;
Expand Down Expand Up @@ -88,9 +83,8 @@ public static <T, R> Builder<T, R> builder(Class<R> responseType) {

public static final class Builder<T, R> {
private final Class<R> responseType;
private Collection<Class<? extends AuthMethod>> authMethods;
private Set<Class<? extends AuthMethod>> authMethods;
private HttpWrapper wrapper;
private boolean applyBasicAuth = false;
private String contentType, accept;
private HttpMethod requestMethod;
private BiFunction<DynamicEndpoint<T, R>, ? super T, String> pathGetter;
Expand All @@ -116,7 +110,7 @@ public Builder<T, R> pathGetter(BiFunction<DynamicEndpoint<T, R>, T, String> pat
}

public Builder<T, R> authMethod(Class<? extends AuthMethod> primary, Class<? extends AuthMethod>... others) {
authMethods = new ArrayList<>(2);
authMethods = new LinkedHashSet<>(2);
authMethods.add(Objects.requireNonNull(primary, "Primary auth method cannot be null"));
if (others != null) {
for (Class<? extends AuthMethod> amc : others) {
Expand All @@ -133,6 +127,10 @@ public Builder<T, R> responseExceptionType(Class<? extends RuntimeException> res
return this;
}

public Builder<T, R> urlFormEncodedContentType(boolean formEncoded) {
return contentTypeHeader(formEncoded ? "application/x-www-form-urlencoded" : null);
}

public Builder<T, R> contentTypeHeader(String contentType) {
this.contentType = contentType;
return this;
Expand All @@ -143,15 +141,6 @@ public Builder<T, R> acceptHeader(String accept) {
return this;
}

public Builder<T, R> applyAsBasicAuth() {
return applyAsBasicAuth(true);
}

public Builder<T, R> applyAsBasicAuth(boolean applyBasicAuth) {
this.applyBasicAuth = applyBasicAuth;
return this;
}

public DynamicEndpoint<T, R> build() {
return new DynamicEndpoint<>(this);
}
Expand All @@ -169,40 +158,31 @@ static RequestBuilder createRequestBuilderFromRequestMethod(HttpMethod requestMe
}

@Override
protected final Class<?>[] getAcceptableAuthMethods() {
Class<?>[] emptyArray = new Class<?>[0];
return authMethods != null ? authMethods.toArray(emptyArray) : emptyArray;
}

@Override
protected final RequestBuilder applyAuth(RequestBuilder request) throws VonageClientException {
if (authMethods == null || authMethods.isEmpty()) {
return request;
}
else if (applyBasicAuth) {
return getAuthMethod(getAcceptableAuthMethods()).applyAsBasicAuth(request);
}
else {
return super.applyAuth(request);
}
protected final Set<Class<? extends AuthMethod>> getAcceptableAuthMethods() {
return authMethods;
}

private boolean isJsonableArrayResponse() {
return responseType.isArray() && Jsonable.class.isAssignableFrom(responseType.getComponentType());
}

private String getRequestHeader(T requestBody) {
if (contentType != null)
if (contentType != null) {
return contentType;
else if (requestBody instanceof Jsonable)
return "application/json";
else if (requestBody instanceof BinaryRequest)
}
else if (requestBody instanceof Jsonable) {
return ContentType.APPLICATION_JSON.getMimeType();
}
else if (requestBody instanceof BinaryRequest) {
return ((BinaryRequest) requestBody).getContentType();
else return null;
}
else {
return null;
}
}

@Override
public final RequestBuilder makeRequest(T requestBody) {
protected final RequestBuilder makeRequest(T requestBody) {
if (requestBody instanceof Jsonable && responseType.isAssignableFrom(requestBody.getClass())) {
cachedRequestBody = requestBody;
}
Expand Down Expand Up @@ -247,7 +227,7 @@ else if (requestBody instanceof byte[]) {
}

@Override
public final R parseResponse(HttpResponse response) throws IOException {
protected final R parseResponse(HttpResponse response) throws IOException {
int statusCode = response.getStatusLine().getStatusCode();
try {
if (statusCode >= 200 && statusCode < 300) {
Expand Down
20 changes: 10 additions & 10 deletions src/main/java/com/vonage/client/HttpWrapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,31 +29,31 @@
* Internal class that holds available authentication methods and a shared HttpClient.
*/
public class HttpWrapper {
private static final String CLIENT_NAME = "vonage-java-sdk";
private static final String CLIENT_VERSION = "8.7.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);
private static final String
CLIENT_NAME = "vonage-java-sdk",
CLIENT_VERSION = "8.7.0",
JAVA_VERSION = System.getProperty("java.version"),
USER_AGENT = String.format("%s/%s java/%s", CLIENT_NAME, CLIENT_VERSION, JAVA_VERSION);

private AuthCollection authCollection;
private HttpClient httpClient;
private HttpConfig httpConfig;

public HttpWrapper(AuthCollection authCollection) {
this(HttpConfig.builder().build(), authCollection);
}

public HttpWrapper(HttpConfig httpConfig, AuthCollection authCollection) {
this.authCollection = authCollection;
this.httpConfig = httpConfig;
}

public HttpWrapper(AuthCollection authCollection) {
this(HttpConfig.builder().build(), authCollection);
}

public HttpWrapper(AuthMethod... authMethods) {
this(HttpConfig.builder().build(), authMethods);
}

public HttpWrapper(HttpConfig httpConfig, AuthMethod... authMethods) {
this(new AuthCollection(authMethods));
this.httpConfig = httpConfig;
this(httpConfig, new AuthCollection(authMethods));
}

public HttpClient getHttpClient() {
Expand Down
Loading
Loading