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));
+            }
+        }
+    }
 }

Reply via email to