Skip to content

Commit f688b3d

Browse files
committed
fix: Refactor OAuth handling and improve token management
1 parent 2644266 commit f688b3d

File tree

4 files changed

+204
-47
lines changed

4 files changed

+204
-47
lines changed

src/main/java/com/contentstack/cms/Contentstack.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -801,11 +801,9 @@ private void validateClient(Contentstack contentstack) {
801801

802802
// Initialize OAuth if configured
803803
if (this.oauthConfig != null) {
804-
this.oauthHandler = contentstack.oauthHandler = new OAuthHandler(httpClient(contentstack, this.retry), this.oauthConfig);
805-
this.oauthInterceptor = contentstack.oauthInterceptor = new OAuthInterceptor(this.oauthHandler);
806-
if (this.earlyAccess != null) {
807-
this.oauthInterceptor.setEarlyAccess(this.earlyAccess);
808-
}
804+
// OAuth handler and interceptor are created in httpClient
805+
contentstack.oauthHandler = this.oauthHandler;
806+
contentstack.oauthInterceptor = this.oauthInterceptor;
809807
}
810808
}
811809

@@ -823,11 +821,13 @@ private OkHttpClient httpClient(Contentstack contentstack, Boolean retryOnFailur
823821
OkHttpClient tempClient = builder.build();
824822
this.oauthHandler = new OAuthHandler(tempClient, this.oauthConfig);
825823
this.oauthInterceptor = new OAuthInterceptor(this.oauthHandler);
824+
825+
// Configure early access if needed
826826
if (this.earlyAccess != null) {
827827
this.oauthInterceptor.setEarlyAccess(this.earlyAccess);
828828
}
829829

830-
// Add interceptor to final client
830+
// Add interceptor to handle OAuth, token refresh, and retries
831831
builder.addInterceptor(this.oauthInterceptor);
832832
} else {
833833
this.authInterceptor = contentstack.interceptor = new AuthInterceptor();

src/main/java/com/contentstack/cms/models/OAuthTokens.java

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
@Getter
1515
@Setter
1616
public class OAuthTokens {
17+
private static final String BEARER_TOKEN_TYPE = "Bearer";
1718
@SerializedName("access_token")
1819
private String accessToken;
1920

@@ -54,10 +55,25 @@ public OAuthTokens() {
5455
public void setExpiresIn(Long expiresIn) {
5556
this.expiresIn = expiresIn;
5657
if (expiresIn != null) {
57-
this.expiresAt = new Date(issuedAt.getTime() + (expiresIn * 1000));
58+
setExpiresAt(new Date(System.currentTimeMillis() + (expiresIn * 1000)));
5859
}
5960
}
6061

62+
public synchronized void setExpiresAt(Date expiresAt) {
63+
this.expiresAt = expiresAt != null ? new Date(expiresAt.getTime()) : null;
64+
if (expiresAt != null) {
65+
this.expiresIn = (expiresAt.getTime() - System.currentTimeMillis()) / 1000;
66+
}
67+
}
68+
69+
public synchronized Date getExpiresAt() {
70+
return expiresAt != null ? new Date(expiresAt.getTime()) : null;
71+
}
72+
73+
public synchronized Date getIssuedAt() {
74+
return issuedAt != null ? new Date(issuedAt.getTime()) : null;
75+
}
76+
6177
/**
6278
* Gets the scopes as a list
6379
* @return List of scope strings or empty list if no scopes
@@ -95,18 +111,32 @@ public boolean hasScope(String scopeToCheck) {
95111
* @return true if token is expired or will expire soon
96112
*/
97113
public boolean isExpired() {
114+
// No expiry time means token is considered expired
98115
if (expiresAt == null) {
99116
return true;
100117
}
101-
return System.currentTimeMillis() + EXPIRY_BUFFER_MS > expiresAt.getTime();
118+
119+
// No access token means token is considered expired
120+
if (!hasAccessToken()) {
121+
return true;
122+
}
123+
124+
// Check if current time + buffer is past expiry
125+
long currentTime = System.currentTimeMillis();
126+
long expiryTime = expiresAt.getTime();
127+
long timeUntilExpiry = expiryTime - currentTime;
128+
129+
// Consider expired if within buffer window
130+
return timeUntilExpiry <= EXPIRY_BUFFER_MS;
102131
}
103132

104133
/**
105134
* Checks if the token is valid (has access token and not expired)
106135
* @return true if token is valid
107136
*/
108-
public boolean isValid() {
109-
return hasAccessToken() && !isExpired();
137+
public synchronized boolean isValid() {
138+
return hasAccessToken() && !isExpired() &&
139+
BEARER_TOKEN_TYPE.equalsIgnoreCase(tokenType);
110140
}
111141

112142
/**

src/main/java/com/contentstack/cms/oauth/OAuthHandler.java

Lines changed: 77 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import java.security.NoSuchAlgorithmException;
88
import java.security.SecureRandom;
99
import java.util.Base64;
10+
import java.util.Date;
1011
import java.util.concurrent.CompletableFuture;
1112

1213
import com.contentstack.cms.models.OAuthConfig;
@@ -31,6 +32,7 @@ public class OAuthHandler {
3132
private OkHttpClient httpClient;
3233
private final OAuthConfig config;
3334
private final Gson gson;
35+
private final Object tokenLock = new Object();
3436

3537
private String codeVerifier;
3638
private String codeChallenge;
@@ -118,7 +120,8 @@ public String authorize() {
118120
StringBuilder urlBuilder = new StringBuilder(baseUrl);
119121
urlBuilder.append("?response_type=").append(config.getResponseType())
120122
.append("&client_id=").append(URLEncoder.encode(config.getClientId(), "UTF-8"))
121-
.append("&redirect_uri=").append(URLEncoder.encode(config.getRedirectUri(), "UTF-8"));
123+
.append("&redirect_uri=").append(URLEncoder.encode(config.getRedirectUri(), "UTF-8"))
124+
.append("&app_id=").append(URLEncoder.encode(config.getAppId(), "UTF-8"));
122125

123126
// Add scope if provided
124127
if (config.getScope() != null && !config.getScope().trim().isEmpty()) {
@@ -145,11 +148,16 @@ public String authorize() {
145148
* @return Future containing the tokens
146149
*/
147150
public CompletableFuture<OAuthTokens> exchangeCodeForToken(String code) {
151+
if (code == null || code.trim().isEmpty()) {
152+
return CompletableFuture.failedFuture(new IllegalArgumentException("Authorization code cannot be null or empty"));
153+
}
154+
155+
System.out.println("\nExchanging authorization code for tokens...");
148156
return CompletableFuture.supplyAsync(() -> {
149157
try {
150158
FormBody.Builder formBuilder = new FormBody.Builder()
151159
.add("grant_type", "authorization_code")
152-
.add("code", code)
160+
.add("code", code.trim())
153161
.add("redirect_uri", config.getRedirectUri())
154162
.add("client_id", config.getClientId())
155163
.add("app_id", config.getAppId());
@@ -178,26 +186,49 @@ public CompletableFuture<OAuthTokens> exchangeCodeForToken(String code) {
178186
* @param tokens The tokens to save
179187
*/
180188
private void _saveTokens(OAuthTokens tokens) {
181-
this.tokens = tokens;
189+
synchronized (tokenLock) {
190+
this.tokens = tokens;
191+
}
192+
}
193+
194+
private OAuthTokens _getTokens() {
195+
synchronized (tokenLock) {
196+
return this.tokens;
197+
}
182198
}
183199

184200
/**
185201
* Refreshes the access token using the refresh token
186202
* @return Future containing the new tokens
187203
*/
188204
public CompletableFuture<OAuthTokens> refreshAccessToken() {
189-
if (tokens == null || !tokens.hasRefreshToken()) {
205+
// Check if we have tokens and refresh token
206+
if (tokens == null) {
207+
return CompletableFuture.failedFuture(
208+
new IllegalStateException("No tokens available"));
209+
}
210+
if (!tokens.hasRefreshToken()) {
190211
return CompletableFuture.failedFuture(
191212
new IllegalStateException("No refresh token available"));
192213
}
214+
215+
// Check if token is actually expired
216+
if (!tokens.isExpired()) {
217+
return CompletableFuture.completedFuture(tokens);
218+
}
193219

194220
return CompletableFuture.supplyAsync(() -> {
195221
try {
222+
System.out.println("\nRefreshing access token...");
223+
System.out.println("Current token expired: " + tokens.isExpired());
224+
System.out.println("Has refresh token: " + tokens.hasRefreshToken());
225+
System.out.println("Time until expiry: " + tokens.getTimeUntilExpiration() + "ms");
226+
196227
FormBody.Builder formBuilder = new FormBody.Builder()
197-
.add("app_id", config.getAppId())
198228
.add("grant_type", "refresh_token")
199229
.add("refresh_token", tokens.getRefreshToken())
200-
.add("client_id", config.getClientId());
230+
.add("client_id", config.getClientId())
231+
.add("app_id", config.getAppId());
201232

202233
// Add client_secret if available, otherwise add code_verifier
203234
if (config.getClientSecret() != null && !config.getClientSecret().trim().isEmpty()) {
@@ -206,13 +237,18 @@ public CompletableFuture<OAuthTokens> refreshAccessToken() {
206237
formBuilder.add("code_verifier", this.codeVerifier);
207238
}
208239

209-
Request request = _getHeaders()
240+
Request request = new Request.Builder()
210241
.url(config.getTokenEndpoint())
242+
.header("Content-Type", "application/x-www-form-urlencoded")
211243
.post(formBuilder.build())
212244
.build();
213245

214-
return executeTokenRequest(request);
246+
OAuthTokens newTokens = executeTokenRequest(request);
247+
System.out.println("Token refresh successful!");
248+
System.out.println("New token expires in: " + newTokens.getExpiresIn() + " seconds");
249+
return newTokens;
215250
} catch (IOException | RuntimeException e) {
251+
System.err.println("Token refresh failed: " + e.getMessage());
216252
throw new RuntimeException("Failed to refresh tokens", e);
217253
}
218254
});
@@ -258,8 +294,13 @@ private OAuthTokens executeTokenRequest(Request request) throws IOException {
258294

259295
OAuthTokens newTokens = gson.fromJson(body, OAuthTokens.class);
260296

261-
// Keep old refresh token if new one not provided
262-
if (this.tokens != null && newTokens.getRefreshToken() == null) {
297+
// Set token expiry time
298+
if (newTokens.getExpiresIn() != null) {
299+
newTokens.setExpiresAt(new Date(System.currentTimeMillis() + (newTokens.getExpiresIn() * 1000)));
300+
}
301+
302+
// Keep refresh token if new one not provided
303+
if (newTokens.getRefreshToken() == null && this.tokens != null && this.tokens.hasRefreshToken()) {
263304
newTokens.setRefreshToken(this.tokens.getRefreshToken());
264305
}
265306

@@ -370,17 +411,37 @@ public CompletableFuture<Void> revokeOauthAppAuthorization() {
370411
}
371412

372413
// Convenience methods for token access
373-
public String getAccessToken() { return tokens != null ? tokens.getAccessToken() : null; }
374-
public String getRefreshToken() { return tokens != null ? tokens.getRefreshToken() : null; }
375-
public String getOrganizationUID() { return tokens != null ? tokens.getOrganizationUid() : null; }
376-
public String getUserUID() { return tokens != null ? tokens.getUserUid() : null; }
377-
public Long getTokenExpiryTime() { return tokens != null ? tokens.getExpiresIn() : null; }
414+
public String getAccessToken() {
415+
OAuthTokens t = _getTokens();
416+
return t != null ? t.getAccessToken() : null;
417+
}
418+
419+
public String getRefreshToken() {
420+
OAuthTokens t = _getTokens();
421+
return t != null ? t.getRefreshToken() : null;
422+
}
423+
424+
public String getOrganizationUID() {
425+
OAuthTokens t = _getTokens();
426+
return t != null ? t.getOrganizationUid() : null;
427+
}
428+
429+
public String getUserUID() {
430+
OAuthTokens t = _getTokens();
431+
return t != null ? t.getUserUid() : null;
432+
}
433+
434+
public Long getTokenExpiryTime() {
435+
OAuthTokens t = _getTokens();
436+
return t != null ? t.getExpiresIn() : null;
437+
}
378438

379439
/**
380440
* Checks if we have a valid access token
381441
* @return true if we have a non-expired access token
382442
*/
383443
public boolean hasValidAccessToken() {
384-
return tokens != null && tokens.hasAccessToken() && !tokens.isExpired();
444+
OAuthTokens t = _getTokens();
445+
return t != null && t.hasAccessToken() && !t.isExpired();
385446
}
386447
}

0 commit comments

Comments
 (0)