This is an automated email from the ASF dual-hosted git repository.
jamesnetherton pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git
The following commit(s) were added to refs/heads/main by this push:
new 939980242f Closes #8090. Increase test coverage for Keycloak
authentication methods
939980242f is described below
commit 939980242ffd5fc9c6d2e023a5f59b3d8474bc5a
Author: Jomin <[email protected]>
AuthorDate: Tue Mar 17 13:11:37 2026 +0000
Closes #8090. Increase test coverage for Keycloak authentication methods
* Fixes #8090. Increase test coverage for Keycloak authentication methods
- Add client credentials grant
tes(service-to-service auth)
- Add refresh token grant tests (token rotation and renewal)
- Add password grant tests with refresh token support
- Test both full response and convenience methods for each grant type
* Fixes #8090. Increase test coverage for Keycloak authentication methods
Renamed couple of tests with more meaningful names
---------
Co-authored-by: jomin mathew <>
---
.../it/KeycloakEvaluatePermissionTest.java | 134 +++++++++++++++++++--
.../component/keycloak/it/KeycloakTestBase.java | 115 +++++++++++++++++-
2 files changed, 241 insertions(+), 8 deletions(-)
diff --git
a/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakEvaluatePermissionTest.java
b/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakEvaluatePermissionTest.java
index a24e15e238..128d44b818 100644
---
a/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakEvaluatePermissionTest.java
+++
b/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakEvaluatePermissionTest.java
@@ -44,6 +44,7 @@ import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
@QuarkusTest
@QuarkusTestResource(KeycloakTestResource.class)
@@ -51,8 +52,11 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
public class KeycloakEvaluatePermissionTest extends KeycloakTestBase {
private static String userToken;
+ private static String userRefreshToken;
private static final String RESOURCE_DOCUMENTS = "documents";
private static final String SCOPE_READ = "read";
+ private static final String SERVICE_ACCOUNT_CLIENT_ID =
"test-service-account-"
+ + UUID.randomUUID().toString().substring(0, 8);
@Test
@Order(1)
@@ -68,13 +72,7 @@ public class KeycloakEvaluatePermissionTest extends
KeycloakTestBase {
authzClient.setServiceAccountsEnabled(true);
authzClient.setAuthorizationServicesEnabled(true);
authzClient.setStandardFlowEnabled(false);
-
- given()
- .contentType(ContentType.JSON)
- .body(authzClient)
- .post("/keycloak/client/{realmName}/pojo",
config("test.realm"))
- .then()
- .statusCode(anyOf(is(200), is(201)));
+ createClient(authzClient);
// 2. Protected resource with a read scope
ScopeRepresentation readScope = new ScopeRepresentation();
@@ -312,6 +310,128 @@ public class KeycloakEvaluatePermissionTest extends
KeycloakTestBase {
.body(containsString("401"));
}
+ // ==================== Authentication Method Tests ====================
+
+ @Test
+ @Order(15)
+ public void testSetupAuthenticationTests() {
+ // Create service account client for client credentials grant testing
+ ClientRepresentation serviceAccountClient = new ClientRepresentation();
+ serviceAccountClient.setClientId(SERVICE_ACCOUNT_CLIENT_ID);
+ serviceAccountClient.setSecret(TEST_CLIENT_SECRET);
+ serviceAccountClient.setPublicClient(false);
+ serviceAccountClient.setServiceAccountsEnabled(true);
+ serviceAccountClient.setDirectAccessGrantsEnabled(false);
+ serviceAccountClient.setStandardFlowEnabled(false);
+ createClient(serviceAccountClient);
+ }
+
+ @Test
+ @Order(20)
+ public void testServiceAccount_FullResponse() {
+ // Verify we can obtain a full token response using client credentials
grant
+ Map<String, Object> tokenResponse = getServiceAccountTokenResponse(
+ SERVICE_ACCOUNT_CLIENT_ID, TEST_CLIENT_SECRET);
+
+ assertNotNull(tokenResponse, "Token response should not be null");
+ assertNotNull(tokenResponse.get("access_token"), "Access token should
be present in response");
+ assertNotNull(tokenResponse.get("expires_in"), "Token expiration
should be present in response");
+ assertNotNull(tokenResponse.get("token_type"), "Token type should be
present in response");
+ }
+
+ @Test
+ @Order(21)
+ public void testServiceAccount_ConvenienceMethod() {
+ // Verify the convenience method that returns just the access token
+ String accessToken = getServiceAccountToken(
+ SERVICE_ACCOUNT_CLIENT_ID, TEST_CLIENT_SECRET);
+
+ assertNotNull(accessToken, "Access token should not be null");
+ }
+
+ @Test
+ @Order(30)
+ public void testPasswordGrant_ObtainTokenWithRefreshToken() {
+ // Obtain a token using password grant to get both access and refresh
tokens
+ Map<String, Object> tokenResponse = getTokenResponse(
+ TEST_USER_NAME,
+ TEST_USER_PASSWORD,
+ TEST_CLIENT_ID,
+ TEST_CLIENT_SECRET);
+
+ assertNotNull(tokenResponse, "Token response should not be null");
+ assertNotNull(tokenResponse.get("access_token"), "Access token should
be present in response");
+ assertNotNull(tokenResponse.get("refresh_token"), "Refresh token
should be present in response");
+ assertNotNull(tokenResponse.get("expires_in"), "Token expiration
should be present in response");
+
+ // Store refresh token for subsequent refresh token tests
+ userRefreshToken = (String) tokenResponse.get("refresh_token");
+ assertNotNull(userRefreshToken, "Stored user refresh token should not
be null");
+ }
+
+ @Test
+ @Order(40)
+ public void testRefreshTokenGrant_FullResponse() {
+ // Verify refresh token can be used to obtain a full token response
+ Map<String, Object> refreshedTokenResponse = getRefreshedTokenResponse(
+ userRefreshToken,
+ TEST_CLIENT_ID,
+ TEST_CLIENT_SECRET);
+
+ assertNotNull(refreshedTokenResponse, "Refreshed token response should
not be null");
+ assertNotNull(refreshedTokenResponse.get("access_token"), "New access
token should be present in response");
+ assertNotNull(refreshedTokenResponse.get("refresh_token"), "New
refresh token should be present in response");
+ assertNotNull(refreshedTokenResponse.get("expires_in"), "Token
expiration should be present in response");
+ }
+
+ @Test
+ @Order(41)
+ public void testRefreshTokenGrant_ConvenienceMethod() {
+ // Verify the convenience method that returns just the new access token
+ String newAccessToken = getRefreshedAccessToken(
+ userRefreshToken,
+ TEST_CLIENT_ID,
+ TEST_CLIENT_SECRET);
+
+ assertNotNull(newAccessToken, "Refreshed access token should not be
null");
+ }
+
+ @Test
+ @Order(43)
+ public void testRefreshTokenGrant_InvalidRefreshToken() {
+ RuntimeException exception = assertThrows(RuntimeException.class, ()
-> {
+ getRefreshedTokenResponse("invalid.refresh.token", TEST_CLIENT_ID,
TEST_CLIENT_SECRET);
+ }, "Should have thrown an exception for invalid refresh token");
+
+ assertNotNull(exception.getMessage(), "Exception message should not be
null");
+ }
+
+ @Test
+ @Order(44)
+ public void testRefreshTokenGrant_MultipleRefreshes() {
+ // Test that we can refresh multiple times (token rotation)
+ // First refresh using the user's refresh token
+ Map<String, Object> firstRefresh = getRefreshedTokenResponse(
+ userRefreshToken,
+ TEST_CLIENT_ID,
+ TEST_CLIENT_SECRET);
+
+ assertNotNull(firstRefresh.get("access_token"), "First refresh should
return new access token");
+ assertNotNull(firstRefresh.get("refresh_token"), "First refresh should
return new refresh token");
+
+ String secondRefreshToken = (String) firstRefresh.get("refresh_token");
+ assertNotNull(secondRefreshToken, "Second refresh token should not be
null");
+
+ // Second refresh using the new refresh token
+ Map<String, Object> secondRefresh = getRefreshedTokenResponse(
+ secondRefreshToken,
+ TEST_CLIENT_ID,
+ TEST_CLIENT_SECRET);
+
+ assertNotNull(secondRefresh.get("access_token"), "Second refresh
should return new access token");
+ assertNotNull(secondRefresh.get("refresh_token"), "Second refresh
should return new refresh token");
+ }
+
@Test
@Order(100)
public void testCleanup_DeleteRealm() {
diff --git
a/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakTestBase.java
b/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakTestBase.java
index 9d8660b7ac..72feed8589 100644
---
a/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakTestBase.java
+++
b/integration-tests/keycloak/src/test/java/org/apache/camel/quarkus/component/keycloak/it/KeycloakTestBase.java
@@ -25,6 +25,7 @@ import io.quarkus.test.common.QuarkusTestResource;
import io.restassured.RestAssured;
import io.restassured.config.ObjectMapperConfig;
import io.restassured.config.RestAssuredConfig;
+import io.restassured.http.ContentType;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
@@ -33,6 +34,10 @@ import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.config.ConfigProvider;
import org.junit.jupiter.api.BeforeAll;
+import org.keycloak.representations.idm.ClientRepresentation;
+
+import static org.hamcrest.CoreMatchers.anyOf;
+import static org.hamcrest.CoreMatchers.is;
/**
* Base class for Keycloak integration tests.
@@ -77,8 +82,35 @@ public abstract class KeycloakTestBase {
return ConfigProvider.getConfig().getValue(name, String.class);
}
+ /**
+ * Helper method to create a Keycloak client via REST API.
+ * Reduces duplication when creating multiple clients in tests.
+ */
+ protected void createClient(ClientRepresentation client) {
+ RestAssured.given()
+ .contentType(ContentType.JSON)
+ .body(client)
+ .post("/keycloak/client/{realmName}/pojo",
config("test.realm"))
+ .then()
+ .statusCode(anyOf(is(200), is(201)));
+ }
+
+ /**
+ * Obtain access token using Resource Owner Password Credentials grant
(username/password).
+ * Returns only the access_token string.
+ */
protected String getAccessToken(String username, String password,
String clientId, String clientSecret) {
+ Map<String, Object> tokenResponse = getTokenResponse(username,
password, clientId, clientSecret);
+ return (String) tokenResponse.get("access_token");
+ }
+
+ /**
+ * Obtain full token response using Resource Owner Password Credentials
grant.
+ * Returns map containing access_token, refresh_token, expires_in, etc.
+ */
+ protected Map<String, Object> getTokenResponse(String username, String
password,
+ String clientId, String clientSecret) {
try (Client client = ClientBuilder.newClient()) {
String tokenUrl =
String.format("%s/realms/%s/protocol/openid-connect/token",
config("keycloak.url"), config("test.realm"));
@@ -97,11 +129,92 @@ public abstract class KeycloakTestBase {
if (response.getStatus() == 200) {
@SuppressWarnings("unchecked")
Map<String, Object> body = response.readEntity(Map.class);
- return (String) body.get("access_token");
+ return body;
}
throw new RuntimeException("Failed to get token for " +
username
+ " [" + response.getStatus() + "]: " +
response.readEntity(String.class));
}
}
}
+
+ /**
+ * Obtain service account access token (service-to-service authentication).
+ * Uses the OAuth 2.0 Client Credentials grant type.
+ * This grant type does not require user credentials.
+ */
+ protected String getServiceAccountToken(String clientId, String
clientSecret) {
+ Map<String, Object> tokenResponse =
getServiceAccountTokenResponse(clientId, clientSecret);
+ return (String) tokenResponse.get("access_token");
+ }
+
+ /**
+ * Obtain full service account token response.
+ * Uses the OAuth 2.0 Client Credentials grant type.
+ * Returns map containing access_token, expires_in, etc.
+ */
+ protected Map<String, Object> getServiceAccountTokenResponse(String
clientId, String clientSecret) {
+ try (Client client = ClientBuilder.newClient()) {
+ String tokenUrl =
String.format("%s/realms/%s/protocol/openid-connect/token",
+ config("keycloak.url"), config("test.realm"));
+
+ Form form = new Form()
+ .param("grant_type", "client_credentials")
+ .param("client_id", clientId)
+ .param("client_secret", clientSecret);
+
+ try (Response response = client.target(tokenUrl)
+ .request(MediaType.APPLICATION_JSON)
+ .post(Entity.entity(form,
MediaType.APPLICATION_FORM_URLENCODED))) {
+
+ if (response.getStatus() == 200) {
+ @SuppressWarnings("unchecked")
+ Map<String, Object> body = response.readEntity(Map.class);
+ return body;
+ }
+ throw new RuntimeException("Failed to get token via client
credentials for " + clientId
+ + " [" + response.getStatus() + "]: " +
response.readEntity(String.class));
+ }
+ }
+ }
+
+ /**
+ * Get a refreshed access token using a refresh token.
+ * Uses the OAuth 2.0 Refresh Token grant type.
+ * This allows obtaining a new access token without re-authenticating.
+ */
+ protected String getRefreshedAccessToken(String refreshToken, String
clientId, String clientSecret) {
+ Map<String, Object> tokenResponse =
getRefreshedTokenResponse(refreshToken, clientId, clientSecret);
+ return (String) tokenResponse.get("access_token");
+ }
+
+ /**
+ * Get full refreshed token response using a refresh token.
+ * Uses the OAuth 2.0 Refresh Token grant type.
+ * Returns map containing new access_token, refresh_token, expires_in, etc.
+ */
+ protected Map<String, Object> getRefreshedTokenResponse(String
refreshToken, String clientId, String clientSecret) {
+ try (Client client = ClientBuilder.newClient()) {
+ String tokenUrl =
String.format("%s/realms/%s/protocol/openid-connect/token",
+ config("keycloak.url"), config("test.realm"));
+
+ Form form = new Form()
+ .param("grant_type", "refresh_token")
+ .param("client_id", clientId)
+ .param("client_secret", clientSecret)
+ .param("refresh_token", refreshToken);
+
+ try (Response response = client.target(tokenUrl)
+ .request(MediaType.APPLICATION_JSON)
+ .post(Entity.entity(form,
MediaType.APPLICATION_FORM_URLENCODED))) {
+
+ if (response.getStatus() == 200) {
+ @SuppressWarnings("unchecked")
+ Map<String, Object> body = response.readEntity(Map.class);
+ return body;
+ }
+ throw new RuntimeException("Failed to refresh token"
+ + " [" + response.getStatus() + "]: " +
response.readEntity(String.class));
+ }
+ }
+ }
}