diff --git a/pom.xml b/pom.xml
index 2e86d53bf6..0fcd999259 100644
--- a/pom.xml
+++ b/pom.xml
@@ -315,6 +315,18 @@
1.10.19
test
+
+ com.auth0
+ java-jwt
+ 4.4.0
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+
+
diff --git a/src/main/java/com/twilio/TwilioOauth.java b/src/main/java/com/twilio/TwilioOauth.java
new file mode 100644
index 0000000000..b6cf2fb446
--- /dev/null
+++ b/src/main/java/com/twilio/TwilioOauth.java
@@ -0,0 +1,77 @@
+package com.twilio;
+
+import com.twilio.exception.AuthenticationException;
+import com.twilio.http.TwilioOAuthRestClient;
+import lombok.Getter;
+
+import java.util.List;
+
+public class TwilioOauth {
+ public static final String JAVA_VERSION = System.getProperty("java.version");
+ public static final String OS_NAME = System.getProperty("os.name");
+ public static final String OS_ARCH = System.getProperty("os.arch");
+ private static String clientId = System.getenv("TWILIO_CLIENT_ID");
+ private static String clientSecret = System.getenv("TWILIO_CLIENT_SECRET");
+ @Getter
+ private static List userAgentExtensions;
+ private static String region = System.getenv("TWILIO_REGION");
+ private static String edge = System.getenv("TWILIO_EDGE");
+
+ private static volatile TwilioOAuthRestClient oAuthRestClient;
+
+ private TwilioOauth() {
+ }
+ public static synchronized void init(final String clientId, final String clientSecret) {
+ TwilioOauth.setClientId(clientId);
+ TwilioOauth.setClientSecret(clientSecret);
+ }
+
+ public static synchronized void setClientId(final String clientId) {
+ if (clientId == null) {
+ throw new AuthenticationException("Client Id can not be null");
+ }
+
+ TwilioOauth.clientId = clientId;
+ }
+
+ public static synchronized void setClientSecret(final String clientSecret) {
+ if (clientSecret == null) {
+ throw new AuthenticationException("Client Secret can not be null");
+ }
+
+ TwilioOauth.clientSecret = clientSecret;
+ }
+
+ public static TwilioOAuthRestClient getRestClient() {
+ if (TwilioOauth.oAuthRestClient == null) {
+ synchronized (TwilioOauth.class) {
+ if (TwilioOauth.oAuthRestClient == null) {
+ TwilioOauth.oAuthRestClient = buildOAuthRestClient();
+ }
+ }
+ }
+
+ return TwilioOauth.oAuthRestClient;
+ }
+
+ private static TwilioOAuthRestClient buildOAuthRestClient() {
+ if (TwilioOauth.clientId == null || TwilioOauth.clientSecret == null) {
+ throw new AuthenticationException(
+ "TwilioOAuthRestClient was used before ClientId and ClientSecret were set, please call TwilioOauth.init()"
+ );
+ }
+
+ TwilioOAuthRestClient.Builder builder = new TwilioOAuthRestClient.Builder(TwilioOauth.clientId, TwilioOauth.clientSecret);
+
+ if (userAgentExtensions != null) {
+ builder.userAgentExtensions(TwilioOauth.userAgentExtensions);
+ }
+
+ builder.region(TwilioOauth.region);
+ builder.edge(TwilioOauth.edge);
+
+ return builder.build();
+ }
+
+
+}
diff --git a/src/main/java/com/twilio/http/BearerTokenRequest.java b/src/main/java/com/twilio/http/BearerTokenRequest.java
new file mode 100644
index 0000000000..5302975af8
--- /dev/null
+++ b/src/main/java/com/twilio/http/BearerTokenRequest.java
@@ -0,0 +1,48 @@
+package com.twilio.http;
+
+public class BearerTokenRequest extends Request {
+
+ protected static final String DEFAULT_REGION = "us1";
+ public static final String QUERY_STRING_DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";
+ public static final String QUERY_STRING_DATE_FORMAT = "yyyy-MM-dd";
+
+ private String bearerToken;
+
+ /**
+ * Create a new API request.
+ *
+ * @param method HTTP method
+ * @param url url of request
+ */
+ public BearerTokenRequest(final HttpMethod method, final String url) {
+ super(method, url);
+ }
+
+ /**
+ * Create a new API request.
+ *
+ * @param method HTTP method
+ * @param domain Twilio domain
+ * @param uri uri of request
+ */
+ public BearerTokenRequest(final HttpMethod method, final String domain, final String uri) {
+ this(method, domain, uri, null);
+ }
+
+ /**
+ * Create a new API request.
+ *
+ * @param method HTTP Method
+ * @param domain Twilio domain
+ * @param uri uri of request
+ * @param region region to make request
+ */
+ public BearerTokenRequest(
+ final HttpMethod method,
+ final String domain,
+ final String uri,
+ final String region
+ ) {
+ super(method, domain, uri, region);
+ }
+}
diff --git a/src/main/java/com/twilio/http/OAuthHttpClient.java b/src/main/java/com/twilio/http/OAuthHttpClient.java
new file mode 100644
index 0000000000..a6e1426dc1
--- /dev/null
+++ b/src/main/java/com/twilio/http/OAuthHttpClient.java
@@ -0,0 +1,113 @@
+package com.twilio.http;
+
+import com.twilio.Twilio;
+import com.twilio.constant.EnumConstants;
+import com.twilio.exception.ApiException;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpHeaders;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpVersion;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.RequestBuilder;
+import org.apache.http.client.utils.HttpClientUtils;
+import org.apache.http.config.SocketConfig;
+import org.apache.http.entity.BufferedHttpEntity;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.apache.http.message.BasicHeader;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+public class OAuthHttpClient extends HttpClient {
+ protected final org.apache.http.client.HttpClient client;
+
+ public OAuthHttpClient() {
+ this(DEFAULT_REQUEST_CONFIG);
+ }
+
+ public OAuthHttpClient(final RequestConfig requestConfig) {
+ this(requestConfig, DEFAULT_SOCKET_CONFIG);
+ }
+
+ public OAuthHttpClient(final RequestConfig requestConfig, final SocketConfig socketConfig) {
+ Collection headers = Arrays.asList(
+ new BasicHeader("X-Twilio-Client", "java-" + Twilio.VERSION),
+ new BasicHeader(HttpHeaders.ACCEPT, "application/json"),
+ new BasicHeader(HttpHeaders.ACCEPT_ENCODING, "utf-8")
+ );
+
+ String googleAppEngineVersion = System.getProperty("com.google.appengine.runtime.version");
+ boolean isGoogleAppEngine = googleAppEngineVersion != null && !googleAppEngineVersion.isEmpty();
+
+ org.apache.http.impl.client.HttpClientBuilder clientBuilder = HttpClientBuilder.create();
+
+ if (!isGoogleAppEngine) {
+ clientBuilder.useSystemProperties();
+ }
+
+ PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
+ connectionManager.setDefaultSocketConfig(socketConfig);
+ /*
+ * Example: Lets say client has one server.
+ * There are 4 servers on edge handling client request.
+ * Each request takes on an average 500ms (2 request per second)
+ * Total number request can be server in a second from a route: 20 * 4 * 2 (DefaultMaxPerRoute * edge servers * request per second)
+ */
+ connectionManager.setDefaultMaxPerRoute(5);
+ connectionManager.setMaxTotal(20);
+
+ client = clientBuilder
+ .setConnectionManager(connectionManager)
+ .setDefaultRequestConfig(requestConfig)
+ .setDefaultHeaders(headers)
+ .setRedirectStrategy(this.getRedirectStrategy())
+ .build();
+ }
+
+ @Override
+ public Response makeRequest(Request request) {
+ HttpMethod method = request.getMethod();
+ RequestBuilder builder = RequestBuilder.create(method.toString())
+ .setUri(request.constructURL().toString())
+ .setVersion(HttpVersion.HTTP_1_1)
+ .setCharset(StandardCharsets.UTF_8);
+
+
+ for (Map.Entry> entry : request.getHeaderParams().entrySet()) {
+ for (String value : entry.getValue()) {
+ builder.addHeader(entry.getKey(), value);
+ }
+ }
+ HttpEntity requestEntity = new StringEntity(request.getBody(), ContentType.APPLICATION_JSON);
+ builder.setEntity(requestEntity);
+ builder.addHeader(
+ HttpHeaders.CONTENT_TYPE, EnumConstants.ContentType.JSON.getValue());
+ builder.addHeader(HttpHeaders.USER_AGENT, HttpUtility.getUserAgentString(request.getUserAgentExtensions()));
+ HttpResponse response = null;
+
+ try {
+ response = client.execute(builder.build());
+ HttpEntity entity = response.getEntity();
+ return new Response(
+ // Consume the entire HTTP response before returning the stream
+ entity == null ? null : new BufferedHttpEntity(entity).getContent(),
+ response.getStatusLine().getStatusCode(),
+ response.getAllHeaders()
+ );
+ } catch (IOException e) {
+ throw new ApiException(e.getMessage(), e);
+ } finally {
+
+ // Ensure this response is properly closed
+ HttpClientUtils.closeQuietly(response);
+
+ }
+ }
+}
diff --git a/src/main/java/com/twilio/http/OAuthRequest.java b/src/main/java/com/twilio/http/OAuthRequest.java
new file mode 100644
index 0000000000..f17f231221
--- /dev/null
+++ b/src/main/java/com/twilio/http/OAuthRequest.java
@@ -0,0 +1,49 @@
+package com.twilio.http;
+
+public class OAuthRequest extends Request {
+
+ protected static final String DEFAULT_REGION = "us1";
+ public static final String QUERY_STRING_DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";
+ public static final String QUERY_STRING_DATE_FORMAT = "yyyy-MM-dd";
+
+ private String clientId;
+ private String clientSecret;
+
+ /**
+ * Create a new API request.
+ *
+ * @param method HTTP method
+ * @param url url of request
+ */
+ public OAuthRequest(final HttpMethod method, final String url) {
+ super(method, url);
+ }
+
+ /**
+ * Create a new API request.
+ *
+ * @param method HTTP method
+ * @param domain Twilio domain
+ * @param uri uri of request
+ */
+ public OAuthRequest(final HttpMethod method, final String domain, final String uri) {
+ this(method, domain, uri, null);
+ }
+
+ /**
+ * Create a new API request.
+ *
+ * @param method HTTP Method
+ * @param domain Twilio domain
+ * @param uri uri of request
+ * @param region region to make request
+ */
+ public OAuthRequest(
+ final HttpMethod method,
+ final String domain,
+ final String uri,
+ final String region
+ ) {
+ super(method, domain, uri, region);
+ }
+}
diff --git a/src/main/java/com/twilio/http/OAuthTokenManager.java b/src/main/java/com/twilio/http/OAuthTokenManager.java
new file mode 100644
index 0000000000..f26c698dbd
--- /dev/null
+++ b/src/main/java/com/twilio/http/OAuthTokenManager.java
@@ -0,0 +1,89 @@
+package com.twilio.http;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.twilio.TwilioOauth;
+import com.twilio.exception.ApiConnectionException;
+import com.twilio.exception.ApiException;
+import com.twilio.exception.RestException;
+import com.twilio.models.OAuthRequestBody;
+import com.twilio.models.OAuthResponseBody;
+import com.twilio.rest.content.v1.Content;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class OAuthTokenManager {
+ private static OAuthTokenManager instance;
+ private OAuthRequestBody oAuthRequestBody;
+ private OAuthResponseBody oAuthResponseBody;
+
+ private TwilioOAuthRestClient twilioOAuthRestClient = TwilioOauth.getRestClient();
+
+ public static OAuthTokenManager getInstance() {
+ if (instance == null) {
+ synchronized (OAuthTokenManager.class) {
+ if (instance == null) {
+ instance = new OAuthTokenManager();
+ }
+ }
+ }
+ return instance;
+ }
+
+ private OAuthTokenManager() { }
+
+ public String fetchToken(OAuthRequest request) {
+ if (oAuthResponseBody != null && !oAuthResponseBody.isAccessTokenExpired()) {
+ System.out.println("Using existing token");
+ return oAuthResponseBody.getAccessToken();
+ }
+ this.oAuthResponseBody = null;
+ System.out.println("Fetching token from server");
+ Response response = twilioOAuthRestClient.request(request);
+ handleResponseExceptions(response);
+
+ this.oAuthResponseBody = fromJson(response.getStream(), twilioOAuthRestClient.getObjectMapper());
+ if (oAuthResponseBody.getAccessToken() == null || oAuthResponseBody.getAccessToken().isEmpty()) {
+ throw new ApiException(
+ "OAuth access token is missing or invalid."
+ );
+ }
+ return this.oAuthResponseBody.getAccessToken();
+ }
+
+ private void handleResponseExceptions(Response response) {
+ if (response == null) {
+ throw new ApiConnectionException(
+ "Content creation failed: Unable to connect to server"
+ );
+ } else if (!TwilioOAuthRestClient.SUCCESS.test(response.getStatusCode())) {
+ RestException restException = RestException.fromJson(
+ response.getStream(),
+ twilioOAuthRestClient.getObjectMapper()
+ );
+ if (restException == null) {
+ throw new ApiException(
+ "Server Error, no content",
+ response.getStatusCode()
+ );
+ }
+ throw new ApiException(restException);
+ }
+ }
+
+ private OAuthResponseBody fromJson(
+ final InputStream json,
+ final ObjectMapper objectMapper
+ ) {
+ // Convert all checked exceptions to Runtime
+ try {
+ return objectMapper.readValue(json, OAuthResponseBody.class);
+ } catch (final JsonMappingException | JsonParseException e) {
+ throw new ApiException(e.getMessage(), e);
+ } catch (final IOException e) {
+ throw new ApiConnectionException(e.getMessage(), e);
+ }
+ }
+}
diff --git a/src/main/java/com/twilio/http/Request.java b/src/main/java/com/twilio/http/Request.java
index 36aa7a5e03..39caabfa02 100644
--- a/src/main/java/com/twilio/http/Request.java
+++ b/src/main/java/com/twilio/http/Request.java
@@ -25,22 +25,22 @@ public class Request {
public static final String QUERY_STRING_DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";
public static final String QUERY_STRING_DATE_FORMAT = "yyyy-MM-dd";
- private final HttpMethod method;
- private final String url;
- private final Map> queryParams;
- private final Map> postParams;
- private final Map> headerParams;
-
- private String region;
- private String edge;
+ protected final HttpMethod method;
+ protected final String url;
+ protected final Map> queryParams;
+ protected final Map> postParams;
+ protected final Map> headerParams;
+
+ protected String region;
+ protected String edge;
private String username;
private String password;
- private List userAgentExtensions;
+ protected List userAgentExtensions;
- private EnumConstants.ContentType contentType;
+ protected EnumConstants.ContentType contentType;
- private String body;
+ protected String body;
/**
* Create a new API request.
diff --git a/src/main/java/com/twilio/http/TwilioOAuthRestClient.java b/src/main/java/com/twilio/http/TwilioOAuthRestClient.java
new file mode 100644
index 0000000000..6c388fd5bd
--- /dev/null
+++ b/src/main/java/com/twilio/http/TwilioOAuthRestClient.java
@@ -0,0 +1,162 @@
+package com.twilio.http;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.twilio.exception.ApiConnectionException;
+import com.twilio.exception.ApiException;
+import com.twilio.exception.RestException;
+import com.twilio.models.OAuthRequestBody;
+import com.twilio.rest.content.v1.Content;
+import lombok.Getter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+
+public class TwilioOAuthRestClient {
+ public static final int HTTP_STATUS_CODE_CREATED = 201;
+ public static final int HTTP_STATUS_CODE_NO_CONTENT = 204;
+ public static final int HTTP_STATUS_CODE_OK = 200;
+ public static final Predicate SUCCESS = i -> i != null && i >= 200 && i < 400;
+ @Getter
+ private final ObjectMapper objectMapper;
+ @Getter
+ private final String region;
+ @Getter
+ private final String edge;
+
+ private OAuthRequestBody oAuthRequestBody;
+
+ private static final Logger logger = LoggerFactory.getLogger(TwilioOAuthRestClient.class);
+
+ @Getter
+ private final HttpClient httpClient;
+ @Getter
+ private final List userAgentExtensions;
+ protected TwilioOAuthRestClient(Builder builder) {
+ this.oAuthRequestBody = builder.oAuthModel;
+ this.region = builder.region;
+ this.edge = builder.edge;
+ this.httpClient = builder.httpClient;
+ this.objectMapper = new ObjectMapper();
+ this.userAgentExtensions = builder.userAgentExtensions;
+ }
+ public static class Builder {
+ private OAuthRequestBody oAuthModel;
+ private String region;
+ private String edge;
+ private HttpClient httpClient;
+ private List userAgentExtensions;
+
+ public Builder(final String clientId, final String clientSecret) {
+ this.oAuthModel = new OAuthRequestBody(clientId, clientSecret);
+ }
+
+ public TwilioOAuthRestClient.Builder region(final String region) {
+ this.region = region;
+ return this;
+ }
+
+ public TwilioOAuthRestClient.Builder edge(final String edge) {
+ this.edge = edge;
+ return this;
+ }
+
+ public TwilioOAuthRestClient.Builder httpClient(final HttpClient httpClient) {
+ this.httpClient = httpClient;
+ return this;
+ }
+
+ public TwilioOAuthRestClient.Builder userAgentExtensions(final List userAgentExtensions) {
+ if (userAgentExtensions != null && !userAgentExtensions.isEmpty()) {
+ this.userAgentExtensions = new ArrayList<>(userAgentExtensions);
+ }
+ return this;
+ }
+
+ public TwilioOAuthRestClient build() {
+ if (this.httpClient == null) {
+ this.httpClient = new OAuthHttpClient();
+ }
+ return new TwilioOAuthRestClient(this);
+ }
+ }
+
+ public Response request(final OAuthRequest request) {
+
+ buildRequest(request);
+ logRequest(request);
+ Response response = httpClient.reliableRequest(request);
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("status code: {}", response.getStatusCode());
+ org.apache.http.Header[] responseHeaders = response.getHeaders();
+ logger.debug("response headers:");
+ for (int i = 0; i < responseHeaders.length; i++) {
+ logger.debug("responseHeader: {}", responseHeaders[i]);
+ }
+ }
+ return response;
+ }
+
+ private void buildRequest(OAuthRequest request) {
+ request.setBody(Content.toJson(oAuthRequestBody, objectMapper));
+ if (region != null)
+ request.setRegion(region);
+ if (edge != null)
+ request.setEdge(edge);
+
+ if (userAgentExtensions != null && !userAgentExtensions.isEmpty()) {
+ request.setUserAgentExtensions(userAgentExtensions);
+ }
+ }
+
+ private void handleOAuthResponse(Response response) {
+ if (response == null) {
+ throw new ApiConnectionException(
+ "Content creation failed: Unable to connect to server"
+ );
+ } else if (!TwilioRestClient.SUCCESS.test(response.getStatusCode())) {
+ RestException restException = RestException.fromJson(
+ response.getStream(),
+ objectMapper
+ );
+ if (restException == null) {
+ throw new ApiException(
+ "Server Error, no content",
+ response.getStatusCode()
+ );
+ }
+ throw new ApiException(restException);
+ }
+ }
+
+
+
+ public void logRequest(final Request request) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("-- BEGIN Twilio API Request --");
+ logger.debug("request method: " + request.getMethod());
+ logger.debug("request URL: " + request.constructURL().toString());
+ final Map> queryParams = request.getQueryParams();
+ final Map> headerParams = request.getHeaderParams();
+
+ if (queryParams != null && !queryParams.isEmpty()) {
+ logger.debug("query parameters: " + queryParams);
+ }
+
+ if (headerParams != null && !headerParams.isEmpty()) {
+ logger.debug("header parameters: ");
+ for (String key : headerParams.keySet()) {
+ if (!key.toLowerCase().contains("authorization")) {
+ logger.debug(key + ": " + headerParams.get(key));
+ }
+ }
+ }
+
+ logger.debug("-- END Twilio API Request --");
+ }
+ }
+}
diff --git a/src/main/java/com/twilio/models/OAuthRequestBody.java b/src/main/java/com/twilio/models/OAuthRequestBody.java
new file mode 100644
index 0000000000..8034262a5c
--- /dev/null
+++ b/src/main/java/com/twilio/models/OAuthRequestBody.java
@@ -0,0 +1,38 @@
+package com.twilio.models;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+@ToString
+public class OAuthRequestBody {
+
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ @JsonProperty("client_id")
+ @Setter
+ private String clientId;
+
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ @JsonProperty("client_secret")
+ @Setter
+ private String clientSecret;
+
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ @JsonProperty("audience")
+ @Getter
+ @Setter
+ private String audience = "https://www.twilio.com/organizations";
+
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ @JsonProperty("grant_type")
+ @Getter
+ @Setter
+ private String grantType = "client_credentials";
+
+ public OAuthRequestBody(String clientId, String clientSecret) {
+ this.clientId = clientId;
+ this.clientSecret = clientSecret;
+ }
+}
diff --git a/src/main/java/com/twilio/models/OAuthResponseBody.java b/src/main/java/com/twilio/models/OAuthResponseBody.java
new file mode 100644
index 0000000000..c07e0f280e
--- /dev/null
+++ b/src/main/java/com/twilio/models/OAuthResponseBody.java
@@ -0,0 +1,38 @@
+package com.twilio.models;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.util.Date;
+
+@ToString
+public class OAuthResponseBody {
+
+ @JsonProperty("access_token")
+ @Getter
+ @Setter
+ private String accessToken;
+
+ @JsonProperty("expires_in")
+ @Getter
+ @Setter
+ private String expiresIn;
+
+ @JsonProperty("token_type")
+ @Getter
+ @Setter
+ private String tokenType;
+
+ public boolean isAccessTokenExpired() {
+ DecodedJWT jwt = JWT.decode(this.accessToken);
+ Date expiresAt = jwt.getExpiresAt();
+ // Add a buffer of 30 seconds
+ long bufferMilliseconds = 30 * 1000;
+ Date bufferExpiresAt = new Date(expiresAt.getTime() - bufferMilliseconds);
+ return bufferExpiresAt.before(new Date());
+ }
+}
\ No newline at end of file