Skip to content

Commit

Permalink
feat: Added OAuth Support for Public APIs with TokenManager Integrati…
Browse files Browse the repository at this point in the history
…on (#813)

Added OAuth functionality for public APIs
  • Loading branch information
sbansla authored Oct 3, 2024
1 parent fab34c1 commit 30e91a6
Show file tree
Hide file tree
Showing 21 changed files with 501 additions and 41 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/test-and-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ jobs:
TWILIO_ORGS_CLIENT_ID: ${{ secrets.TWILIO_ORGS_CLIENT_ID }}
TWILIO_ORGS_CLIENT_SECRET: ${{ secrets.TWILIO_ORGS_CLIENT_SECRET }}
TWILIO_ORG_SID: ${{ secrets.TWILIO_ORG_SID }}
TWILIO_CLIENT_ID: ${{ secrets.TWILIO_CLIENT_ID }}
TWILIO_CLIENT_SECRET: ${{ secrets.TWILIO_CLIENT_SECRET }}
TWILIO_MESSAGE_SID: ${{ secrets.TWILIO_MESSAGE_SID }}
run: mvn test -DTest="ClusterTest" -B

- uses: actions/setup-java@v4
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,14 @@ public class Example {
}
```



### OAuth Feature for Twilio APIs
We are introducing Client Credentials Flow-based OAuth 2.0 authentication.
This feature is currently in `beta` and its implementation is subject to change.

Detailed examples [here](https://github.com/twilio/twilio-java/blob/main/examples/FetchMessageUsingOAuth.md)

### Iterate through records

The library automatically handles paging for you. With the `read` method, you can specify the number of records you want to receive (`limit`) and the maximum size you want each page fetch to be (`pageSize`). The library will then handle the task for you, fetching new pages under the hood as you iterate over the records.
Expand Down
21 changes: 21 additions & 0 deletions examples/FetchMessageUsingOAuth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
```
import com.twilio.Twilio;
import com.twilio.credential.ClientCredentialProvider;
import com.twilio.rest.api.v2010.account.Message;
public class FetchMessageUsingOAuth {
public static void main(String[] args) {
String clientId = "YOUR_CLIENT_ID";
String clientSecret = "YOUR_CLIENT_SECRET";
String accountSid = "YOUR_ACCOUNT_SID";
Twilio.init(new ClientCredentialProvider(clientId, clientSecret), accountSid);
/*
Or use the following if accountSid is not required as a path parameter for an API or when setting accountSid in the API.
Twilio.init(new ClientCredentialProvider(clientId, clientSecret));
*/
String messageSid = "YOUR_MESSAGE_SID";
Message message = Message.fetcher(messageSid).fetch();
}
}
```

58 changes: 53 additions & 5 deletions src/main/java/com/twilio/Twilio.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.twilio;

import com.twilio.annotations.Beta;
import com.twilio.auth_strategy.AuthStrategy;
import com.twilio.exception.ApiException;
import com.twilio.exception.AuthenticationException;
import com.twilio.exception.CertificateValidationException;
import com.twilio.credential.CredentialProvider;
import com.twilio.http.HttpMethod;
import com.twilio.http.NetworkHttpClient;
import com.twilio.http.Request;
Expand Down Expand Up @@ -34,6 +37,8 @@ public class Twilio {
private static String edge = System.getenv("TWILIO_EDGE");
private static volatile TwilioRestClient restClient;
private static volatile ExecutorService executorService;

private static CredentialProvider credentialProvider;

private Twilio() {
}
Expand Down Expand Up @@ -64,6 +69,31 @@ public static synchronized void init(final String username, final String passwor
Twilio.setAccountSid(null);
}

@Beta
public static synchronized void init(final CredentialProvider credentialProvider) {
Twilio.setCredentialProvider(credentialProvider);
Twilio.setAccountSid(null);
}

@Beta
public static synchronized void init(final CredentialProvider credentialProvider, String accountSid) {
Twilio.setCredentialProvider(credentialProvider);
Twilio.setAccountSid(accountSid);
}

private static void setCredentialProvider(final CredentialProvider credentialProvider) {
if (credentialProvider == null) {
throw new AuthenticationException("Credential Provider can not be null");
}

if (!credentialProvider.equals(Twilio.credentialProvider)) {
Twilio.invalidate();
}
// Invalidate Basic Creds as they might be initialized via environment variables.
invalidateBasicCreds();
Twilio.credentialProvider = credentialProvider;
}

/**
* Initialize the Twilio environment.
*
Expand Down Expand Up @@ -91,6 +121,7 @@ public static synchronized void setUsername(final String username) {
if (!username.equals(Twilio.username)) {
Twilio.invalidate();
}
Twilio.invalidateOAuthCreds();

Twilio.username = username;
}
Expand All @@ -109,6 +140,7 @@ public static synchronized void setPassword(final String password) {
if (!password.equals(Twilio.password)) {
Twilio.invalidate();
}
Twilio.invalidateOAuthCreds();

Twilio.password = password;
}
Expand Down Expand Up @@ -181,12 +213,19 @@ public static TwilioRestClient getRestClient() {

private static TwilioRestClient buildRestClient() {
if (Twilio.username == null || Twilio.password == null) {
throw new AuthenticationException(
"TwilioRestClient was used before AccountSid and AuthToken were set, please call Twilio.init()"
);
if (credentialProvider == null) {
throw new AuthenticationException(
"Credentials have not been initialized or changed, please call Twilio.init()"
);
}
}
TwilioRestClient.Builder builder;
if (credentialProvider != null) {
AuthStrategy authStrategy = credentialProvider.toAuthStrategy();
builder = new TwilioRestClient.Builder(authStrategy);
} else {
builder = new TwilioRestClient.Builder(Twilio.username, Twilio.password);
}

TwilioRestClient.Builder builder = new TwilioRestClient.Builder(Twilio.username, Twilio.password);

if (Twilio.accountSid != null) {
builder.accountSid(Twilio.accountSid);
Expand Down Expand Up @@ -273,6 +312,15 @@ private static void invalidate() {
Twilio.restClient = null;
}

private static void invalidateOAuthCreds() {
Twilio.credentialProvider = null;
}

private static void invalidateBasicCreds() {
Twilio.username = null;
Twilio.password = null;
}

/**
* Attempts to gracefully shutdown the ExecutorService if it is present.
*/
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/twilio/TwilioNoAuth.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package com.twilio;

import com.twilio.annotations.Preview;
import com.twilio.annotations.Beta;
import com.twilio.http.noauth.NoAuthTwilioRestClient;
import lombok.Getter;

import java.util.List;
import com.twilio.exception.AuthenticationException;

@Preview
@Beta
public class TwilioNoAuth {
@Getter
private static List<String> userAgentExtensions;
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/twilio/TwilioOrgsTokenAuth.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.twilio;

import com.twilio.annotations.Preview;
import com.twilio.annotations.Beta;
import com.twilio.exception.AuthenticationException;
import com.twilio.http.bearertoken.BearerTokenTwilioRestClient;
import lombok.Getter;
Expand All @@ -12,7 +12,7 @@
import com.twilio.http.bearertoken.TokenManager;
import com.twilio.http.bearertoken.OrgsTokenManager;

@Preview
@Beta
public class TwilioOrgsTokenAuth {
private static String accessToken;
@Getter
Expand Down
12 changes: 0 additions & 12 deletions src/main/java/com/twilio/annotations/Preview.java

This file was deleted.

17 changes: 17 additions & 0 deletions src/main/java/com/twilio/auth_strategy/AuthStrategy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.twilio.auth_strategy;

import com.twilio.constant.EnumConstants;
import lombok.Getter;

public abstract class AuthStrategy {
@Getter
private EnumConstants.AuthType authType;

public AuthStrategy(EnumConstants.AuthType authType) {
this.authType = authType;
}
public abstract String getAuthString();

public abstract boolean requiresAuthentication();

}
44 changes: 44 additions & 0 deletions src/main/java/com/twilio/auth_strategy/BasicAuthStrategy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.twilio.auth_strategy;

import com.twilio.constant.EnumConstants;

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Objects;

public class BasicAuthStrategy extends AuthStrategy {
private String username;
private String password;

public BasicAuthStrategy(String username, String password) {
super(EnumConstants.AuthType.BASIC);
this.username = username;
this.password = password;
}

@Override
public String getAuthString() {
String credentials = username + ":" + password;
String encoded = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.US_ASCII));
return "Basic " + encoded;
}

@Override
public boolean requiresAuthentication() {
return true;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BasicAuthStrategy that = (BasicAuthStrategy) o;
return Objects.equals(username, that.username) &&
Objects.equals(password, that.password);
}

@Override
public int hashCode() {
return Objects.hash(username, password);
}
}
20 changes: 20 additions & 0 deletions src/main/java/com/twilio/auth_strategy/NoAuthStrategy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.twilio.auth_strategy;

import com.twilio.constant.EnumConstants;

public class NoAuthStrategy extends AuthStrategy {

public NoAuthStrategy(String token) {
super(EnumConstants.AuthType.NO_AUTH);
}

@Override
public String getAuthString() {
return "";
}

@Override
public boolean requiresAuthentication() {
return false;
}
}
66 changes: 66 additions & 0 deletions src/main/java/com/twilio/auth_strategy/TokenAuthStrategy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.twilio.auth_strategy;

import com.auth0.jwt.JWT;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.twilio.constant.EnumConstants;
import com.twilio.http.bearertoken.TokenManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Date;
import java.util.Objects;

public class TokenAuthStrategy extends AuthStrategy {
private String token;
private TokenManager tokenManager;
private static final Logger logger = LoggerFactory.getLogger(TokenAuthStrategy.class);
public TokenAuthStrategy(TokenManager tokenManager) {
super(EnumConstants.AuthType.TOKEN);
this.tokenManager = tokenManager;
}

@Override
public String getAuthString() {
fetchToken();
return "Bearer " + token;
}

@Override
public boolean requiresAuthentication() {
return true;
}

// Token-specific refresh logic
public void fetchToken() {
if (this.token == null || this.token.isEmpty() || isTokenExpired(this.token)) {
synchronized (TokenAuthStrategy.class){
if (this.token == null || this.token.isEmpty() || isTokenExpired(this.token)) {
logger.info("Fetching new token for Apis");
this.token = tokenManager.fetchAccessToken();
}
}
}
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TokenAuthStrategy that = (TokenAuthStrategy) o;
return Objects.equals(token, that.token) &&
Objects.equals(tokenManager, that.tokenManager);
}
@Override
public int hashCode() {
return Objects.hash(token, tokenManager);
}

public boolean isTokenExpired(final String token) {
DecodedJWT jwt = JWT.decode(token);
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());
}
}
12 changes: 12 additions & 0 deletions src/main/java/com/twilio/constant/EnumConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,16 @@ public enum ContentType {

private final String value;
}

@Getter
@RequiredArgsConstructor
public enum AuthType {
NO_AUTH("noauth"),
BASIC("basic"),
TOKEN("token"),
API_KEY("api_key"),
CLIENT_CREDENTIALS("client_credentials");

private final String value;
}
}
Loading

0 comments on commit 30e91a6

Please sign in to comment.