Skip to content

Commit 30e91a6

Browse files
authored
feat: Added OAuth Support for Public APIs with TokenManager Integration (#813)
Added OAuth functionality for public APIs
1 parent fab34c1 commit 30e91a6

21 files changed

+501
-41
lines changed

.github/workflows/test-and-deploy.yml

+3
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ jobs:
5353
TWILIO_ORGS_CLIENT_ID: ${{ secrets.TWILIO_ORGS_CLIENT_ID }}
5454
TWILIO_ORGS_CLIENT_SECRET: ${{ secrets.TWILIO_ORGS_CLIENT_SECRET }}
5555
TWILIO_ORG_SID: ${{ secrets.TWILIO_ORG_SID }}
56+
TWILIO_CLIENT_ID: ${{ secrets.TWILIO_CLIENT_ID }}
57+
TWILIO_CLIENT_SECRET: ${{ secrets.TWILIO_CLIENT_SECRET }}
58+
TWILIO_MESSAGE_SID: ${{ secrets.TWILIO_MESSAGE_SID }}
5659
run: mvn test -DTest="ClusterTest" -B
5760

5861
- uses: actions/setup-java@v4

README.md

+8
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,14 @@ public class Example {
214214
}
215215
```
216216

217+
218+
219+
### OAuth Feature for Twilio APIs
220+
We are introducing Client Credentials Flow-based OAuth 2.0 authentication.
221+
This feature is currently in `beta` and its implementation is subject to change.
222+
223+
Detailed examples [here](https://github.com/twilio/twilio-java/blob/main/examples/FetchMessageUsingOAuth.md)
224+
217225
### Iterate through records
218226

219227
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.

examples/FetchMessageUsingOAuth.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
```
2+
import com.twilio.Twilio;
3+
import com.twilio.credential.ClientCredentialProvider;
4+
import com.twilio.rest.api.v2010.account.Message;
5+
6+
public class FetchMessageUsingOAuth {
7+
public static void main(String[] args) {
8+
String clientId = "YOUR_CLIENT_ID";
9+
String clientSecret = "YOUR_CLIENT_SECRET";
10+
String accountSid = "YOUR_ACCOUNT_SID";
11+
Twilio.init(new ClientCredentialProvider(clientId, clientSecret), accountSid);
12+
/*
13+
Or use the following if accountSid is not required as a path parameter for an API or when setting accountSid in the API.
14+
Twilio.init(new ClientCredentialProvider(clientId, clientSecret));
15+
*/
16+
String messageSid = "YOUR_MESSAGE_SID";
17+
Message message = Message.fetcher(messageSid).fetch();
18+
}
19+
}
20+
```
21+

src/main/java/com/twilio/Twilio.java

+53-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package com.twilio;
22

3+
import com.twilio.annotations.Beta;
4+
import com.twilio.auth_strategy.AuthStrategy;
35
import com.twilio.exception.ApiException;
46
import com.twilio.exception.AuthenticationException;
57
import com.twilio.exception.CertificateValidationException;
8+
import com.twilio.credential.CredentialProvider;
69
import com.twilio.http.HttpMethod;
710
import com.twilio.http.NetworkHttpClient;
811
import com.twilio.http.Request;
@@ -34,6 +37,8 @@ public class Twilio {
3437
private static String edge = System.getenv("TWILIO_EDGE");
3538
private static volatile TwilioRestClient restClient;
3639
private static volatile ExecutorService executorService;
40+
41+
private static CredentialProvider credentialProvider;
3742

3843
private Twilio() {
3944
}
@@ -64,6 +69,31 @@ public static synchronized void init(final String username, final String passwor
6469
Twilio.setAccountSid(null);
6570
}
6671

72+
@Beta
73+
public static synchronized void init(final CredentialProvider credentialProvider) {
74+
Twilio.setCredentialProvider(credentialProvider);
75+
Twilio.setAccountSid(null);
76+
}
77+
78+
@Beta
79+
public static synchronized void init(final CredentialProvider credentialProvider, String accountSid) {
80+
Twilio.setCredentialProvider(credentialProvider);
81+
Twilio.setAccountSid(accountSid);
82+
}
83+
84+
private static void setCredentialProvider(final CredentialProvider credentialProvider) {
85+
if (credentialProvider == null) {
86+
throw new AuthenticationException("Credential Provider can not be null");
87+
}
88+
89+
if (!credentialProvider.equals(Twilio.credentialProvider)) {
90+
Twilio.invalidate();
91+
}
92+
// Invalidate Basic Creds as they might be initialized via environment variables.
93+
invalidateBasicCreds();
94+
Twilio.credentialProvider = credentialProvider;
95+
}
96+
6797
/**
6898
* Initialize the Twilio environment.
6999
*
@@ -91,6 +121,7 @@ public static synchronized void setUsername(final String username) {
91121
if (!username.equals(Twilio.username)) {
92122
Twilio.invalidate();
93123
}
124+
Twilio.invalidateOAuthCreds();
94125

95126
Twilio.username = username;
96127
}
@@ -109,6 +140,7 @@ public static synchronized void setPassword(final String password) {
109140
if (!password.equals(Twilio.password)) {
110141
Twilio.invalidate();
111142
}
143+
Twilio.invalidateOAuthCreds();
112144

113145
Twilio.password = password;
114146
}
@@ -181,12 +213,19 @@ public static TwilioRestClient getRestClient() {
181213

182214
private static TwilioRestClient buildRestClient() {
183215
if (Twilio.username == null || Twilio.password == null) {
184-
throw new AuthenticationException(
185-
"TwilioRestClient was used before AccountSid and AuthToken were set, please call Twilio.init()"
186-
);
216+
if (credentialProvider == null) {
217+
throw new AuthenticationException(
218+
"Credentials have not been initialized or changed, please call Twilio.init()"
219+
);
220+
}
221+
}
222+
TwilioRestClient.Builder builder;
223+
if (credentialProvider != null) {
224+
AuthStrategy authStrategy = credentialProvider.toAuthStrategy();
225+
builder = new TwilioRestClient.Builder(authStrategy);
226+
} else {
227+
builder = new TwilioRestClient.Builder(Twilio.username, Twilio.password);
187228
}
188-
189-
TwilioRestClient.Builder builder = new TwilioRestClient.Builder(Twilio.username, Twilio.password);
190229

191230
if (Twilio.accountSid != null) {
192231
builder.accountSid(Twilio.accountSid);
@@ -273,6 +312,15 @@ private static void invalidate() {
273312
Twilio.restClient = null;
274313
}
275314

315+
private static void invalidateOAuthCreds() {
316+
Twilio.credentialProvider = null;
317+
}
318+
319+
private static void invalidateBasicCreds() {
320+
Twilio.username = null;
321+
Twilio.password = null;
322+
}
323+
276324
/**
277325
* Attempts to gracefully shutdown the ExecutorService if it is present.
278326
*/

src/main/java/com/twilio/TwilioNoAuth.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
package com.twilio;
22

3-
import com.twilio.annotations.Preview;
3+
import com.twilio.annotations.Beta;
44
import com.twilio.http.noauth.NoAuthTwilioRestClient;
55
import lombok.Getter;
66

77
import java.util.List;
88
import com.twilio.exception.AuthenticationException;
99

10-
@Preview
10+
@Beta
1111
public class TwilioNoAuth {
1212
@Getter
1313
private static List<String> userAgentExtensions;

src/main/java/com/twilio/TwilioOrgsTokenAuth.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.twilio;
22

3-
import com.twilio.annotations.Preview;
3+
import com.twilio.annotations.Beta;
44
import com.twilio.exception.AuthenticationException;
55
import com.twilio.http.bearertoken.BearerTokenTwilioRestClient;
66
import lombok.Getter;
@@ -12,7 +12,7 @@
1212
import com.twilio.http.bearertoken.TokenManager;
1313
import com.twilio.http.bearertoken.OrgsTokenManager;
1414

15-
@Preview
15+
@Beta
1616
public class TwilioOrgsTokenAuth {
1717
private static String accessToken;
1818
@Getter

src/main/java/com/twilio/annotations/Preview.java

-12
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.twilio.auth_strategy;
2+
3+
import com.twilio.constant.EnumConstants;
4+
import lombok.Getter;
5+
6+
public abstract class AuthStrategy {
7+
@Getter
8+
private EnumConstants.AuthType authType;
9+
10+
public AuthStrategy(EnumConstants.AuthType authType) {
11+
this.authType = authType;
12+
}
13+
public abstract String getAuthString();
14+
15+
public abstract boolean requiresAuthentication();
16+
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.twilio.auth_strategy;
2+
3+
import com.twilio.constant.EnumConstants;
4+
5+
import java.nio.charset.StandardCharsets;
6+
import java.util.Base64;
7+
import java.util.Objects;
8+
9+
public class BasicAuthStrategy extends AuthStrategy {
10+
private String username;
11+
private String password;
12+
13+
public BasicAuthStrategy(String username, String password) {
14+
super(EnumConstants.AuthType.BASIC);
15+
this.username = username;
16+
this.password = password;
17+
}
18+
19+
@Override
20+
public String getAuthString() {
21+
String credentials = username + ":" + password;
22+
String encoded = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.US_ASCII));
23+
return "Basic " + encoded;
24+
}
25+
26+
@Override
27+
public boolean requiresAuthentication() {
28+
return true;
29+
}
30+
31+
@Override
32+
public boolean equals(Object o) {
33+
if (this == o) return true;
34+
if (o == null || getClass() != o.getClass()) return false;
35+
BasicAuthStrategy that = (BasicAuthStrategy) o;
36+
return Objects.equals(username, that.username) &&
37+
Objects.equals(password, that.password);
38+
}
39+
40+
@Override
41+
public int hashCode() {
42+
return Objects.hash(username, password);
43+
}
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.twilio.auth_strategy;
2+
3+
import com.twilio.constant.EnumConstants;
4+
5+
public class NoAuthStrategy extends AuthStrategy {
6+
7+
public NoAuthStrategy(String token) {
8+
super(EnumConstants.AuthType.NO_AUTH);
9+
}
10+
11+
@Override
12+
public String getAuthString() {
13+
return "";
14+
}
15+
16+
@Override
17+
public boolean requiresAuthentication() {
18+
return false;
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.twilio.auth_strategy;
2+
3+
import com.auth0.jwt.JWT;
4+
import com.auth0.jwt.interfaces.DecodedJWT;
5+
import com.twilio.constant.EnumConstants;
6+
import com.twilio.http.bearertoken.TokenManager;
7+
import org.slf4j.Logger;
8+
import org.slf4j.LoggerFactory;
9+
10+
import java.util.Date;
11+
import java.util.Objects;
12+
13+
public class TokenAuthStrategy extends AuthStrategy {
14+
private String token;
15+
private TokenManager tokenManager;
16+
private static final Logger logger = LoggerFactory.getLogger(TokenAuthStrategy.class);
17+
public TokenAuthStrategy(TokenManager tokenManager) {
18+
super(EnumConstants.AuthType.TOKEN);
19+
this.tokenManager = tokenManager;
20+
}
21+
22+
@Override
23+
public String getAuthString() {
24+
fetchToken();
25+
return "Bearer " + token;
26+
}
27+
28+
@Override
29+
public boolean requiresAuthentication() {
30+
return true;
31+
}
32+
33+
// Token-specific refresh logic
34+
public void fetchToken() {
35+
if (this.token == null || this.token.isEmpty() || isTokenExpired(this.token)) {
36+
synchronized (TokenAuthStrategy.class){
37+
if (this.token == null || this.token.isEmpty() || isTokenExpired(this.token)) {
38+
logger.info("Fetching new token for Apis");
39+
this.token = tokenManager.fetchAccessToken();
40+
}
41+
}
42+
}
43+
}
44+
45+
@Override
46+
public boolean equals(Object o) {
47+
if (this == o) return true;
48+
if (o == null || getClass() != o.getClass()) return false;
49+
TokenAuthStrategy that = (TokenAuthStrategy) o;
50+
return Objects.equals(token, that.token) &&
51+
Objects.equals(tokenManager, that.tokenManager);
52+
}
53+
@Override
54+
public int hashCode() {
55+
return Objects.hash(token, tokenManager);
56+
}
57+
58+
public boolean isTokenExpired(final String token) {
59+
DecodedJWT jwt = JWT.decode(token);
60+
Date expiresAt = jwt.getExpiresAt();
61+
// Add a buffer of 30 seconds
62+
long bufferMilliseconds = 30 * 1000;
63+
Date bufferExpiresAt = new Date(expiresAt.getTime() - bufferMilliseconds);
64+
return bufferExpiresAt.before(new Date());
65+
}
66+
}

src/main/java/com/twilio/constant/EnumConstants.java

+12
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,16 @@ public enum ContentType {
1414

1515
private final String value;
1616
}
17+
18+
@Getter
19+
@RequiredArgsConstructor
20+
public enum AuthType {
21+
NO_AUTH("noauth"),
22+
BASIC("basic"),
23+
TOKEN("token"),
24+
API_KEY("api_key"),
25+
CLIENT_CREDENTIALS("client_credentials");
26+
27+
private final String value;
28+
}
1729
}

0 commit comments

Comments
 (0)