From 80995537eabf064761607932c0f50e9706ca0c6d Mon Sep 17 00:00:00 2001 From: Luke Morfill Date: Wed, 15 Nov 2023 21:55:37 +0000 Subject: [PATCH 1/4] Add support for GraphQL WebSocket authorization --- .github/native-tests.json | 2 +- .../deployment/pom.xml | 10 + ...phQLClientWebSocketAuthenticationTest.java | 207 +++++++++++++++ .../resources/application-secured.properties | 9 + .../src/test/resources/roles.properties | 1 + .../src/test/resources/users.properties | 1 + .../deployment/SmallRyeGraphQLProcessor.java | 3 +- .../SmallRyeGraphQLOverWebSocketHandler.java | 28 +- integration-tests/pom.xml | 1 + .../smallrye-graphql-client-keycloak/pom.xml | 247 ++++++++++++++++++ .../keycloak/GraphQLAuthExpiryTester.java | 83 ++++++ .../graphql/keycloak/SecuredResource.java | 42 +++ .../src/main/resources/application.properties | 4 + .../graphql/keycloak/GraphQLAuthExpiryIT.java | 7 + .../keycloak/GraphQLAuthExpiryTest.java | 32 +++ .../KeycloakRealmResourceManager.java | 133 ++++++++++ 16 files changed, 807 insertions(+), 3 deletions(-) create mode 100644 extensions/smallrye-graphql-client/deployment/src/test/java/io/quarkus/smallrye/graphql/client/deployment/DynamicGraphQLClientWebSocketAuthenticationTest.java create mode 100644 extensions/smallrye-graphql-client/deployment/src/test/resources/application-secured.properties create mode 100644 extensions/smallrye-graphql-client/deployment/src/test/resources/roles.properties create mode 100644 extensions/smallrye-graphql-client/deployment/src/test/resources/users.properties create mode 100644 integration-tests/smallrye-graphql-client-keycloak/pom.xml create mode 100644 integration-tests/smallrye-graphql-client-keycloak/src/main/java/io/quarkus/io/smallrye/graphql/keycloak/GraphQLAuthExpiryTester.java create mode 100644 integration-tests/smallrye-graphql-client-keycloak/src/main/java/io/quarkus/io/smallrye/graphql/keycloak/SecuredResource.java create mode 100644 integration-tests/smallrye-graphql-client-keycloak/src/main/resources/application.properties create mode 100644 integration-tests/smallrye-graphql-client-keycloak/src/test/java/io/quarkus/it/smallrye/graphql/keycloak/GraphQLAuthExpiryIT.java create mode 100644 integration-tests/smallrye-graphql-client-keycloak/src/test/java/io/quarkus/it/smallrye/graphql/keycloak/GraphQLAuthExpiryTest.java create mode 100644 integration-tests/smallrye-graphql-client-keycloak/src/test/java/io/quarkus/it/smallrye/graphql/keycloak/KeycloakRealmResourceManager.java diff --git a/.github/native-tests.json b/.github/native-tests.json index d137a6435f5a1..6542e8537d8b9 100644 --- a/.github/native-tests.json +++ b/.github/native-tests.json @@ -111,7 +111,7 @@ { "category": "Misc3", "timeout": 80, - "test-modules": "kubernetes-client, openshift-client, kubernetes-service-binding-jdbc, smallrye-config, smallrye-graphql, smallrye-graphql-client, smallrye-metrics", + "test-modules": "kubernetes-client, openshift-client, kubernetes-service-binding-jdbc, smallrye-config, smallrye-graphql, smallrye-graphql-client, smallrye-graphql-client-keycloak, smallrye-metrics", "os-name": "ubuntu-latest" }, { diff --git a/extensions/smallrye-graphql-client/deployment/pom.xml b/extensions/smallrye-graphql-client/deployment/pom.xml index e2416a9795720..c955fb72b1a02 100644 --- a/extensions/smallrye-graphql-client/deployment/pom.xml +++ b/extensions/smallrye-graphql-client/deployment/pom.xml @@ -63,6 +63,16 @@ stork-service-discovery-static-list test + + io.quarkus + quarkus-elytron-security-deployment + test + + + io.quarkus + quarkus-elytron-security-properties-file-deployment + test + diff --git a/extensions/smallrye-graphql-client/deployment/src/test/java/io/quarkus/smallrye/graphql/client/deployment/DynamicGraphQLClientWebSocketAuthenticationTest.java b/extensions/smallrye-graphql-client/deployment/src/test/java/io/quarkus/smallrye/graphql/client/deployment/DynamicGraphQLClientWebSocketAuthenticationTest.java new file mode 100644 index 0000000000000..b04b74a6b3a24 --- /dev/null +++ b/extensions/smallrye-graphql-client/deployment/src/test/java/io/quarkus/smallrye/graphql/client/deployment/DynamicGraphQLClientWebSocketAuthenticationTest.java @@ -0,0 +1,207 @@ +package io.quarkus.smallrye.graphql.client.deployment; + +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.atomic.AtomicBoolean; + +import jakarta.annotation.security.RolesAllowed; +import jakarta.json.JsonValue; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Query; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.common.annotation.NonBlocking; +import io.smallrye.graphql.api.Subscription; +import io.smallrye.graphql.client.Response; +import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient; +import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClientBuilder; +import io.smallrye.mutiny.Multi; + +/** + * Due to the complexity of establishing a WebSocket, WebSocket/Subscription testing of the GraphQL server is done here, + * as the client framework comes in very useful for establishing the connection to the server. + *
+ * This test establishes connections to the server, and ensures that the connected user has the necessary permissions to + * execute the operation. + */ +public class DynamicGraphQLClientWebSocketAuthenticationTest { + + static String url = "http://" + System.getProperty("quarkus.http.host", "localhost") + ":" + + System.getProperty("quarkus.http.test-port", "8081") + "/graphql"; + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(SecuredApi.class, Foo.class) + .addAsResource("application-secured.properties", "application.properties") + .addAsResource("users.properties") + .addAsResource("roles.properties") + .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml")); + + @Test + public void testAuthenticatedUserForSubscription() throws Exception { + DynamicGraphQLClientBuilder clientBuilder = DynamicGraphQLClientBuilder.newBuilder() + .url(url) + .header("Authorization", "Basic ZGF2aWQ6cXdlcnR5MTIz"); + try (DynamicGraphQLClient client = clientBuilder.build()) { + Multi subscription = client + .subscription("subscription fooSub { fooSub { message } }"); + + assertNotNull(subscription); + + AtomicBoolean hasData = new AtomicBoolean(false); + AtomicBoolean hasCompleted = new AtomicBoolean(false); + + subscription.subscribe().with(item -> { + assertFalse(hasData.get()); + assertTrue(item.hasData()); + assertEquals(JsonValue.ValueType.OBJECT, item.getData().get("fooSub").getValueType()); + assertEquals("foo", item.getData().getJsonObject("fooSub").getString("message")); + hasData.set(true); + }, Assertions::fail, () -> { + hasCompleted.set(true); + }); + + await().untilTrue(hasCompleted); + assertTrue(hasData.get()); + } + } + + @Test + public void testAuthenticatedUserForQueryWebSocket() throws Exception { + DynamicGraphQLClientBuilder clientBuilder = DynamicGraphQLClientBuilder.newBuilder() + .url(url) + .header("Authorization", "Basic ZGF2aWQ6cXdlcnR5MTIz") + .executeSingleOperationsOverWebsocket(true); + try (DynamicGraphQLClient client = clientBuilder.build()) { + Response response = client.executeSync("{ foo { message} }"); + assertTrue(response.hasData()); + assertEquals("foo", response.getData().getJsonObject("foo").getString("message")); + } + } + + @Test + public void testAuthorizedAndUnauthorizedForQueryWebSocket() throws Exception { + DynamicGraphQLClientBuilder clientBuilder = DynamicGraphQLClientBuilder.newBuilder() + .url(url) + .header("Authorization", "Basic ZGF2aWQ6cXdlcnR5MTIz") + .executeSingleOperationsOverWebsocket(true); + try (DynamicGraphQLClient client = clientBuilder.build()) { + Response response = client.executeSync("{ foo { message} }"); + assertTrue(response.hasData()); + assertEquals("foo", response.getData().getJsonObject("foo").getString("message")); + + // Run a second query with a different result to validate that the result of the first query isn't being cached at all. + response = client.executeSync("{ bar { message} }"); + assertEquals(JsonValue.ValueType.NULL, response.getData().get("bar").getValueType()); + } + } + + @Test + public void testUnauthorizedUserForSubscription() throws Exception { + DynamicGraphQLClientBuilder clientBuilder = DynamicGraphQLClientBuilder.newBuilder() + .url(url) + .header("Authorization", "Basic ZGF2aWQ6cXdlcnR5MTIz"); + try (DynamicGraphQLClient client = clientBuilder.build()) { + Multi subscription = client + .subscription("subscription barSub { barSub { message } }"); + + assertNotNull(subscription); + + AtomicBoolean returned = new AtomicBoolean(false); + + subscription.subscribe().with(item -> { + assertEquals(JsonValue.ValueType.NULL, item.getData().get("barSub").getValueType()); + returned.set(true); + }, throwable -> Assertions.fail(throwable)); + + await().untilTrue(returned); + } + } + + @Test + public void testUnauthorizedUserForQueryWebSocket() throws Exception { + DynamicGraphQLClientBuilder clientBuilder = DynamicGraphQLClientBuilder.newBuilder() + .url(url) + .header("Authorization", "Basic ZGF2aWQ6cXdlcnR5MTIz") + .executeSingleOperationsOverWebsocket(true); + try (DynamicGraphQLClient client = clientBuilder.build()) { + Response response = client.executeSync("{ bar { message } }"); + assertEquals(JsonValue.ValueType.NULL, response.getData().get("bar").getValueType()); + } + } + + @Test + public void testUnauthenticatedForQueryWebSocket() throws Exception { + DynamicGraphQLClientBuilder clientBuilder = DynamicGraphQLClientBuilder.newBuilder() + .url(url) + .executeSingleOperationsOverWebsocket(true); + try (DynamicGraphQLClient client = clientBuilder.build()) { + Response response = client.executeSync("{ foo { message} }"); + assertEquals(JsonValue.ValueType.NULL, response.getData().get("foo").getValueType()); + } + } + + public static class Foo { + + private String message; + + public Foo(String foo) { + this.message = foo; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + } + + @GraphQLApi + public static class SecuredApi { + + @Query + @RolesAllowed("fooRole") + @NonBlocking + public Foo foo() { + return new Foo("foo"); + } + + @Query + @RolesAllowed("barRole") + public Foo bar() { + return new Foo("bar"); + } + + @Subscription + @RolesAllowed("fooRole") + public Multi fooSub() { + return Multi.createFrom().emitter(emitter -> { + emitter.emit(new Foo("foo")); + emitter.complete(); + }); + } + + @Subscription + @RolesAllowed("barRole") + public Multi barSub() { + return Multi.createFrom().emitter(emitter -> { + emitter.emit(new Foo("bar")); + emitter.complete(); + }); + } + + } +} diff --git a/extensions/smallrye-graphql-client/deployment/src/test/resources/application-secured.properties b/extensions/smallrye-graphql-client/deployment/src/test/resources/application-secured.properties new file mode 100644 index 0000000000000..eb7d901e0c93f --- /dev/null +++ b/extensions/smallrye-graphql-client/deployment/src/test/resources/application-secured.properties @@ -0,0 +1,9 @@ +quarkus.security.users.file.enabled=true +quarkus.security.users.file.plain-text=true +quarkus.security.users.file.users=users.properties +quarkus.security.users.file.roles=roles.properties +quarkus.http.auth.basic=true + +quarkus.smallrye-graphql.log-payload=queryAndVariables +quarkus.smallrye-graphql.print-data-fetcher-exception=true +quarkus.smallrye-graphql.error-extension-fields=exception,classification,code,description,validationErrorType,queryPath diff --git a/extensions/smallrye-graphql-client/deployment/src/test/resources/roles.properties b/extensions/smallrye-graphql-client/deployment/src/test/resources/roles.properties new file mode 100644 index 0000000000000..ef2a67ac7e9e6 --- /dev/null +++ b/extensions/smallrye-graphql-client/deployment/src/test/resources/roles.properties @@ -0,0 +1 @@ +david=fooRole \ No newline at end of file diff --git a/extensions/smallrye-graphql-client/deployment/src/test/resources/users.properties b/extensions/smallrye-graphql-client/deployment/src/test/resources/users.properties new file mode 100644 index 0000000000000..0f1cc7592d055 --- /dev/null +++ b/extensions/smallrye-graphql-client/deployment/src/test/resources/users.properties @@ -0,0 +1 @@ +david=qwerty123 \ No newline at end of file diff --git a/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java index f10de933cc1da..4ead0d35da8f1 100644 --- a/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java +++ b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java @@ -65,6 +65,7 @@ import io.quarkus.smallrye.graphql.runtime.SmallRyeGraphQLRecorder; import io.quarkus.smallrye.graphql.runtime.SmallRyeGraphQLRuntimeConfig; import io.quarkus.vertx.http.deployment.BodyHandlerBuildItem; +import io.quarkus.vertx.http.deployment.FilterBuildItem; import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem; import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; import io.quarkus.vertx.http.deployment.RouteBuildItem; @@ -149,7 +150,7 @@ public class SmallRyeGraphQLProcessor { private static final List SUPPORTED_WEBSOCKET_SUBPROTOCOLS = List.of(SUBPROTOCOL_GRAPHQL_WS, SUBPROTOCOL_GRAPHQL_TRANSPORT_WS); - private static final int GRAPHQL_WEBSOCKET_HANDLER_ORDER = -10000; + private static final int GRAPHQL_WEBSOCKET_HANDLER_ORDER = (-1 * FilterBuildItem.AUTHORIZATION) + 1; private static final String GRAPHQL_MEDIA_TYPE = "application/graphql+json"; diff --git a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLOverWebSocketHandler.java b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLOverWebSocketHandler.java index 2cae3cd455bb6..d593b32ef6640 100644 --- a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLOverWebSocketHandler.java +++ b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLOverWebSocketHandler.java @@ -1,17 +1,21 @@ package io.quarkus.smallrye.graphql.runtime; import java.util.Map; +import java.util.concurrent.TimeUnit; import org.jboss.logging.Logger; +import io.netty.util.concurrent.ScheduledFuture; import io.quarkus.security.identity.CurrentIdentityAssociation; import io.quarkus.vertx.http.runtime.CurrentVertxRequest; +import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; import io.smallrye.graphql.websocket.GraphQLWebSocketSession; import io.smallrye.graphql.websocket.GraphQLWebsocketHandler; import io.smallrye.graphql.websocket.graphqltransportws.GraphQLTransportWSSubprotocolHandler; import io.smallrye.graphql.websocket.graphqlws.GraphQLWSSubprotocolHandler; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.ServerWebSocket; +import io.vertx.core.net.impl.ConnectionBase; import io.vertx.ext.web.RoutingContext; /** @@ -54,9 +58,31 @@ protected void doHandle(final RoutingContext ctx) { serverWebSocket.close(); return; } + + QuarkusHttpUser user = (QuarkusHttpUser) ctx.user(); + ScheduledFuture authExpiryFuture = null; + if (user != null) { + //close the connection when the identity expires + Long expire = user.getSecurityIdentity().getAttribute("quarkus.identity.expire-time"); + if (expire != null) { + authExpiryFuture = ((ConnectionBase) ctx.request().connection()).channel().eventLoop() + .schedule(() -> { + if (!serverWebSocket.isClosed()) { + serverWebSocket.close((short) 1008, "Authentication expired"); + } + }, (expire * 1000) - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + } + log.debugf("Starting websocket with subprotocol = %s", subprotocol); GraphQLWebsocketHandler finalHandler = handler; - serverWebSocket.closeHandler(v -> finalHandler.onClose()); + ScheduledFuture finalAuthExpiryFuture = authExpiryFuture; + serverWebSocket.closeHandler(v -> { + finalHandler.onClose(); + if (finalAuthExpiryFuture != null) { + finalAuthExpiryFuture.cancel(false); + } + }); serverWebSocket.endHandler(v -> finalHandler.onEnd()); serverWebSocket.exceptionHandler(finalHandler::onThrowable); serverWebSocket.textMessageHandler(finalHandler::onMessage); diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 3fdb4245dcf4b..8e027683779b5 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -318,6 +318,7 @@ smallrye-metrics smallrye-graphql smallrye-graphql-client + smallrye-graphql-client-keycloak smallrye-stork-registration jpa-without-entity quartz diff --git a/integration-tests/smallrye-graphql-client-keycloak/pom.xml b/integration-tests/smallrye-graphql-client-keycloak/pom.xml new file mode 100644 index 0000000000000..e87c0810c3da8 --- /dev/null +++ b/integration-tests/smallrye-graphql-client-keycloak/pom.xml @@ -0,0 +1,247 @@ + + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-integration-test-smallrye-graphql-client-keycloak + Quarkus - Integration Tests - SmallRye GraphQL Client with Keycloak + + + http://localhost:8180/auth + + + + + io.quarkus + quarkus-smallrye-graphql + + + io.quarkus + quarkus-smallrye-graphql-client + + + io.quarkus + quarkus-resteasy-deployment + + + io.rest-assured + rest-assured + + + + io.quarkus + quarkus-rest-client-deployment + + + io.quarkus + quarkus-junit5 + test + + + + + io.quarkus + quarkus-smallrye-graphql-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-smallrye-graphql-client-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-test-keycloak-server + test + + + junit + junit + + + + + io.quarkus + quarkus-oidc-deployment + test + + + + + + + maven-surefire-plugin + + true + + + + maven-failsafe-plugin + + true + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + + test-keycloak + + + test-containers + + + + + + maven-surefire-plugin + + false + + ${keycloak.url} + + + + + maven-failsafe-plugin + + false + + ${keycloak.url} + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + + docker-keycloak + + + start-containers + + + + http://localhost:8180/auth + + + + + io.fabric8 + docker-maven-plugin + + + + ${keycloak.docker.legacy.image} + quarkus-test-keycloak + + + 8180:8080 + + + admin + admin + + + Keycloak: + default + cyan + + + + + http://localhost:8180 + + + + + + + true + + + + docker-start + compile + + stop + start + + + + docker-stop + post-integration-test + + stop + + + + + + + + + + + diff --git a/integration-tests/smallrye-graphql-client-keycloak/src/main/java/io/quarkus/io/smallrye/graphql/keycloak/GraphQLAuthExpiryTester.java b/integration-tests/smallrye-graphql-client-keycloak/src/main/java/io/quarkus/io/smallrye/graphql/keycloak/GraphQLAuthExpiryTester.java new file mode 100644 index 0000000000000..3aa53e1acb18a --- /dev/null +++ b/integration-tests/smallrye-graphql-client-keycloak/src/main/java/io/quarkus/io/smallrye/graphql/keycloak/GraphQLAuthExpiryTester.java @@ -0,0 +1,83 @@ +package io.quarkus.io.smallrye.graphql.keycloak; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; + +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import io.restassured.RestAssured; +import io.smallrye.common.annotation.Blocking; +import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient; +import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClientBuilder; + +/** + * We can't perform these tests in the `@Test` methods directly, because the GraphQL client + * relies on CDI, and CDI is not available in native mode on the `@Test` side. + * Therefore the test only calls this REST endpoint which then performs all the client related work. + *
+ * This test establishes connections to the server, and ensures that if authentication has an expiry, that following the + * expiry of their access the connection is correctly terminated. + */ +@Path("/") +public class GraphQLAuthExpiryTester { + + @ConfigProperty(name = "quarkus.oidc.auth-server-url") + String keycloakRealm; + + @GET + @Path("/dynamic-subscription-auth-expiry/{url}") + @Blocking + public void dynamicSubscription(@PathParam("url") String url) + throws Exception { + String authHeader = getAuthHeader(); + + DynamicGraphQLClientBuilder clientBuilder = DynamicGraphQLClientBuilder.newBuilder() + .url(url + "/graphql") + .header("Authorization", authHeader) + .executeSingleOperationsOverWebsocket(true); + + try (DynamicGraphQLClient client = clientBuilder.build()) { + CompletableFuture authenticationExpired = new CompletableFuture<>(); + AtomicBoolean receivedValue = new AtomicBoolean(false); + client.subscription("subscription { sub { value } }").subscribe().with(item -> { + if (item.hasData()) { + receivedValue.set(true); + } else { + authenticationExpired.completeExceptionally(new RuntimeException("Subscription provided no data")); + } + }, cause -> { + if (cause.getMessage().contains("Authentication expired")) { + authenticationExpired.complete(null); + } else { + authenticationExpired + .completeExceptionally(new RuntimeException("Invalid close response from server.", cause)); + } + }, () -> authenticationExpired + .completeExceptionally(new RuntimeException("Subscription should not complete successfully"))); + + authenticationExpired.get(10, TimeUnit.SECONDS); + if (!receivedValue.get()) { + throw new RuntimeException("Did not receive subscription value"); + } + } + } + + private String getAuthHeader() { + io.restassured.response.Response response = RestAssured.given() + .contentType("application/x-www-form-urlencoded") + .accept("application/json") + .formParam("username", "alice") + .formParam("password", "alice") + .param("client_id", "quarkus-app") + .param("client_secret", "secret") + .formParam("grant_type", "password") + .post(keycloakRealm + "/protocol/openid-connect/token"); + + return "Bearer " + response.getBody().jsonPath().getString("access_token"); + } +} diff --git a/integration-tests/smallrye-graphql-client-keycloak/src/main/java/io/quarkus/io/smallrye/graphql/keycloak/SecuredResource.java b/integration-tests/smallrye-graphql-client-keycloak/src/main/java/io/quarkus/io/smallrye/graphql/keycloak/SecuredResource.java new file mode 100644 index 0000000000000..1957991834db2 --- /dev/null +++ b/integration-tests/smallrye-graphql-client-keycloak/src/main/java/io/quarkus/io/smallrye/graphql/keycloak/SecuredResource.java @@ -0,0 +1,42 @@ +package io.quarkus.io.smallrye.graphql.keycloak; + +import jakarta.annotation.security.RolesAllowed; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Query; + +import io.smallrye.common.annotation.NonBlocking; +import io.smallrye.graphql.api.Subscription; +import io.smallrye.mutiny.Multi; + +@GraphQLApi +public class SecuredResource { + + // Seems to be a requirement to have a query or mutation in a GraphQLApi. + // This is a workaround for the time being. + @Query + public TestResponse unusedQuery() { + return null; + } + + @Subscription + @RolesAllowed("user") + @NonBlocking + public Multi sub() { + return Multi.createFrom().emitter(emitter -> emitter.emit(new TestResponse("Hello World"))); + } + + public static class TestResponse { + + private final String value; + + public TestResponse(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + +} diff --git a/integration-tests/smallrye-graphql-client-keycloak/src/main/resources/application.properties b/integration-tests/smallrye-graphql-client-keycloak/src/main/resources/application.properties new file mode 100644 index 0000000000000..20c981d528c15 --- /dev/null +++ b/integration-tests/smallrye-graphql-client-keycloak/src/main/resources/application.properties @@ -0,0 +1,4 @@ +quarkus.oidc.client-id=quarkus-app +quarkus.oidc.credentials.secret=secret +quarkus.smallrye-graphql.log-payload=queryAndVariables +quarkus.keycloak.devservices.enabled=false diff --git a/integration-tests/smallrye-graphql-client-keycloak/src/test/java/io/quarkus/it/smallrye/graphql/keycloak/GraphQLAuthExpiryIT.java b/integration-tests/smallrye-graphql-client-keycloak/src/test/java/io/quarkus/it/smallrye/graphql/keycloak/GraphQLAuthExpiryIT.java new file mode 100644 index 0000000000000..758e4780144ae --- /dev/null +++ b/integration-tests/smallrye-graphql-client-keycloak/src/test/java/io/quarkus/it/smallrye/graphql/keycloak/GraphQLAuthExpiryIT.java @@ -0,0 +1,7 @@ +package io.quarkus.it.smallrye.graphql.keycloak; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class GraphQLAuthExpiryIT extends GraphQLAuthExpiryTest { +} diff --git a/integration-tests/smallrye-graphql-client-keycloak/src/test/java/io/quarkus/it/smallrye/graphql/keycloak/GraphQLAuthExpiryTest.java b/integration-tests/smallrye-graphql-client-keycloak/src/test/java/io/quarkus/it/smallrye/graphql/keycloak/GraphQLAuthExpiryTest.java new file mode 100644 index 0000000000000..554753d09fc98 --- /dev/null +++ b/integration-tests/smallrye-graphql-client-keycloak/src/test/java/io/quarkus/it/smallrye/graphql/keycloak/GraphQLAuthExpiryTest.java @@ -0,0 +1,32 @@ +package io.quarkus.it.smallrye.graphql.keycloak; + +import static io.restassured.RestAssured.when; + +import java.net.URL; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.test.junit.QuarkusTest; + +/** + * See `GraphQLClientTester` for the actual testing code that uses GraphQL clients. + */ +@QuarkusTest +@QuarkusTestResource(KeycloakRealmResourceManager.class) +public class GraphQLAuthExpiryTest { + + @TestHTTPResource + URL url; + + @Test + public void testDynamicClientWebSocketAuthenticationExpiry() { + when() + .get("/dynamic-subscription-auth-expiry/" + url.toString()) + .then() + .log().everything() + .statusCode(204); + } + +} diff --git a/integration-tests/smallrye-graphql-client-keycloak/src/test/java/io/quarkus/it/smallrye/graphql/keycloak/KeycloakRealmResourceManager.java b/integration-tests/smallrye-graphql-client-keycloak/src/test/java/io/quarkus/it/smallrye/graphql/keycloak/KeycloakRealmResourceManager.java new file mode 100644 index 0000000000000..80dd04f08a79e --- /dev/null +++ b/integration-tests/smallrye-graphql-client-keycloak/src/test/java/io/quarkus/it/smallrye/graphql/keycloak/KeycloakRealmResourceManager.java @@ -0,0 +1,133 @@ +package io.quarkus.it.smallrye.graphql.keycloak; + +import java.io.IOException; +import java.util.*; + +import org.keycloak.representations.AccessTokenResponse; +import org.keycloak.representations.idm.*; +import org.keycloak.util.JsonSerialization; +import org.testcontainers.containers.GenericContainer; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; +import io.restassured.RestAssured; +import io.restassured.response.Response; + +public class KeycloakRealmResourceManager implements QuarkusTestResourceLifecycleManager { + + private static final String KEYCLOAK_SERVER_URL = System.getProperty("keycloak.url", "http://localhost:8180/auth"); + //private static String KEYCLOAK_SERVER_URL; + private static final String KEYCLOAK_REALM = "quarkus"; + //private static final String KEYCLOAK_IMAGE = "quay.io/keycloak/keycloak:22.0.5"; + + private GenericContainer keycloak; + + @Override + public Map start() { + RealmRepresentation realm = createRealm(KEYCLOAK_REALM); + realm.setRevokeRefreshToken(true); + realm.setRefreshTokenMaxReuse(0); + realm.setAccessTokenLifespan(3); + + realm.getClients().add(createClient("quarkus-app")); + realm.getUsers().add(createUser("alice", "user")); + + try { + Response response = RestAssured + .given() + .auth().oauth2(getAdminAccessToken()) + .contentType("application/json") + .body(JsonSerialization.writeValueAsBytes(realm)) + .when() + .post(KEYCLOAK_SERVER_URL + "/admin/realms"); + response.then() + .statusCode(201); + + } catch (IOException e) { + throw new RuntimeException(e); + } + + Map properties = new HashMap<>(); + + properties.put("quarkus.oidc.auth-server-url", KEYCLOAK_SERVER_URL + "/realms/" + KEYCLOAK_REALM); + properties.put("keycloak.url", KEYCLOAK_SERVER_URL); + + return properties; + } + + private static String getAdminAccessToken() { + return RestAssured + .given() + .param("grant_type", "password") + .param("username", "admin") + .param("password", "admin") + .param("client_id", "admin-cli") + .when() + .post(KEYCLOAK_SERVER_URL + "/realms/master/protocol/openid-connect/token") + .as(AccessTokenResponse.class).getToken(); + } + + private static RealmRepresentation createRealm(String name) { + RealmRepresentation realm = new RealmRepresentation(); + + realm.setRealm(name); + realm.setEnabled(true); + realm.setUsers(new ArrayList<>()); + realm.setClients(new ArrayList<>()); + realm.setAccessTokenLifespan(3); + realm.setSsoSessionMaxLifespan(3); + RolesRepresentation roles = new RolesRepresentation(); + List realmRoles = new ArrayList<>(); + + roles.setRealm(realmRoles); + realm.setRoles(roles); + + realm.getRoles().getRealm().add(new RoleRepresentation("user", null, false)); + realm.getRoles().getRealm().add(new RoleRepresentation("admin", null, false)); + + return realm; + } + + private static ClientRepresentation createClient(String clientId) { + ClientRepresentation client = new ClientRepresentation(); + + client.setClientId(clientId); + client.setPublicClient(false); + client.setSecret("secret"); + client.setDirectAccessGrantsEnabled(true); + client.setServiceAccountsEnabled(true); + client.setRedirectUris(Arrays.asList("*")); + client.setEnabled(true); + client.setDefaultClientScopes(List.of("microprofile-jwt")); + + return client; + } + + private static UserRepresentation createUser(String username, String... realmRoles) { + UserRepresentation user = new UserRepresentation(); + + user.setUsername(username); + user.setEnabled(true); + user.setCredentials(new ArrayList<>()); + user.setRealmRoles(Arrays.asList(realmRoles)); + user.setEmail(username + "@gmail.com"); + + CredentialRepresentation credential = new CredentialRepresentation(); + + credential.setType(CredentialRepresentation.PASSWORD); + credential.setValue(username); + credential.setTemporary(false); + + user.getCredentials().add(credential); + + return user; + } + + @Override + public void stop() { + RestAssured + .given() + .auth().oauth2(getAdminAccessToken()) + .when() + .delete(KEYCLOAK_SERVER_URL + "/admin/realms/" + KEYCLOAK_REALM).then().statusCode(204); + } +} From a5246bf5ab1ce1a99c7340bfe64d3ec49df196d7 Mon Sep 17 00:00:00 2001 From: Clement Escoffier Date: Wed, 15 Nov 2023 16:00:44 +0100 Subject: [PATCH 2/4] Use Vert.x instace of Netty to schedule the closing task --- .../SmallRyeGraphQLOverWebSocketHandler.java | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLOverWebSocketHandler.java b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLOverWebSocketHandler.java index d593b32ef6640..c309a4f09eaf7 100644 --- a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLOverWebSocketHandler.java +++ b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLOverWebSocketHandler.java @@ -1,11 +1,9 @@ package io.quarkus.smallrye.graphql.runtime; import java.util.Map; -import java.util.concurrent.TimeUnit; import org.jboss.logging.Logger; -import io.netty.util.concurrent.ScheduledFuture; import io.quarkus.security.identity.CurrentIdentityAssociation; import io.quarkus.vertx.http.runtime.CurrentVertxRequest; import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; @@ -13,9 +11,9 @@ import io.smallrye.graphql.websocket.GraphQLWebsocketHandler; import io.smallrye.graphql.websocket.graphqltransportws.GraphQLTransportWSSubprotocolHandler; import io.smallrye.graphql.websocket.graphqlws.GraphQLWSSubprotocolHandler; +import io.vertx.core.Handler; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.ServerWebSocket; -import io.vertx.core.net.impl.ConnectionBase; import io.vertx.ext.web.RoutingContext; /** @@ -60,27 +58,30 @@ protected void doHandle(final RoutingContext ctx) { } QuarkusHttpUser user = (QuarkusHttpUser) ctx.user(); - ScheduledFuture authExpiryFuture = null; + long cancellation = -1L; // Do not use 0, as you won't be able to distinguish between not set, and the first task Id if (user != null) { //close the connection when the identity expires Long expire = user.getSecurityIdentity().getAttribute("quarkus.identity.expire-time"); if (expire != null) { - authExpiryFuture = ((ConnectionBase) ctx.request().connection()).channel().eventLoop() - .schedule(() -> { - if (!serverWebSocket.isClosed()) { - serverWebSocket.close((short) 1008, "Authentication expired"); + cancellation = ctx.vertx().setTimer((expire * 1000) - System.currentTimeMillis(), + new Handler() { + @Override + public void handle(Long event) { + if (!serverWebSocket.isClosed()) { + serverWebSocket.close((short) 1008, "Authentication expired"); + } } - }, (expire * 1000) - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + }); } } log.debugf("Starting websocket with subprotocol = %s", subprotocol); GraphQLWebsocketHandler finalHandler = handler; - ScheduledFuture finalAuthExpiryFuture = authExpiryFuture; + long finalCancellation = cancellation; serverWebSocket.closeHandler(v -> { finalHandler.onClose(); - if (finalAuthExpiryFuture != null) { - finalAuthExpiryFuture.cancel(false); + if (finalCancellation != -1) { + ctx.vertx().cancelTimer(finalCancellation); } }); serverWebSocket.endHandler(v -> finalHandler.onEnd()); From 631a30794a601d79dcc33272b0867814c08e8494 Mon Sep 17 00:00:00 2001 From: Jan Martiska Date: Thu, 16 Nov 2023 08:33:56 +0100 Subject: [PATCH 3/4] Add test for graphql over websockets with only HTTP-based permissions --- ...cketAuthenticationHttpPermissionsTest.java | 137 ++++++++++++++++++ ...cation-secured-http-permissions.properties | 13 ++ 2 files changed, 150 insertions(+) create mode 100644 extensions/smallrye-graphql-client/deployment/src/test/java/io/quarkus/smallrye/graphql/client/deployment/DynamicGraphQLClientWebSocketAuthenticationHttpPermissionsTest.java create mode 100644 extensions/smallrye-graphql-client/deployment/src/test/resources/application-secured-http-permissions.properties diff --git a/extensions/smallrye-graphql-client/deployment/src/test/java/io/quarkus/smallrye/graphql/client/deployment/DynamicGraphQLClientWebSocketAuthenticationHttpPermissionsTest.java b/extensions/smallrye-graphql-client/deployment/src/test/java/io/quarkus/smallrye/graphql/client/deployment/DynamicGraphQLClientWebSocketAuthenticationHttpPermissionsTest.java new file mode 100644 index 0000000000000..f2a72d45f8231 --- /dev/null +++ b/extensions/smallrye-graphql-client/deployment/src/test/java/io/quarkus/smallrye/graphql/client/deployment/DynamicGraphQLClientWebSocketAuthenticationHttpPermissionsTest.java @@ -0,0 +1,137 @@ +package io.quarkus.smallrye.graphql.client.deployment; + +import jakarta.annotation.security.RolesAllowed; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Query; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.common.annotation.NonBlocking; +import io.smallrye.graphql.api.Subscription; +import io.smallrye.graphql.client.Response; +import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient; +import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClientBuilder; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.helpers.test.AssertSubscriber; +import io.vertx.core.http.UpgradeRejectedException; + +/** + * Due to the complexity of establishing a WebSocket, WebSocket/Subscription testing of the GraphQL server is done here, + * as the client framework comes in very useful for establishing the connection to the server. + *
+ * This test establishes connections to the server, and ensures that the connected user has the necessary permissions to + * execute the operation. + */ +public class DynamicGraphQLClientWebSocketAuthenticationHttpPermissionsTest { + + static String url = "http://" + System.getProperty("quarkus.http.host", "localhost") + ":" + + System.getProperty("quarkus.http.test-port", "8081") + "/graphql"; + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(SecuredApi.class, Foo.class) + .addAsResource("application-secured-http-permissions.properties", "application.properties") + .addAsResource("users.properties") + .addAsResource("roles.properties") + .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml")); + + @Disabled("TODO: enable after upgrade to smallrye-graphql 1.6.1, with 1.6.0 a websocket upgrade failure causes a hang here") + @Test + public void testUnauthenticatedForQueryWebSocket() throws Exception { + DynamicGraphQLClientBuilder clientBuilder = DynamicGraphQLClientBuilder.newBuilder() + .url(url) + .executeSingleOperationsOverWebsocket(true); + try (DynamicGraphQLClient client = clientBuilder.build()) { + try { + client.executeSync("{ baz { message} }"); + Assertions.fail("WebSocket upgrade should fail"); + } catch (UpgradeRejectedException e) { + // ok + } + } + } + + @Test + public void testUnauthenticatedForSubscriptionWebSocket() throws Exception { + DynamicGraphQLClientBuilder clientBuilder = DynamicGraphQLClientBuilder.newBuilder() + .url(url); + try (DynamicGraphQLClient client = clientBuilder.build()) { + AssertSubscriber subscriber = new AssertSubscriber<>(); + client.subscription("{ bazSub { message} }").subscribe().withSubscriber(subscriber); + subscriber.awaitFailure().assertFailedWith(UpgradeRejectedException.class); + } + } + + public static class Foo { + + private String message; + + public Foo(String foo) { + this.message = foo; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + } + + @GraphQLApi + public static class SecuredApi { + + @Query + @RolesAllowed("fooRole") + @NonBlocking + public Foo foo() { + return new Foo("foo"); + } + + @Query + @RolesAllowed("barRole") + public Foo bar() { + return new Foo("bar"); + } + + @Query + public Foo baz() { + return new Foo("baz"); + } + + @Subscription + @RolesAllowed("fooRole") + public Multi fooSub() { + return Multi.createFrom().emitter(emitter -> { + emitter.emit(new Foo("foo")); + emitter.complete(); + }); + } + + @Subscription + @RolesAllowed("barRole") + public Multi barSub() { + return Multi.createFrom().emitter(emitter -> { + emitter.emit(new Foo("bar")); + emitter.complete(); + }); + } + + @Subscription + public Multi bazSub() { + return Multi.createFrom().emitter(emitter -> { + emitter.emit(new Foo("baz")); + emitter.complete(); + }); + } + + } +} diff --git a/extensions/smallrye-graphql-client/deployment/src/test/resources/application-secured-http-permissions.properties b/extensions/smallrye-graphql-client/deployment/src/test/resources/application-secured-http-permissions.properties new file mode 100644 index 0000000000000..770567e9e3565 --- /dev/null +++ b/extensions/smallrye-graphql-client/deployment/src/test/resources/application-secured-http-permissions.properties @@ -0,0 +1,13 @@ +quarkus.security.users.file.enabled=true +quarkus.security.users.file.plain-text=true +quarkus.security.users.file.users=users.properties +quarkus.security.users.file.roles=roles.properties +quarkus.http.auth.basic=true + +quarkus.smallrye-graphql.log-payload=queryAndVariables +quarkus.smallrye-graphql.print-data-fetcher-exception=true +quarkus.smallrye-graphql.error-extension-fields=exception,classification,code,description,validationErrorType,queryPath + +quarkus.http.auth.permission.authenticated.paths=/graphql +quarkus.http.auth.permission.authenticated.methods=GET,POST +quarkus.http.auth.permission.authenticated.policy=authenticated \ No newline at end of file From c1fde71411588022c78c53a43b4884bb3229a1be Mon Sep 17 00:00:00 2001 From: Jan Martiska Date: Thu, 16 Nov 2023 11:03:06 +0100 Subject: [PATCH 4/4] Fix native mode GraphQLAuthExpiryIT --- .../smallrye-graphql-client-keycloak/pom.xml | 13 +++++++++ .../keycloak/GraphQLAuthExpiryTester.java | 27 +++---------------- .../keycloak/GraphQLAuthExpiryTest.java | 3 ++- .../KeycloakRealmResourceManager.java | 13 +++++++++ 4 files changed, 31 insertions(+), 25 deletions(-) diff --git a/integration-tests/smallrye-graphql-client-keycloak/pom.xml b/integration-tests/smallrye-graphql-client-keycloak/pom.xml index e87c0810c3da8..b1481bb6df0bf 100644 --- a/integration-tests/smallrye-graphql-client-keycloak/pom.xml +++ b/integration-tests/smallrye-graphql-client-keycloak/pom.xml @@ -29,9 +29,14 @@ io.quarkus quarkus-resteasy-deployment + + io.quarkus + quarkus-oidc + io.rest-assured rest-assured + test @@ -85,7 +90,15 @@ io.quarkus quarkus-oidc-deployment + ${project.version} + pom test + + + * + * + + diff --git a/integration-tests/smallrye-graphql-client-keycloak/src/main/java/io/quarkus/io/smallrye/graphql/keycloak/GraphQLAuthExpiryTester.java b/integration-tests/smallrye-graphql-client-keycloak/src/main/java/io/quarkus/io/smallrye/graphql/keycloak/GraphQLAuthExpiryTester.java index 3aa53e1acb18a..9a8c49e6b28f4 100644 --- a/integration-tests/smallrye-graphql-client-keycloak/src/main/java/io/quarkus/io/smallrye/graphql/keycloak/GraphQLAuthExpiryTester.java +++ b/integration-tests/smallrye-graphql-client-keycloak/src/main/java/io/quarkus/io/smallrye/graphql/keycloak/GraphQLAuthExpiryTester.java @@ -8,9 +8,6 @@ import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; -import org.eclipse.microprofile.config.inject.ConfigProperty; - -import io.restassured.RestAssured; import io.smallrye.common.annotation.Blocking; import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient; import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClientBuilder; @@ -26,19 +23,14 @@ @Path("/") public class GraphQLAuthExpiryTester { - @ConfigProperty(name = "quarkus.oidc.auth-server-url") - String keycloakRealm; - @GET - @Path("/dynamic-subscription-auth-expiry/{url}") + @Path("/dynamic-subscription-auth-expiry/{token}/{url}") @Blocking - public void dynamicSubscription(@PathParam("url") String url) + public void dynamicSubscription(@PathParam("token") String token, @PathParam("url") String url) throws Exception { - String authHeader = getAuthHeader(); - DynamicGraphQLClientBuilder clientBuilder = DynamicGraphQLClientBuilder.newBuilder() .url(url + "/graphql") - .header("Authorization", authHeader) + .header("Authorization", "Bearer " + token) .executeSingleOperationsOverWebsocket(true); try (DynamicGraphQLClient client = clientBuilder.build()) { @@ -67,17 +59,4 @@ public void dynamicSubscription(@PathParam("url") String url) } } - private String getAuthHeader() { - io.restassured.response.Response response = RestAssured.given() - .contentType("application/x-www-form-urlencoded") - .accept("application/json") - .formParam("username", "alice") - .formParam("password", "alice") - .param("client_id", "quarkus-app") - .param("client_secret", "secret") - .formParam("grant_type", "password") - .post(keycloakRealm + "/protocol/openid-connect/token"); - - return "Bearer " + response.getBody().jsonPath().getString("access_token"); - } } diff --git a/integration-tests/smallrye-graphql-client-keycloak/src/test/java/io/quarkus/it/smallrye/graphql/keycloak/GraphQLAuthExpiryTest.java b/integration-tests/smallrye-graphql-client-keycloak/src/test/java/io/quarkus/it/smallrye/graphql/keycloak/GraphQLAuthExpiryTest.java index 554753d09fc98..01338e9915215 100644 --- a/integration-tests/smallrye-graphql-client-keycloak/src/test/java/io/quarkus/it/smallrye/graphql/keycloak/GraphQLAuthExpiryTest.java +++ b/integration-tests/smallrye-graphql-client-keycloak/src/test/java/io/quarkus/it/smallrye/graphql/keycloak/GraphQLAuthExpiryTest.java @@ -22,8 +22,9 @@ public class GraphQLAuthExpiryTest { @Test public void testDynamicClientWebSocketAuthenticationExpiry() { + String token = KeycloakRealmResourceManager.getAccessToken(); when() - .get("/dynamic-subscription-auth-expiry/" + url.toString()) + .get("/dynamic-subscription-auth-expiry/" + token + "/" + url.toString()) .then() .log().everything() .statusCode(204); diff --git a/integration-tests/smallrye-graphql-client-keycloak/src/test/java/io/quarkus/it/smallrye/graphql/keycloak/KeycloakRealmResourceManager.java b/integration-tests/smallrye-graphql-client-keycloak/src/test/java/io/quarkus/it/smallrye/graphql/keycloak/KeycloakRealmResourceManager.java index 80dd04f08a79e..a527c265f76af 100644 --- a/integration-tests/smallrye-graphql-client-keycloak/src/test/java/io/quarkus/it/smallrye/graphql/keycloak/KeycloakRealmResourceManager.java +++ b/integration-tests/smallrye-graphql-client-keycloak/src/test/java/io/quarkus/it/smallrye/graphql/keycloak/KeycloakRealmResourceManager.java @@ -130,4 +130,17 @@ public void stop() { .when() .delete(KEYCLOAK_SERVER_URL + "/admin/realms/" + KEYCLOAK_REALM).then().statusCode(204); } + + public static String getAccessToken() { + io.restassured.response.Response response = RestAssured.given() + .contentType("application/x-www-form-urlencoded") + .accept("application/json") + .formParam("username", "alice") + .formParam("password", "alice") + .param("client_id", "quarkus-app") + .param("client_secret", "secret") + .formParam("grant_type", "password") + .post(KEYCLOAK_SERVER_URL + "/realms/" + KEYCLOAK_REALM + "/protocol/openid-connect/token"); + return response.getBody().jsonPath().getString("access_token"); + } }