From 60d660543bb34a529710c6ea25e95c5b1233bbf0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 11 Aug 2023 15:18:02 -0300 Subject: [PATCH] [quarkus2] Added tests for token propagation (#436) * Added tests for token propagation (#433) * Added tests for token propagation Signed-off-by: Helber Belmiro --------- Signed-off-by: Helber Belmiro * Replaced jakarta with javax --------- Signed-off-by: Helber Belmiro Co-authored-by: Helber Belmiro --- integration-tests/security/pom.xml | 5 ++ .../it/security/TokenPropagationResource.java | 60 ++++++++++++++ .../token-propagation-external-service1.yaml | 19 +++++ .../token-propagation-external-service2.yaml | 23 ++++++ .../token-propagation-external-service3.yaml | 19 +++++ .../token-propagation-external-service4.yaml | 23 ++++++ .../token-propagation-external-service5.yaml | 23 ++++++ .../src/main/resources/application.properties | 52 ++++++++++++ .../it/security/KeycloakServiceMock.java | 79 +++++++++++++++++++ .../TokenPropagationExternalServicesMock.java | 75 ++++++++++++++++++ .../it/security/TokenPropagationTest.java | 46 +++++++++++ 11 files changed, 424 insertions(+) create mode 100644 integration-tests/security/src/main/java/io/quarkiverse/openapi/generator/it/security/TokenPropagationResource.java create mode 100644 integration-tests/security/src/main/openapi/token-propagation-external-service1.yaml create mode 100644 integration-tests/security/src/main/openapi/token-propagation-external-service2.yaml create mode 100644 integration-tests/security/src/main/openapi/token-propagation-external-service3.yaml create mode 100644 integration-tests/security/src/main/openapi/token-propagation-external-service4.yaml create mode 100644 integration-tests/security/src/main/openapi/token-propagation-external-service5.yaml create mode 100644 integration-tests/security/src/test/java/io/quarkiverse/openapi/generator/it/security/KeycloakServiceMock.java create mode 100644 integration-tests/security/src/test/java/io/quarkiverse/openapi/generator/it/security/TokenPropagationExternalServicesMock.java create mode 100644 integration-tests/security/src/test/java/io/quarkiverse/openapi/generator/it/security/TokenPropagationTest.java diff --git a/integration-tests/security/pom.xml b/integration-tests/security/pom.xml index 79a8b253..eb822ffa 100644 --- a/integration-tests/security/pom.xml +++ b/integration-tests/security/pom.xml @@ -26,6 +26,11 @@ wiremock-jre8 test + + io.rest-assured + rest-assured + test + diff --git a/integration-tests/security/src/main/java/io/quarkiverse/openapi/generator/it/security/TokenPropagationResource.java b/integration-tests/security/src/main/java/io/quarkiverse/openapi/generator/it/security/TokenPropagationResource.java new file mode 100644 index 00000000..52fcf77f --- /dev/null +++ b/integration-tests/security/src/main/java/io/quarkiverse/openapi/generator/it/security/TokenPropagationResource.java @@ -0,0 +1,60 @@ +package io.quarkiverse.openapi.generator.it.security; + +import javax.ws.rs.POST; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RestClient; + +@Path("/token_propagation") +public class TokenPropagationResource { + + @RestClient + org.acme.externalservice1.api.DefaultApi defaultApi1; + + @RestClient + org.acme.externalservice2.api.DefaultApi defaultApi2; + + @RestClient + org.acme.externalservice3.api.DefaultApi defaultApi3; + + @RestClient + org.acme.externalservice4.api.DefaultApi defaultApi4; + + @RestClient + org.acme.externalservice5.api.DefaultApi defaultApi5; + + @POST + @Path("service1") + public String service1() { + defaultApi1.executeQuery1(); + return "hello"; + } + + @POST + @Path("service2") + public String service2() { + defaultApi2.executeQuery2(); + return "hello"; + } + + @POST + @Path("service3") + public String service3() { + defaultApi3.executeQuery3(); + return "hello"; + } + + @POST + @Path("service4") + public String service4() { + defaultApi4.executeQuery4(); + return "hello"; + } + + @POST + @Path("service5") + public String service5() { + defaultApi5.executeQuery5(); + return "hello"; + } +} diff --git a/integration-tests/security/src/main/openapi/token-propagation-external-service1.yaml b/integration-tests/security/src/main/openapi/token-propagation-external-service1.yaml new file mode 100644 index 00000000..5de46f77 --- /dev/null +++ b/integration-tests/security/src/main/openapi/token-propagation-external-service1.yaml @@ -0,0 +1,19 @@ +--- +openapi: 3.0.3 +info: + title: token-propagation-external-service1 API + version: 2.0.0-SNAPSHOT +paths: + /token-propagation-external-service1/executeQuery1: + post: + operationId: executeQuery1 + responses: + "200": + description: OK + security: + - service1-http-bearer: [] +components: + securitySchemes: + service1-http-bearer: + type: http + scheme: bearer \ No newline at end of file diff --git a/integration-tests/security/src/main/openapi/token-propagation-external-service2.yaml b/integration-tests/security/src/main/openapi/token-propagation-external-service2.yaml new file mode 100644 index 00000000..504b669c --- /dev/null +++ b/integration-tests/security/src/main/openapi/token-propagation-external-service2.yaml @@ -0,0 +1,23 @@ +--- +openapi: 3.0.3 +info: + title: external-service2 API + version: 2.0.0-SNAPSHOT +paths: + /token-propagation-external-service2/executeQuery2: + post: + operationId: executeQuery2 + responses: + "200": + description: OK + security: + - service2-oauth2: [] +components: + securitySchemes: + service2-oauth2: + type: oauth2 + flows: + clientCredentials: + authorizationUrl: https://example.com/oauth + tokenUrl: https://example.com/oauth/token + scopes: {} \ No newline at end of file diff --git a/integration-tests/security/src/main/openapi/token-propagation-external-service3.yaml b/integration-tests/security/src/main/openapi/token-propagation-external-service3.yaml new file mode 100644 index 00000000..2aaf4cff --- /dev/null +++ b/integration-tests/security/src/main/openapi/token-propagation-external-service3.yaml @@ -0,0 +1,19 @@ +--- +openapi: 3.0.3 +info: + title: token-propagation-external-service3 API + version: 2.0.0-SNAPSHOT +paths: + /token-propagation-external-service3/executeQuery3: + post: + operationId: executeQuery3 + responses: + "200": + description: OK + security: + - service3-http-bearer: [] +components: + securitySchemes: + service3-http-bearer: + type: http + scheme: bearer \ No newline at end of file diff --git a/integration-tests/security/src/main/openapi/token-propagation-external-service4.yaml b/integration-tests/security/src/main/openapi/token-propagation-external-service4.yaml new file mode 100644 index 00000000..1b71eb0b --- /dev/null +++ b/integration-tests/security/src/main/openapi/token-propagation-external-service4.yaml @@ -0,0 +1,23 @@ +--- +openapi: 3.0.3 +info: + title: external-service4 API + version: 2.0.0-SNAPSHOT +paths: + /token-propagation-external-service4/executeQuery4: + post: + operationId: executeQuery4 + responses: + "200": + description: OK + security: + - service4-oauth2: [] +components: + securitySchemes: + service4-oauth2: + type: oauth2 + flows: + clientCredentials: + authorizationUrl: https://example.com/oauth + tokenUrl: https://example.com/oauth/token + scopes: {} \ No newline at end of file diff --git a/integration-tests/security/src/main/openapi/token-propagation-external-service5.yaml b/integration-tests/security/src/main/openapi/token-propagation-external-service5.yaml new file mode 100644 index 00000000..fb47293a --- /dev/null +++ b/integration-tests/security/src/main/openapi/token-propagation-external-service5.yaml @@ -0,0 +1,23 @@ +--- +openapi: 3.0.3 +info: + title: external-service5 API + version: 2.0.0-SNAPSHOT +paths: + /token-propagation-external-service5/executeQuery5: + post: + operationId: executeQuery5 + responses: + "200": + description: OK + security: + - service5-oauth2: [] +components: + securitySchemes: + service5-oauth2: + type: oauth2 + flows: + clientCredentials: + authorizationUrl: https://example.com/oauth + tokenUrl: https://example.com/oauth/token + scopes: {} \ No newline at end of file diff --git a/integration-tests/security/src/main/resources/application.properties b/integration-tests/security/src/main/resources/application.properties index e2b991c8..7bc13c5e 100644 --- a/integration-tests/security/src/main/resources/application.properties +++ b/integration-tests/security/src/main/resources/application.properties @@ -24,3 +24,55 @@ quarkus.openapi-generator.open_weather_no_security_yaml.auth.app_id.api-key=1234 # KOGITO-6458 (https://issues.redhat.com/browse/KOGITO-6458) - generate auth bindings even if security definition is missing # Note: The property value is the name of an existing securityScheme in the spec file quarkus.openapi-generator.codegen.default.security.scheme=app_id + +#Token propagation +quarkus.openapi-generator.codegen.spec.token_propagation_external_service1_yaml.base-package=org.acme.externalservice1 +quarkus.openapi-generator.codegen.spec.token_propagation_external_service2_yaml.base-package=org.acme.externalservice2 +quarkus.openapi-generator.codegen.spec.token_propagation_external_service3_yaml.base-package=org.acme.externalservice3 +quarkus.openapi-generator.codegen.spec.token_propagation_external_service4_yaml.base-package=org.acme.externalservice4 +quarkus.openapi-generator.codegen.spec.token_propagation_external_service5_yaml.base-package=org.acme.externalservice5 + +quarkus.rest-client.token_propagation_external_service1_yaml.url=${propagation-external-service-mock.url} +quarkus.rest-client.token_propagation_external_service2_yaml.url=${propagation-external-service-mock.url} +quarkus.rest-client.token_propagation_external_service3_yaml.url=${propagation-external-service-mock.url} +quarkus.rest-client.token_propagation_external_service4_yaml.url=${propagation-external-service-mock.url} +quarkus.rest-client.token_propagation_external_service5_yaml.url=${propagation-external-service-mock.url} + +# default propagation for token_propagation_external_service1 invocation +quarkus.openapi-generator.token_propagation_external_service1_yaml.auth.service1_http_bearer.token-propagation=true +# default propagation for token_propagation_external_service2 invocation +quarkus.openapi-generator.token_propagation_external_service2_yaml.auth.service2_oauth2.token-propagation=true +# propagate the token coming in the header SERVICE3_HEADER_TO_PROPAGATE for token_propagation_external_service3 invocation +quarkus.openapi-generator.token_propagation_external_service3_yaml.auth.service3_http_bearer.token-propagation=true +quarkus.openapi-generator.token_propagation_external_service3_yaml.auth.service3_http_bearer.header-name=SERVICE3_HEADER_TO_PROPAGATE +# propagate the token coming in the header SERVICE4_HEADER_TO_PROPAGATE for token_propagation_external_service4 invocation +quarkus.openapi-generator.token_propagation_external_service4_yaml.auth.service4_oauth2.token-propagation=true +quarkus.openapi-generator.token_propagation_external_service4_yaml.auth.service4_oauth2.header-name=SERVICE4_HEADER_TO_PROPAGATE + +# Oidc clients for the services that has oauth2 security. +# Oidc client used by the token_propagation_external_service2 +quarkus.oidc-client.service2_oauth2.auth-server-url=${keycloak.mock.service.url} +quarkus.oidc-client.service2_oauth2.token-path=${keycloak.mock.service.token-path} +quarkus.oidc-client.service2_oauth2.discovery-enabled=false +quarkus.oidc-client.service2_oauth2.client-id=kogito-app +quarkus.oidc-client.service2_oauth2.grant.type=client +quarkus.oidc-client.service2_oauth2.credentials.client-secret.method=basic +quarkus.oidc-client.service2_oauth2.credentials.client-secret.value=secret + +# Oidc client used by the token_propagation_external_service4 +quarkus.oidc-client.service4_oauth2.auth-server-url=${keycloak.mock.service.url} +quarkus.oidc-client.service4_oauth2.token-path=${keycloak.mock.service.token-path} +quarkus.oidc-client.service4_oauth2.discovery-enabled=false +quarkus.oidc-client.service4_oauth2.client-id=kogito-app +quarkus.oidc-client.service4_oauth2.grant.type=client +quarkus.oidc-client.service4_oauth2.credentials.client-secret.method=basic +quarkus.oidc-client.service4_oauth2.credentials.client-secret.value=secret + +# Oidc client used by the token_propagation_external_service5 +quarkus.oidc-client.service5_oauth2.auth-server-url=${keycloak.mock.service.url} +quarkus.oidc-client.service5_oauth2.token-path=${keycloak.mock.service.token-path} +quarkus.oidc-client.service5_oauth2.discovery-enabled=false +quarkus.oidc-client.service5_oauth2.client-id=kogito-app +quarkus.oidc-client.service5_oauth2.grant.type=client +quarkus.oidc-client.service5_oauth2.credentials.client-secret.method=basic +quarkus.oidc-client.service5_oauth2.credentials.client-secret.value=secret \ No newline at end of file diff --git a/integration-tests/security/src/test/java/io/quarkiverse/openapi/generator/it/security/KeycloakServiceMock.java b/integration-tests/security/src/test/java/io/quarkiverse/openapi/generator/it/security/KeycloakServiceMock.java new file mode 100644 index 00000000..1f985b0d --- /dev/null +++ b/integration-tests/security/src/test/java/io/quarkiverse/openapi/generator/it/security/KeycloakServiceMock.java @@ -0,0 +1,79 @@ +package io.quarkiverse.openapi.generator.it.security; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.configureFor; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE; +import static javax.ws.rs.core.MediaType.APPLICATION_FORM_URLENCODED; +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; + +import java.util.HashMap; +import java.util.Map; + +import com.github.tomakehurst.wiremock.WireMockServer; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; + +/** + * Lightweight Keycloak mock to use when an OidcClient is required, and we don't want/need to start a full Keycloak + * container as part of the tests, etc. Keep the things simple. + */ +public class KeycloakServiceMock implements QuarkusTestResourceLifecycleManager { + + public static final String KEY_CLOAK_SERVICE_URL = "keycloak.mock.service.url"; + public static final String KEY_CLOAK_SERVICE_TOKEN_PATH = "keycloak.mock.service.token-path"; + public static final String REALM = "kogito-tests"; + public static final String KEY_CLOAK_SERVICE_TOKEN_PATH_VALUE = "/realms/" + REALM + "/protocol/openid-connect/token"; + public static final String CLIENT_ID = "kogito-app"; + public static final String SECRET = "secret"; + public static final String KEYCLOAK_ACCESS_TOKEN = "KEYCLOAK_ACCESS_TOKEN"; + public static final String KEYCLOAK_REFRESH_TOKEN = "KEYCLOAK_REFRESH_TOKEN"; + public static final String KEYCLOAK_SESSION_STATE = "KEYCLOAK_SESSION_STATE"; + + public static final String AUTH_REQUEST_BODY = "grant_type=client_credentials"; + + private WireMockServer wireMockServer; + + @Override + public Map start() { + wireMockServer = new WireMockServer(options().dynamicPort()); + wireMockServer.start(); + configureFor(wireMockServer.port()); + + stubFor(post(KEY_CLOAK_SERVICE_TOKEN_PATH_VALUE) + .withHeader(CONTENT_TYPE, equalTo(APPLICATION_FORM_URLENCODED)) + .withBasicAuth(CLIENT_ID, SECRET) + .withRequestBody(equalTo(AUTH_REQUEST_BODY)) + .willReturn(aResponse() + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody(getTokenResult()))); + + Map properties = new HashMap<>(); + properties.put(KEY_CLOAK_SERVICE_URL, wireMockServer.baseUrl()); + properties.put(KEY_CLOAK_SERVICE_TOKEN_PATH, KEY_CLOAK_SERVICE_TOKEN_PATH_VALUE); + return properties; + } + + private static String getTokenResult() { + return "{\n" + + " \"access_token\": \"" + KEYCLOAK_ACCESS_TOKEN + "\",\n" + + " \"expires_in\": 300,\n" + + " \"refresh_expires_in\": 1800,\n" + + " \"refresh_token\": \"" + KEYCLOAK_REFRESH_TOKEN + "\",\n" + + " \"token_type\": \"bearer\",\n" + + " \"not-before-policy\": 0,\n" + + " \"session_state\": \"" + KEYCLOAK_SESSION_STATE + "\",\n" + + " \"scope\": \"email profile\"\n" + + "}"; + } + + @Override + public void stop() { + if (wireMockServer != null) { + wireMockServer.stop(); + } + } +} diff --git a/integration-tests/security/src/test/java/io/quarkiverse/openapi/generator/it/security/TokenPropagationExternalServicesMock.java b/integration-tests/security/src/test/java/io/quarkiverse/openapi/generator/it/security/TokenPropagationExternalServicesMock.java new file mode 100644 index 00000000..e9b0b5b1 --- /dev/null +++ b/integration-tests/security/src/test/java/io/quarkiverse/openapi/generator/it/security/TokenPropagationExternalServicesMock.java @@ -0,0 +1,75 @@ +package io.quarkiverse.openapi.generator.it.security; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.configureFor; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static io.quarkiverse.openapi.generator.it.security.KeycloakServiceMock.KEYCLOAK_ACCESS_TOKEN; +import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE; +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; + +import java.util.Map; + +import javax.ws.rs.core.HttpHeaders; + +import com.github.tomakehurst.wiremock.WireMockServer; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; + +public class TokenPropagationExternalServicesMock implements QuarkusTestResourceLifecycleManager { + + public static final String AUTHORIZATION_TOKEN = "AUTHORIZATION_TOKEN"; + public static final String SERVICE3_HEADER_TO_PROPAGATE = "SERVICE3_HEADER_TO_PROPAGATE"; + public static final String SERVICE3_AUTHORIZATION_TOKEN = "SERVICE3_AUTHORIZATION_TOKEN"; + public static final String SERVICE4_HEADER_TO_PROPAGATE = "SERVICE4_HEADER_TO_PROPAGATE"; + public static final String SERVICE4_AUTHORIZATION_TOKEN = "SERVICE4_AUTHORIZATION_TOKEN"; + + private static final String BEARER = "Bearer "; + + public static final String TOKEN_PROPAGATION_EXTERNAL_SERVICE_MOCK_URL = "propagation-external-service-mock.url"; + + private WireMockServer wireMockServer; + + @Override + public Map start() { + wireMockServer = new WireMockServer(options().dynamicPort()); + wireMockServer.start(); + configureFor(wireMockServer.port()); + + // stub the token-propagation-external-service1 invocation with the expected token + stubForExternalService("/token-propagation-external-service1/executeQuery1", AUTHORIZATION_TOKEN); + + // stub the token-propagation-external-service2 invocation with the expected token + stubForExternalService("/token-propagation-external-service2/executeQuery2", AUTHORIZATION_TOKEN); + + // stub the token-propagation-external-service3 invocation with the expected token + stubForExternalService("/token-propagation-external-service3/executeQuery3", SERVICE3_AUTHORIZATION_TOKEN); + + // stub the token-propagation-external-service4 invocation with the expected token + stubForExternalService("/token-propagation-external-service4/executeQuery4", SERVICE4_AUTHORIZATION_TOKEN); + + // stub the token-propagation-external-service5 invocation with the expected token, no propagation is produced + // in this case but the service must receive the token provided by Keycloak since it has oauth2 security + // configured. + stubForExternalService("/token-propagation-external-service5/executeQuery5", KEYCLOAK_ACCESS_TOKEN); + + return Map.of(TOKEN_PROPAGATION_EXTERNAL_SERVICE_MOCK_URL, wireMockServer.baseUrl()); + } + + private static void stubForExternalService(String tokenPropagationExternalServiceUrl, String authorizationToken) { + stubFor(post(tokenPropagationExternalServiceUrl) + .withHeader(HttpHeaders.AUTHORIZATION, equalTo(BEARER + authorizationToken)) + .willReturn(aResponse() + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody("{}"))); + } + + @Override + public void stop() { + if (wireMockServer != null) { + wireMockServer.stop(); + } + } +} diff --git a/integration-tests/security/src/test/java/io/quarkiverse/openapi/generator/it/security/TokenPropagationTest.java b/integration-tests/security/src/test/java/io/quarkiverse/openapi/generator/it/security/TokenPropagationTest.java new file mode 100644 index 00000000..31911448 --- /dev/null +++ b/integration-tests/security/src/test/java/io/quarkiverse/openapi/generator/it/security/TokenPropagationTest.java @@ -0,0 +1,46 @@ +package io.quarkiverse.openapi.generator.it.security; + +import static io.quarkiverse.openapi.generator.it.security.TokenPropagationExternalServicesMock.AUTHORIZATION_TOKEN; +import static io.quarkiverse.openapi.generator.it.security.TokenPropagationExternalServicesMock.SERVICE3_AUTHORIZATION_TOKEN; +import static io.quarkiverse.openapi.generator.it.security.TokenPropagationExternalServicesMock.SERVICE3_HEADER_TO_PROPAGATE; +import static io.quarkiverse.openapi.generator.it.security.TokenPropagationExternalServicesMock.SERVICE4_AUTHORIZATION_TOKEN; +import static io.quarkiverse.openapi.generator.it.security.TokenPropagationExternalServicesMock.SERVICE4_HEADER_TO_PROPAGATE; +import static io.restassured.RestAssured.given; + +import java.util.HashMap; +import java.util.Map; + +import javax.ws.rs.core.HttpHeaders; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTestResource(TokenPropagationExternalServicesMock.class) +@QuarkusTestResource(KeycloakServiceMock.class) +@QuarkusTest +// Enabled only for RESTEasy Classic while https://github.com/quarkiverse/quarkus-openapi-generator/issues/434 is not fixed +@Tag("resteasy-classic") +class TokenPropagationTest { + + @ParameterizedTest + @ValueSource(strings = { "service1", "service2", "service3", "service4", "service5" }) + void service1(String service) { + Map headers = new HashMap<>(); + // service token-propagation-external-service1 and token-propagation-external-service2 will receive the AUTHORIZATION_TOKEN + headers.put(HttpHeaders.AUTHORIZATION, AUTHORIZATION_TOKEN); + // service token-propagation-external-service3 will receive the SERVICE3_AUTHORIZATION_TOKEN + headers.put(SERVICE3_HEADER_TO_PROPAGATE, SERVICE3_AUTHORIZATION_TOKEN); + // service token-propagation-external-service4 will receive the SERVICE4_AUTHORIZATION_TOKEN + headers.put(SERVICE4_HEADER_TO_PROPAGATE, SERVICE4_AUTHORIZATION_TOKEN); + + given() + .headers(headers) + .post("/token_propagation/" + service) + .then() + .statusCode(200); + } +}