diff --git a/extensions/oidc-token-propagation/deployment/src/test/java/io/quarkus/oidc/token/propagation/OidcTokenPropagationTest.java b/extensions/oidc-token-propagation/deployment/src/test/java/io/quarkus/oidc/token/propagation/OidcTokenPropagationTest.java index 74af1487421298..2ad093b9bec6d9 100644 --- a/extensions/oidc-token-propagation/deployment/src/test/java/io/quarkus/oidc/token/propagation/OidcTokenPropagationTest.java +++ b/extensions/oidc-token-propagation/deployment/src/test/java/io/quarkus/oidc/token/propagation/OidcTokenPropagationTest.java @@ -2,19 +2,21 @@ import static org.hamcrest.Matchers.equalTo; -import java.util.Set; - +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import io.quarkus.test.QuarkusUnitTest; import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.oidc.client.OidcTestClient; import io.quarkus.test.oidc.server.OidcWiremockTestResource; import io.restassured.RestAssured; @QuarkusTestResource(OidcWiremockTestResource.class) public class OidcTokenPropagationTest { + final static OidcTestClient client = new OidcTestClient(); + private static Class[] testClasses = { FrontendResource.class, ProtectedResource.class, @@ -27,6 +29,11 @@ public class OidcTokenPropagationTest { .addClasses(testClasses) .addAsResource("application.properties")); + @AfterAll + public static void close() { + client.close(); + } + @Test public void testGetUserNameWithTokenPropagation() { RestAssured.given().auth().oauth2(getBearerAccessToken()) @@ -37,7 +44,7 @@ public void testGetUserNameWithTokenPropagation() { } public String getBearerAccessToken() { - return OidcWiremockTestResource.getAccessToken("alice", Set.of("admin")); + return client.getAccessToken("alice", "alice"); } } diff --git a/test-framework/oidc-server/pom.xml b/test-framework/oidc-server/pom.xml index 84b923627046f4..850d44d016a43d 100644 --- a/test-framework/oidc-server/pom.xml +++ b/test-framework/oidc-server/pom.xml @@ -29,6 +29,14 @@ io.quarkus quarkus-test-common + + io.smallrye.reactive + smallrye-mutiny-vertx-web-client + + + org.awaitility + awaitility + diff --git a/test-framework/oidc-server/src/main/java/io/quarkus/test/oidc/client/OidcTestClient.java b/test-framework/oidc-server/src/main/java/io/quarkus/test/oidc/client/OidcTestClient.java new file mode 100644 index 00000000000000..d0964004442a34 --- /dev/null +++ b/test-framework/oidc-server/src/main/java/io/quarkus/test/oidc/client/OidcTestClient.java @@ -0,0 +1,204 @@ +package io.quarkus.test.oidc.client; + +import static org.awaitility.Awaitility.await; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Map; + +import org.eclipse.microprofile.config.ConfigProvider; + +import io.vertx.core.MultiMap; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.ext.web.client.WebClient; + +public class OidcTestClient { + + private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(10); + private final static String CLIENT_AUTH_SERVER_URL_PROP = "client.quarkus.oidc.auth-server-url"; + private final static String AUTH_SERVER_URL_PROP = "quarkus.oidc.auth-server-url"; + private final static String CLIENT_ID_PROP = "quarkus.oidc.client-id"; + private final static String CLIENT_SECRET_PROP = "quarkus.oidc.credentials.secret"; + + Vertx vertx = Vertx.vertx(); + WebClient client = WebClient.create(vertx); + + private String authServerUrl; + private String tokenUrl; + + /** + * Get an access token a client_credentials grant. + * Client id must be configured with the `quarkus.oidc.client-id` property. + * Client secret must be configured with the `quarkus.oidc.credentials.secret` property. + */ + public String getClientAccessToken() { + return getClientAccessToken(null); + } + + /** + * Get an access token a client_credentials grant with additional properties. + * Client id must be configured with the `quarkus.oidc.client-id` property. + * Client secret must be configured with the `quarkus.oidc.credentials.secret` property. + */ + public String getClientAccessToken(Map extraProps) { + return getClientAccessToken(getClientId(), getClientSecret(), extraProps); + } + + /** + * Get an access token from the default tenant realm using a client_credentials grant with a + * the provided client id and secret. + */ + public String getClientAccessToken(String clientId, String clientSecret) { + return getClientAccessToken(clientId, clientSecret, null); + } + + /** + * Get an access token using a client_credentials grant with the provided client id and secret, + * and additional properties. + */ + public String getClientAccessToken(String clientId, String clientSecret, Map extraProps) { + MultiMap requestMap = MultiMap.caseInsensitiveMultiMap(); + requestMap.add("grant_type", "client_credentials") + .add("client_id", clientId); + if (clientSecret != null && !clientSecret.isBlank()) { + requestMap.add("client_secret", clientSecret); + } + return getAccessTokenInternal(requestMap, extraProps); + } + + /** + * Get an access token from the default tenant realm using a password grant with the provided user name, user secret. + * Client id must be configured with the `quarkus.oidc.client-id` property. + * Client secret must be configured with the `quarkus.oidc.credentials.secret` property. + */ + public String getAccessToken(String userName, String userSecret) { + return getAccessToken(userName, userSecret, null); + } + + /** + * Get an access token from the default tenant realm using a password grant with the provided user name, user secret, + * and additional properties. + * Client id must be configured with the `quarkus.oidc.client-id` property. + * Client secret must be configured with the `quarkus.oidc.credentials.secret` property. + */ + public String getAccessToken(String userName, String userSecret, Map extraProps) { + return getAccessToken(getClientId(), getClientSecret(), userName, userSecret, extraProps); + } + + /** + * Get an access token from the default tenant realm using a password grant with the provided client id, client secret, user + * name, user secret, client + * id and user secret. + */ + public String getAccessToken(String clientId, String clientSecret, String userName, String userSecret) { + return getAccessToken(userName, userSecret, clientId, clientSecret, null); + } + + /** + * Get an access token using a password grant with the provided user name, user secret, client + * id and secret, and scopes. + */ + public String getAccessToken(String clientId, String clientSecret, String userName, String userSecret, + Map extraProps) { + + MultiMap requestMap = MultiMap.caseInsensitiveMultiMap(); + requestMap.add("grant_type", "password") + .add("username", userName) + .add("password", userSecret); + + requestMap.add("client_id", clientId); + if (clientSecret != null && !clientSecret.isBlank()) { + requestMap.add("client_secret", clientSecret); + } + return getAccessTokenInternal(requestMap, extraProps); + } + + private String getAccessTokenInternal(MultiMap requestMap, Map extraProps) { + + if (extraProps != null) { + requestMap = requestMap.addAll(extraProps); + } + + var result = client.postAbs(getTokenUrl()) + .putHeader("Content-Type", "application/x-www-form-urlencoded") + .sendBuffer(encodeForm(requestMap)); + await().atMost(REQUEST_TIMEOUT).until(result::isComplete); + + return result.result().bodyAsJsonObject().getString("access_token"); + } + + private String getClientId() { + return getPropertyValue(CLIENT_ID_PROP); + } + + private String getClientSecret() { + return getPropertyValue(CLIENT_SECRET_PROP); + } + + /** + * Return URL string configured with a 'quarkus.oidc.auth-server' property. + */ + public String getAuthServerUrl() { + if (authServerUrl == null) { + authServerUrl = getOptionalPropertyValue(CLIENT_AUTH_SERVER_URL_PROP, AUTH_SERVER_URL_PROP); + } + return authServerUrl; + } + + /** + * Return URL string configured with a 'quarkus.oidc.auth-server' property. + */ + public String getTokenUrl() { + if (tokenUrl == null) { + getAuthServerUrl(); + var result = client.getAbs(authServerUrl + "/.well-known/openid-configuration") + .send(); + await().atMost(REQUEST_TIMEOUT).until(result::isComplete); + tokenUrl = result.result().bodyAsJsonObject().getString("token_endpoint"); + } + return tokenUrl; + } + + private String getPropertyValue(String prop) { + return ConfigProvider.getConfig().getValue(prop, String.class); + } + + private String getOptionalPropertyValue(String prop, String defaultProp) { + return ConfigProvider.getConfig().getOptionalValue(prop, String.class) + .orElseGet(() -> ConfigProvider.getConfig().getValue(defaultProp, String.class)); + } + + public static Buffer encodeForm(MultiMap form) { + Buffer buffer = Buffer.buffer(); + for (Map.Entry entry : form) { + if (buffer.length() != 0) { + buffer.appendByte((byte) '&'); + } + buffer.appendString(entry.getKey()); + buffer.appendByte((byte) '='); + buffer.appendString(urlEncode(entry.getValue())); + } + return buffer; + } + + private static String urlEncode(String value) { + try { + return URLEncoder.encode(value, StandardCharsets.UTF_8.name()); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + public void close() { + if (client != null) { + client.close(); + client = null; + } + if (vertx != null) { + vertx.close().toCompletionStage().toCompletableFuture().join(); + vertx = null; + } + } +} diff --git a/test-framework/oidc-server/src/main/java/io/quarkus/test/oidc/server/OidcWiremockTestResource.java b/test-framework/oidc-server/src/main/java/io/quarkus/test/oidc/server/OidcWiremockTestResource.java index 7b4ffff14d1f6f..8c1533edc2430c 100644 --- a/test-framework/oidc-server/src/main/java/io/quarkus/test/oidc/server/OidcWiremockTestResource.java +++ b/test-framework/oidc-server/src/main/java/io/quarkus/test/oidc/server/OidcWiremockTestResource.java @@ -205,6 +205,9 @@ public Map start() { "") .withTransformers("response-template"))); + definePasswordGrantTokenStub(); + defineClientCredGrantTokenStub(); + LOG.infof("Keycloak started in mock mode: %s", server.baseUrl()); Map conf = new HashMap<>(); conf.put("keycloak.url", server.baseUrl() + "/auth"); @@ -293,6 +296,28 @@ private void defineCodeFlowAuthorizationMockTokenStub() { "}"))); } + private void definePasswordGrantTokenStub() { + server.stubFor(post("/auth/realms/quarkus/token") + .withRequestBody(containing("grant_type=password")) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBody("{\n" + + " \"access_token\": \"" + + getAccessToken("alice", getAdminRoles()) + "\",\n" + + " \"refresh_token\": \"07e08903-1263-4dd1-9fd1-4a59b0db5283\"}"))); + } + + private void defineClientCredGrantTokenStub() { + server.stubFor(post("/auth/realms/quarkus/token") + .withRequestBody(containing("grant_type=client_credentials")) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBody("{\n" + + " \"access_token\": \"" + + getAccessToken("alice", getAdminRoles()) + "\",\n" + + " \"refresh_token\": \"07e08903-1263-4dd1-9fd1-4a59b0db5283\"}"))); + } + private void defineCodeFlowAuthorizationMockEncryptedTokenStub() { server.stubFor(post("/auth/realms/quarkus/encrypted-id-token") .withRequestBody(containing("authorization_code"))