From 726291ca55e10f6abb22d9ee3c902ae360f20a7b Mon Sep 17 00:00:00 2001 From: Mauro Mugnaini Date: Mon, 22 Apr 2024 17:50:00 +0200 Subject: [PATCH] Added custom base URL set via factory (not automatically working cross environments) [#27234] Better tests for exchange-token features --- CHANGELOG.md | 1 + .../keycloak/DefaultKeycloakClient.java | 36 +++++-- .../keycloak/KeycloakClientFactory.java | 25 ++++- .../common/keycloak/TestKeycloakClient.java | 101 +++++++++++------- 4 files changed, 115 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5d81ff..7f71847 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [v2.1.0-SNAPSHOT] - Added `token-exchange` support, also with `offline-token` scope, and methods to add extra headers during the OIDC token requests. +- Added custom base URL set via factory (not automatically working cross environments) [#27234]. ## [v2.0.0] - Removed the discovery functionality to be compatible with SmartGears.v4 and moved to the new library `keycloak-client-legacy-is` that will provide the backward compatibility. (#23478). diff --git a/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java b/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java index e091446..e592f89 100644 --- a/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java +++ b/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java @@ -52,7 +52,21 @@ public class DefaultKeycloakClient implements KeycloakClient { protected final static String AUTHORIZATION_HEADER = "Authorization"; - public static final String BASE_URL = "https://url.d4science.org/auth/realms/"; + public static final String DEFAULT_BASE_URL = "https://url.d4science.org/auth/realms/"; + + private String customBaseURL = null; + + public void setCustomBaseURL(String customBaseURL) { + if (customBaseURL == null || customBaseURL.endsWith("/")) { + this.customBaseURL = customBaseURL; + } else { + this.customBaseURL = customBaseURL += "/"; + } + } + + public String getCustomBaseURL() { + return customBaseURL; + } @Override public URL getRealmBaseURL(String context) throws KeycloakClientException { @@ -61,16 +75,20 @@ public class DefaultKeycloakClient implements KeycloakClient { @Override public URL getRealmBaseURL(String context, String realm) throws KeycloakClientException { - String urlString = BASE_URL + realm + "/"; - if (!context.startsWith(PROD_ROOT_SCOPE)) { - String root = checkContext(context).split("/")[1]; - urlString = urlString.replace("url", "url." + root.replaceAll("\\.", "-")); + String realmBaseURLString = null; + if (getCustomBaseURL() != null) { + realmBaseURLString = getCustomBaseURL() + realm + "/"; + } else { + realmBaseURLString = DEFAULT_BASE_URL + realm + "/"; + if (!context.startsWith(PROD_ROOT_SCOPE)) { + String root = checkContext(context).split("/")[1]; + realmBaseURLString = realmBaseURLString.replace("url", "url." + root.replaceAll("\\.", "-")); + } } try { - return new URL(urlString); + return new URL(realmBaseURLString); } catch (MalformedURLException e) { - // That should be almost impossible - logger.warn("Cannot create base URL from string: {}", urlString, e); + logger.error("Cannot create base URL from string: {}", realmBaseURLString, e); return null; } } @@ -192,7 +210,7 @@ public class DefaultKeycloakClient implements KeycloakClient { String username, String password, String audience, Map extraHeaders) throws KeycloakClientException { return queryOIDCTokenOfUserWithContext(getTokenEndpointURL(getRealmBaseURL(context)), clientId, clientSecret, - username, password, null, extraHeaders); + username, password, audience, extraHeaders); } @Override diff --git a/src/main/java/org/gcube/common/keycloak/KeycloakClientFactory.java b/src/main/java/org/gcube/common/keycloak/KeycloakClientFactory.java index 9c1d64c..a14b006 100644 --- a/src/main/java/org/gcube/common/keycloak/KeycloakClientFactory.java +++ b/src/main/java/org/gcube/common/keycloak/KeycloakClientFactory.java @@ -7,9 +7,32 @@ public class KeycloakClientFactory { protected static final Logger logger = LoggerFactory.getLogger(KeycloakClientFactory.class); + protected static String CUSTOM_BASE_URL; + + public static void setCustomBaseURL(String customBaseURL) { + if (customBaseURL != null) { + logger.info("Setting custom base URL static value to {}", customBaseURL); + } else { + logger.info("Removing custom base URL static value"); + } + if (customBaseURL == null || customBaseURL.endsWith("/")) { + CUSTOM_BASE_URL = customBaseURL; + } else { + CUSTOM_BASE_URL = customBaseURL += "/"; + } + } + + public static String getCustomBaseURL() { + return CUSTOM_BASE_URL; + } + public static KeycloakClient newInstance() { logger.debug("Instantiating a new keycloak client instance"); - return new DefaultKeycloakClient(); + DefaultKeycloakClient newInstance = new DefaultKeycloakClient(); + if (getCustomBaseURL() != null) { + newInstance.setCustomBaseURL(CUSTOM_BASE_URL); + } + return newInstance; } } \ No newline at end of file diff --git a/src/test/java/org/gcube/common/keycloak/TestKeycloakClient.java b/src/test/java/org/gcube/common/keycloak/TestKeycloakClient.java index 7a4151f..4d31847 100644 --- a/src/test/java/org/gcube/common/keycloak/TestKeycloakClient.java +++ b/src/test/java/org/gcube/common/keycloak/TestKeycloakClient.java @@ -45,6 +45,8 @@ public class TestKeycloakClient { @After public void tearDown() throws Exception { + // Assure to reset the factory's default base URL + KeycloakClientFactory.setCustomBaseURL(null); } @Test @@ -105,6 +107,23 @@ public class TestKeycloakClient { Assert.assertEquals(devAvatarURL.toString(), avatarURL.toString()); } + @Test + public void test04CustomEndpointTest() throws Exception { + String customBase = "https://accounts.cloud.dev.d4science.org/auth/realms/"; + logger.info("*** [0.4] Start testing Keycloak token endpoint construction from custom base URL {}...", + customBase); + + KeycloakClientFactory.setCustomBaseURL(customBase); + KeycloakClient client = KeycloakClientFactory.newInstance(); + URL customBaseURL = client.getRealmBaseURL(DEV_ROOT_CONTEXT); + logger.info("*** [0.4] Constructed token URL is: {}", customBaseURL); + URL devTokenURL = new URL(DEV_TOKEN_ENDPOINT); + logger.info("*** [0.4] DEV token URL is: {}", devTokenURL); + Assert.assertNotNull(customBaseURL); + Assert.assertEquals(customBaseURL.getProtocol(), "https"); + Assert.assertEquals(customBase + KeycloakClient.DEFAULT_REALM + "/", customBaseURL.toString()); + } + @Test public void test12QueryOIDCToken() throws Exception { logger.info("*** [1.2] Start testing query OIDC token from Keycloak with context..."); @@ -145,13 +164,28 @@ public class TestKeycloakClient { @Test public void test13aQueryOIDCTokenOfUserWithContext() throws Exception { logger.info("*** [1.3a] Start testing query OIDC token for audience from Keycloak with context for user..."); - TokenResponse oidcTR = KeycloakClientFactory.newInstance().queryOIDCTokenOfUserWithContext(DEV_ROOT_CONTEXT, - CLIENT_ID, CLIENT_SECRET, TEST_USER_USERNAME, TEST_USER_PASSWORD, TEST_AUDIENCE); + TokenResponse oidcTR = KeycloakClientFactory.newInstance().queryOIDCTokenOfUser(DEV_ROOT_CONTEXT, + CLIENT_ID, CLIENT_SECRET, TEST_USER_USERNAME, TEST_USER_PASSWORD); logger.info("*** [1.3a] OIDC access token: {}", oidcTR.getAccessToken()); logger.info("*** [1.3a] OIDC refresh token: {}", oidcTR.getRefreshToken()); TestModels.checkTokenResponse(oidcTR); TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcTR), TEST_USER_USERNAME, true); + + TokenResponse oidcRestrictedTR = KeycloakClientFactory.newInstance().queryOIDCTokenOfUserWithContext( + DEV_ROOT_CONTEXT, CLIENT_ID, CLIENT_SECRET, TEST_USER_USERNAME, TEST_USER_PASSWORD, + TOKEN_RESTRICTION_VRE_CONTEXT); + + logger.info("*** [1.3a] OIDC restricted access token: {}", oidcRestrictedTR.getAccessToken()); + logger.info("*** [1.3a] OIDC restricted refresh token: {}", oidcRestrictedTR.getRefreshToken()); + TestModels.checkTokenResponse(oidcTR); + TestModels.checkTokenResponse(oidcRestrictedTR); + TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcTR), TEST_USER_USERNAME, true); + TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcRestrictedTR), TEST_USER_USERNAME, true); + assertTrue(ModelUtils.getAccessTokenFrom(oidcTR).getAudience().length > 1); + assertTrue(ModelUtils.getAccessTokenFrom(oidcRestrictedTR).getAudience().length == 1); + assertTrue( + TOKEN_RESTRICTION_VRE_CONTEXT.equals(ModelUtils.getAccessTokenFrom(oidcRestrictedTR).getAudience()[0])); } @Test @@ -178,28 +212,18 @@ public class TestKeycloakClient { @Test public void test13aQueryOIDCTokenOfUserWithContextAndCustomHeader() throws Exception { logger.info( - "*** [1.3c] Start testing query OIDC token for audience from Keycloak with context and custom header for user..."); + "*** [1.3c] Start testing query OIDC token for audience from Keycloak with context and custom headers for user..."); TokenResponse oidcTR = KeycloakClientFactory.newInstance().queryOIDCTokenOfUserWithContext(DEV_ROOT_CONTEXT, - CLIENT_ID, CLIENT_SECRET, TEST_USER_USERNAME, TEST_USER_PASSWORD, TOKEN_RESTRICTION_VRE_CONTEXT); + CLIENT_ID, CLIENT_SECRET, TEST_USER_USERNAME, TEST_USER_PASSWORD, TOKEN_RESTRICTION_VRE_CONTEXT, + Collections.singletonMap("X_A_CUSTOM_HEADER", "HEADER_VALUE")); logger.info("*** [1.3c] OIDC access token: {}", oidcTR.getAccessToken()); logger.info("*** [1.3c] OIDC refresh token: {}", oidcTR.getRefreshToken()); - TokenResponse oidcRestrictedTR = KeycloakClientFactory.newInstance().queryOIDCTokenOfUserWithContext( - DEV_ROOT_CONTEXT, - CLIENT_ID, CLIENT_SECRET, TEST_USER_USERNAME, TEST_USER_PASSWORD, TOKEN_RESTRICTION_VRE_CONTEXT, - Collections.singletonMap(KeycloakClient.D4S_CONTEXT_HEADER_NAME, TOKEN_RESTRICTION_VRE_CONTEXT)); - - logger.info("*** [1.3c] OIDC restricted access token: {}", oidcRestrictedTR.getAccessToken()); - logger.info("*** [1.3c] OIDC restricted refresh token: {}", oidcRestrictedTR.getRefreshToken()); TestModels.checkTokenResponse(oidcTR); - TestModels.checkTokenResponse(oidcRestrictedTR); TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcTR), TEST_USER_USERNAME, true); - TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcRestrictedTR), TEST_USER_USERNAME, true); - assertTrue(ModelUtils.getAccessTokenFrom(oidcTR).getAudience().length > 1); - assertTrue(ModelUtils.getAccessTokenFrom(oidcRestrictedTR).getAudience().length == 1); - assertTrue( - TOKEN_RESTRICTION_VRE_CONTEXT.equals(ModelUtils.getAccessTokenFrom(oidcRestrictedTR).getAudience()[0])); + assertTrue(ModelUtils.getAccessTokenFrom(oidcTR).getAudience().length == 1); + // It is not possible to check programmatically if the header has been added to the call, See the logs if the Header is present. } @Test @@ -443,27 +467,28 @@ public class TestKeycloakClient { client.introspectAccessToken(DEV_ROOT_CONTEXT, CLIENT_ID, CLIENT_SECRET, exchangedTR.getAccessToken())); } - @Test - public void test53ExchangeToken4Offline() throws Exception { - logger.info("*** [5.3] Start testing token exchange for offline token from Keycloak..."); - KeycloakClient client = KeycloakClientFactory.newInstance(); - TokenResponse oidcTR = client.queryOIDCTokenOfUser(DEV_ROOT_CONTEXT, CLIENT_ID, CLIENT_SECRET, TEST_USER_USERNAME, - TEST_USER_PASSWORD); - - logger.info("*** [5.3] OIDC access token: {}", oidcTR.getAccessToken()); - - TokenResponse exchangedTR = client.exchangeTokenForOfflineToken(DEV_ROOT_CONTEXT, oidcTR, CLIENT_ID, - CLIENT_SECRET, CLIENT_ID); - - logger.info("*** [5.3] Exchanged access token: {}", exchangedTR.getAccessToken()); - logger.info("*** [5.3] Exchanged refresh token: {}", exchangedTR.getRefreshToken()); - TestModels.checkTokenResponse(exchangedTR, true); - TestModels.checkOfflineToken(exchangedTR); - - TestModels.checkTokenIntrospectionResponse(client.introspectAccessToken(DEV_ROOT_CONTEXT, CLIENT_ID, - CLIENT_SECRET, exchangedTR.getAccessToken())); - - } + @Test + public void test53ExchangeToken4Offline() throws Exception { + logger.info("*** [5.3] Start testing token exchange for offline token from Keycloak..."); + KeycloakClient client = KeycloakClientFactory.newInstance(); + TokenResponse oidcTR = client.queryOIDCTokenOfUser(DEV_ROOT_CONTEXT, CLIENT_ID, CLIENT_SECRET, + TEST_USER_USERNAME, + TEST_USER_PASSWORD); + + logger.info("*** [5.3] OIDC access token: {}", oidcTR.getAccessToken()); + + TokenResponse exchangedTR = client.exchangeTokenForOfflineToken(DEV_ROOT_CONTEXT, oidcTR, CLIENT_ID, + CLIENT_SECRET, CLIENT_ID); + + logger.info("*** [5.3] Exchanged access token: {}", exchangedTR.getAccessToken()); + logger.info("*** [5.3] Exchanged refresh token: {}", exchangedTR.getRefreshToken()); + TestModels.checkTokenResponse(exchangedTR, true); + TestModels.checkOfflineToken(exchangedTR); + + TestModels.checkTokenIntrospectionResponse(client.introspectAccessToken(DEV_ROOT_CONTEXT, CLIENT_ID, + CLIENT_SECRET, exchangedTR.getAccessToken())); + + } // @Test // public void test6GetAvatar() throws Exception {