Skip to content

Commit 96403b1

Browse files
committed
fix: Update OAuth host handling to support for regions
1 parent 10383cd commit 96403b1

File tree

3 files changed

+190
-7
lines changed

3 files changed

+190
-7
lines changed

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,11 +748,25 @@ public Builder setOAuthConfig(OAuthConfig config) {
748748
* @return Builder instance
749749
*/
750750
public Builder setOAuth(String appId, String clientId, String clientSecret, String redirectUri) {
751+
return setOAuth(appId, clientId, clientSecret, redirectUri, this.hostname);
752+
}
753+
754+
/**
755+
* Configures OAuth with client credentials and specific host
756+
* @param appId Application ID
757+
* @param clientId Client ID
758+
* @param clientSecret Client secret
759+
* @param redirectUri Redirect URI
760+
* @param host API host (e.g. "api.contentstack.io", "eu-api.contentstack.com")
761+
* @return Builder instance
762+
*/
763+
public Builder setOAuth(String appId, String clientId, String clientSecret, String redirectUri, String host) {
751764
this.oauthConfig = OAuthConfig.builder()
752765
.appId(appId)
753766
.clientId(clientId)
754767
.clientSecret(clientSecret)
755768
.redirectUri(redirectUri)
769+
.host(host)
756770
.build();
757771
return this;
758772
}
@@ -765,10 +779,23 @@ public Builder setOAuth(String appId, String clientId, String clientSecret, Stri
765779
* @return Builder instance
766780
*/
767781
public Builder setOAuthWithPKCE(String appId, String clientId, String redirectUri) {
782+
return setOAuthWithPKCE(appId, clientId, redirectUri, this.hostname);
783+
}
784+
785+
/**
786+
* Configures OAuth with PKCE (no client secret) and specific host
787+
* @param appId Application ID
788+
* @param clientId Client ID
789+
* @param redirectUri Redirect URI
790+
* @param host API host (e.g. "api.contentstack.io", "eu-api.contentstack.com")
791+
* @return Builder instance
792+
*/
793+
public Builder setOAuthWithPKCE(String appId, String clientId, String redirectUri, String host) {
768794
this.oauthConfig = OAuthConfig.builder()
769795
.appId(appId)
770796
.clientId(clientId)
771797
.redirectUri(redirectUri)
798+
.host(host)
772799
.build();
773800
return this;
774801
}

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

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public class OAuthConfig {
2525
private final String scope;
2626
private final String authEndpoint;
2727
private final String tokenEndpoint;
28+
private final String host;
2829

2930
/**
3031
* Validates the configuration
@@ -69,13 +70,16 @@ public String getFormattedAuthorizationEndpoint() {
6970
return authEndpoint;
7071
}
7172

72-
String hostname = Util.OAUTH_APP_HOST;
73+
String hostname = host != null ? host : Util.OAUTH_APP_HOST;
7374

7475
// Transform hostname if needed
7576
if (hostname.contains("contentstack")) {
7677
hostname = hostname
77-
.replaceAll("^api\\.", "app.") // api.contentstack -> app.contentstack
78-
.replaceAll("\\.io$", ".com"); // *.io -> *.com
78+
.replaceAll("-api\\.", "-app.") // eu-api.contentstack.com -> eu-app.contentstack.com
79+
.replaceAll("^api\\.", "app.") // api.contentstack.io -> app.contentstack.io
80+
.replaceAll("\\.io$", ".com"); // *.io -> *.com
81+
} else {
82+
hostname = Util.OAUTH_APP_HOST;
7983
}
8084

8185
return "https://" + hostname + String.format(Util.OAUTH_AUTHORIZE_ENDPOINT, appId);
@@ -91,13 +95,16 @@ public String getTokenEndpoint() {
9195
return tokenEndpoint;
9296
}
9397

94-
String hostname = Util.OAUTH_API_HOST;
98+
String hostname = host != null ? host : Util.OAUTH_API_HOST;
9599

96100
// Transform hostname if needed
97101
if (hostname.contains("contentstack")) {
98-
hostname = hostname
99-
.replaceAll("^dev\\d+\\.", "dev.") // dev1.* -> dev.*
100-
.replaceAll("\\.io$", ".com"); // *.io -> *.com
102+
hostname = hostname
103+
.replaceAll("-api\\.", "-developerhub-api.") // eu-api.contentstack.com -> eu-developerhub-api.contentstack.com
104+
.replaceAll("^api\\.", "developerhub-api.") // api.contentstack.io -> developerhub-api.contentstack.io
105+
.replaceAll("\\.io$", ".com"); // *.io -> *.com
106+
} else {
107+
hostname = Util.OAUTH_API_HOST;
101108
}
102109

103110
return "https://" + hostname + Util.OAUTH_TOKEN_ENDPOINT;

src/test/java/com/contentstack/cms/oauth/OAuthTest.java

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.mockito.junit.MockitoJUnitRunner;
1515

1616
import com.contentstack.cms.Contentstack;
17+
import com.contentstack.cms.core.Util;
1718
import com.contentstack.cms.models.OAuthConfig;
1819
import com.contentstack.cms.models.OAuthTokens;
1920
import com.google.gson.Gson;
@@ -195,6 +196,154 @@ public void testAuthUrlUniqueness() {
195196
assertNotEquals("URLs should be different due to different PKCE challenges", url1, url2);
196197
}
197198

199+
@Test
200+
public void testDefaultOAuthEndpoints() {
201+
// Test with default hosts
202+
OAuthConfig config = OAuthConfig.builder()
203+
.appId(TEST_APP_ID)
204+
.clientId(TEST_CLIENT_ID)
205+
.redirectUri(TEST_REDIRECT_URI)
206+
.build();
207+
OAuthHandler handler = new OAuthHandler(mockHttpClient, config);
208+
209+
String authUrl = handler.authorize();
210+
String tokenUrl = config.getTokenEndpoint();
211+
212+
assertTrue("Auth URL should use default app host",
213+
authUrl.contains(Util.OAUTH_APP_HOST));
214+
assertTrue("Token URL should use default API host",
215+
tokenUrl.contains(Util.OAUTH_API_HOST));
216+
}
217+
218+
@Test
219+
public void testHostTransformations() {
220+
// Test cases: {API Host, Expected App Host, Expected Token Host}
221+
String[][] testCases = {
222+
// Default region
223+
{"api.contentstack.io", "app.contentstack.com", "developerhub-api.contentstack.com"},
224+
{"api-contentstack.com", "app-contentstack.com", "developerhub-api-contentstack.com"},
225+
226+
// AWS regions
227+
{"eu-api.contentstack.com", "eu-app.contentstack.com", "eu-developerhub-api.contentstack.com"},
228+
{"eu-api-contentstack.com", "eu-app-contentstack.com", "eu-developerhub-api-contentstack.com"},
229+
{"au-api.contentstack.com", "au-app.contentstack.com", "au-developerhub-api.contentstack.com"},
230+
{"au-api-contentstack.com", "au-app-contentstack.com", "au-developerhub-api-contentstack.com"},
231+
232+
// Azure regions
233+
{"azure-na-api.contentstack.com", "azure-na-app.contentstack.com", "azure-na-developerhub-api.contentstack.com"},
234+
{"azure-na-api-contentstack.com", "azure-na-app-contentstack.com", "azure-na-developerhub-api-contentstack.com"},
235+
{"azure-eu-api.contentstack.com", "azure-eu-app.contentstack.com", "azure-eu-developerhub-api.contentstack.com"},
236+
{"azure-eu-api-contentstack.com", "azure-eu-app-contentstack.com", "azure-eu-developerhub-api-contentstack.com"},
237+
238+
// GCP regions
239+
{"gcp-na-api.contentstack.com", "gcp-na-app.contentstack.com", "gcp-na-developerhub-api.contentstack.com"},
240+
{"gcp-na-api-contentstack.com", "gcp-na-app-contentstack.com", "gcp-na-developerhub-api-contentstack.com"},
241+
{"gcp-eu-api.contentstack.com", "gcp-eu-app.contentstack.com", "gcp-eu-developerhub-api.contentstack.com"},
242+
{"gcp-eu-api-contentstack.com", "gcp-eu-app-contentstack.com", "gcp-eu-developerhub-api-contentstack.com"}
243+
};
244+
245+
for (String[] testCase : testCases) {
246+
String apiHost = testCase[0];
247+
String expectedAppHost = testCase[1];
248+
String expectedTokenHost = testCase[2];
249+
250+
OAuthConfig config = OAuthConfig.builder()
251+
.appId(TEST_APP_ID)
252+
.clientId(TEST_CLIENT_ID)
253+
.redirectUri(TEST_REDIRECT_URI)
254+
.host(apiHost)
255+
.build();
256+
OAuthHandler handler = new OAuthHandler(mockHttpClient, config);
257+
258+
String authUrl = handler.authorize();
259+
String tokenUrl = config.getTokenEndpoint();
260+
261+
assertTrue(String.format("Auth URL for %s should contain %s", apiHost, expectedAppHost),
262+
authUrl.contains(expectedAppHost));
263+
assertTrue(String.format("Token URL for %s should contain %s", apiHost, expectedTokenHost),
264+
tokenUrl.contains(expectedTokenHost));
265+
}
266+
}
267+
268+
@Test
269+
public void testHostStorage() {
270+
String testHost = "eu-api.contentstack.com";
271+
272+
// Test host storage in OAuthConfig
273+
OAuthConfig config = OAuthConfig.builder()
274+
.appId(TEST_APP_ID)
275+
.clientId(TEST_CLIENT_ID)
276+
.redirectUri(TEST_REDIRECT_URI)
277+
.host(testHost)
278+
.build();
279+
280+
assertEquals("Host should be stored in OAuthConfig",
281+
testHost, config.getHost());
282+
283+
// Test host storage via Contentstack.Builder
284+
Contentstack client = new Contentstack.Builder()
285+
.setOAuth(TEST_APP_ID, TEST_CLIENT_ID, TEST_CLIENT_SECRET, TEST_REDIRECT_URI, testHost)
286+
.build();
287+
288+
String authUrl = client.getOAuthAuthorizationUrl();
289+
assertTrue("Auth URL should use stored host",
290+
authUrl.contains("eu-app.contentstack.com"));
291+
292+
// Test host storage via PKCE builder
293+
client = new Contentstack.Builder()
294+
.setOAuthWithPKCE(TEST_APP_ID, TEST_CLIENT_ID, TEST_REDIRECT_URI, testHost)
295+
.build();
296+
297+
authUrl = client.getOAuthAuthorizationUrl();
298+
assertTrue("Auth URL should use stored host with PKCE",
299+
authUrl.contains("eu-app.contentstack.com"));
300+
}
301+
302+
@Test
303+
public void testDefaultHosts() {
304+
// Test with no host specified
305+
OAuthConfig config = OAuthConfig.builder()
306+
.appId(TEST_APP_ID)
307+
.clientId(TEST_CLIENT_ID)
308+
.redirectUri(TEST_REDIRECT_URI)
309+
.build();
310+
OAuthHandler handler = new OAuthHandler(mockHttpClient, config);
311+
312+
String authUrl = handler.authorize();
313+
String tokenUrl = config.getTokenEndpoint();
314+
315+
assertTrue("Auth URL should use default app host",
316+
authUrl.contains(Util.OAUTH_APP_HOST));
317+
assertTrue("Token URL should use default API host",
318+
tokenUrl.contains(Util.OAUTH_API_HOST));
319+
}
320+
321+
@Test
322+
public void testCustomEndpoints() {
323+
// Test with custom endpoints
324+
String customAuthEndpoint = "https://custom.auth.endpoint";
325+
String customTokenEndpoint = "https://custom.token.endpoint";
326+
327+
OAuthConfig config = OAuthConfig.builder()
328+
.appId(TEST_APP_ID)
329+
.clientId(TEST_CLIENT_ID)
330+
.redirectUri(TEST_REDIRECT_URI)
331+
.authEndpoint(customAuthEndpoint)
332+
.tokenEndpoint(customTokenEndpoint)
333+
.build();
334+
OAuthHandler handler = new OAuthHandler(mockHttpClient, config);
335+
336+
String authUrl = handler.authorize();
337+
String tokenUrl = config.getTokenEndpoint();
338+
339+
assertEquals("Should use custom auth endpoint",
340+
customAuthEndpoint, authUrl);
341+
assertEquals("Should use custom token endpoint",
342+
customTokenEndpoint, tokenUrl);
343+
}
344+
345+
346+
198347
// =================
199348
// TOKEN EXCHANGE TESTS
200349
// =================

0 commit comments

Comments
 (0)