Added custom base URL set via factory (not automatically working cross environments) [#27234]

Better tests for exchange-token features
This commit is contained in:
Mauro Mugnaini 2024-04-22 17:50:00 +02:00
parent 35c913db02
commit 726291ca55
Signed by: mauro.mugnaini
GPG Key ID: 2440CFD0EB321EA8
4 changed files with 115 additions and 48 deletions

View File

@ -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).

View File

@ -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<String, String> extraHeaders) throws KeycloakClientException {
return queryOIDCTokenOfUserWithContext(getTokenEndpointURL(getRealmBaseURL(context)), clientId, clientSecret,
username, password, null, extraHeaders);
username, password, audience, extraHeaders);
}
@Override

View File

@ -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;
}
}

View File

@ -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 {