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: Added OAuth Support for Public APIs with TokenManager Integration #813

Merged
merged 15 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we mention the orgs auth is also in beta? since we changed back from preview?

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a null check here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call fetch 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
Loading