diff --git a/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/GeneratorProcessor.java b/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/GeneratorProcessor.java index 6538f9ce4..fa9e3faa9 100644 --- a/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/GeneratorProcessor.java +++ b/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/GeneratorProcessor.java @@ -174,6 +174,7 @@ void produceOauthAuthentication(CombinedIndexBuildItem beanArchiveBuildItem, .annotation(OpenApiSpec.class) .addValue("openApiSpecId", openApiSpecId) .done() + .addInjectionPoint(ClassType.create(DotName.createSimple(CredentialsProvider.class))) .addInjectionPoint(ClassType.create(OAuth2AuthenticationProvider.OidcClientRequestFilterDelegate.class), AnnotationInstance.builder(OidcClient.class).add("name", sanitizeAuthName(name)).build()) .addInjectionPoint(ClassType.create(DotName.createSimple(CredentialsProvider.class))) diff --git a/client/integration-tests/auth-provider/pom.xml b/client/integration-tests/auth-provider/pom.xml new file mode 100644 index 000000000..84a493979 --- /dev/null +++ b/client/integration-tests/auth-provider/pom.xml @@ -0,0 +1,124 @@ + + + + quarkus-openapi-generator-integration-tests + io.quarkiverse.openapi.generator + 3.0.0-lts-SNAPSHOT + + 4.0.0 + + quarkus-openapi-generator-it-auth-provider + Quarkus - OpenAPI Generator - Integration Tests - Client - Auth Provider + A few use cases that relies on authentication provider use cases with the OpenAPI Generator + + + + io.quarkiverse.openapi.generator + quarkus-openapi-generator + + + io.quarkiverse.openapi.generator + quarkus-openapi-generator-oidc + + + io.quarkus + quarkus-junit5 + test + + + org.wiremock + wiremock + test + + + io.rest-assured + rest-assured + test + + + + + + + io.quarkus + quarkus-maven-plugin + true + + + + build + generate-code + generate-code-tests + + + + + + + + + native-image + + + native + + + + + + maven-surefire-plugin + + ${native.surefire.skip} + + + + maven-failsafe-plugin + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + + native + + + + resteasy-reactive + + + io.quarkus + quarkus-rest-client-oidc-filter + + + + + resteasy-classic + + true + + + + io.quarkus + quarkus-resteasy-client-oidc-filter + + + io.quarkus + quarkus-resteasy-multipart + + + + + \ No newline at end of file diff --git a/client/integration-tests/auth-provider/src/main/java/io/quarkiverse/openapi/generator/it/auth/TokenServerResource.java b/client/integration-tests/auth-provider/src/main/java/io/quarkiverse/openapi/generator/it/auth/TokenServerResource.java new file mode 100644 index 000000000..9c3ae7e57 --- /dev/null +++ b/client/integration-tests/auth-provider/src/main/java/io/quarkiverse/openapi/generator/it/auth/TokenServerResource.java @@ -0,0 +1,50 @@ +package io.quarkiverse.openapi.generator.it.auth; + +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RestClient; + +@Path("/token_server") +public class TokenServerResource { + + @RestClient + org.acme.externalservice1.api.DefaultApi defaultApi1; + + @RestClient + org.acme.externalservice2.api.DefaultApi defaultApi2; + + @RestClient + org.acme.externalservice3.api.DefaultApi defaultApi3; + + @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("service5") + public String service5() { + defaultApi5.executeQuery5(); + return "hello"; + } +} diff --git a/client/integration-tests/auth-provider/src/main/java/io/quarkiverse/openapi/generator/it/auth/provider/CustomCredentialsProvider.java b/client/integration-tests/auth-provider/src/main/java/io/quarkiverse/openapi/generator/it/auth/provider/CustomCredentialsProvider.java new file mode 100644 index 000000000..b9022e801 --- /dev/null +++ b/client/integration-tests/auth-provider/src/main/java/io/quarkiverse/openapi/generator/it/auth/provider/CustomCredentialsProvider.java @@ -0,0 +1,30 @@ +package io.quarkiverse.openapi.generator.it.auth.provider; + +import java.util.Optional; + +import jakarta.annotation.Priority; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.Alternative; +import jakarta.enterprise.inject.Specializes; + +import io.quarkiverse.openapi.generator.providers.ConfigCredentialsProvider; +import io.quarkiverse.openapi.generator.providers.CredentialsContext; + +@Dependent +@Alternative +@Specializes +@Priority(201) +public class CustomCredentialsProvider extends ConfigCredentialsProvider { + public CustomCredentialsProvider() { + } + + @Override + public Optional getBearerToken(CredentialsContext input) { + return Optional.of("BEARER_TOKEN_TEST"); + } + + @Override + public Optional getOauth2BearerToken(CredentialsContext input) { + return Optional.of("KEYCLOAK_ACCESS_TOKEN_TEST"); + } +} diff --git a/client/integration-tests/auth-provider/src/main/openapi/token-external-service1.yaml b/client/integration-tests/auth-provider/src/main/openapi/token-external-service1.yaml new file mode 100644 index 000000000..dd18964c1 --- /dev/null +++ b/client/integration-tests/auth-provider/src/main/openapi/token-external-service1.yaml @@ -0,0 +1,19 @@ +--- +openapi: 3.0.3 +info: + title: token-external-service1 API + version: 3.0.0-SNAPSHOT +paths: + /token-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/client/integration-tests/auth-provider/src/main/openapi/token-external-service2.yaml b/client/integration-tests/auth-provider/src/main/openapi/token-external-service2.yaml new file mode 100644 index 000000000..726200831 --- /dev/null +++ b/client/integration-tests/auth-provider/src/main/openapi/token-external-service2.yaml @@ -0,0 +1,23 @@ +--- +openapi: 3.0.3 +info: + title: token-external-service2 API + version: 3.0.0-SNAPSHOT +paths: + /token-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/client/integration-tests/auth-provider/src/main/openapi/token-external-service3.yaml b/client/integration-tests/auth-provider/src/main/openapi/token-external-service3.yaml new file mode 100644 index 000000000..981daef3c --- /dev/null +++ b/client/integration-tests/auth-provider/src/main/openapi/token-external-service3.yaml @@ -0,0 +1,19 @@ +--- +openapi: 3.0.3 +info: + title: token-external-service3 API + version: 3.0.0-SNAPSHOT +paths: + /token-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/client/integration-tests/auth-provider/src/main/openapi/token-external-service5.yaml b/client/integration-tests/auth-provider/src/main/openapi/token-external-service5.yaml new file mode 100644 index 000000000..4490dcf00 --- /dev/null +++ b/client/integration-tests/auth-provider/src/main/openapi/token-external-service5.yaml @@ -0,0 +1,23 @@ +--- +openapi: 3.0.3 +info: + title: token-external-service5 API + version: 3.0.0-SNAPSHOT +paths: + /token-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/client/integration-tests/auth-provider/src/main/resources/application.properties b/client/integration-tests/auth-provider/src/main/resources/application.properties new file mode 100644 index 000000000..eeed16b8a --- /dev/null +++ b/client/integration-tests/auth-provider/src/main/resources/application.properties @@ -0,0 +1,42 @@ +# 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 service +quarkus.openapi-generator.codegen.spec.token_external_service1_yaml.base-package=org.acme.externalservice1 +quarkus.openapi-generator.codegen.spec.token_external_service2_yaml.base-package=org.acme.externalservice2 +quarkus.openapi-generator.codegen.spec.token_external_service3_yaml.base-package=org.acme.externalservice3 +quarkus.openapi-generator.codegen.spec.token_external_service5_yaml.base-package=org.acme.externalservice5 + +quarkus.rest-client.token_external_service1_yaml.url=${propagation-external-service-mock.url} +quarkus.rest-client.token_external_service2_yaml.url=${propagation-external-service-mock.url} +quarkus.rest-client.token_external_service3_yaml.url=${propagation-external-service-mock.url} +quarkus.rest-client.token_external_service5_yaml.url=${propagation-external-service-mock.url} + +# default propagation for token_external_service1 invocation +quarkus.openapi-generator.token_external_service1_yaml.auth.service1_http_bearer.token-propagation=true +# default propagation for token_external_service2 invocation +quarkus.openapi-generator.token_external_service2_yaml.auth.service2_oauth2.token-propagation=true + +quarkus.openapi-generator.token_external_service3_yaml.auth.service3_http_bearer.bearer-token=BEARER_TOKEN + +# Oidc clients for the services that has oauth2 security. +# Oidc client used by the token_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_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 + +quarkus.keycloak.devservices.enabled=false \ No newline at end of file diff --git a/client/integration-tests/auth-provider/src/test/java/io/quarkiverse/openapi/generator/it/auth/KeycloakServiceMock.java b/client/integration-tests/auth-provider/src/test/java/io/quarkiverse/openapi/generator/it/auth/KeycloakServiceMock.java new file mode 100644 index 000000000..477d7d253 --- /dev/null +++ b/client/integration-tests/auth-provider/src/test/java/io/quarkiverse/openapi/generator/it/auth/KeycloakServiceMock.java @@ -0,0 +1,82 @@ +package io.quarkiverse.openapi.generator.it.auth; + +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 jakarta.ws.rs.core.HttpHeaders.CONTENT_TYPE; +import static jakarta.ws.rs.core.MediaType.APPLICATION_FORM_URLENCODED; +import static jakarta.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 static final ThreadLocal wireMockServer = new ThreadLocal<>(); + + @Override + public Map start() { + wireMockServer.set(new WireMockServer(options().dynamicPort())); + wireMockServer.get().start(); + configureFor(wireMockServer.get().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.get().baseUrl()); + properties.put(KEY_CLOAK_SERVICE_TOKEN_PATH, KEY_CLOAK_SERVICE_TOKEN_PATH_VALUE); + return properties; + } + + private static String getTokenResult() { + return """ + { + "access_token": "%s", + "expires_in": 300, + "refresh_expires_in": 1800, + "refresh_token": "%s", + "token_type": "bearer", + "not-before-policy": 0, + "session_state": "%s", + "scope": "email profile" + } + """.formatted(KEYCLOAK_ACCESS_TOKEN, KEYCLOAK_REFRESH_TOKEN, KEYCLOAK_SESSION_STATE); + } + + @Override + public void stop() { + if (wireMockServer.get() != null) { + wireMockServer.get().stop(); + wireMockServer.remove(); + } + } +} diff --git a/client/integration-tests/auth-provider/src/test/java/io/quarkiverse/openapi/generator/it/auth/TokenExternalServicesMock.java b/client/integration-tests/auth-provider/src/test/java/io/quarkiverse/openapi/generator/it/auth/TokenExternalServicesMock.java new file mode 100644 index 000000000..7a4545218 --- /dev/null +++ b/client/integration-tests/auth-provider/src/test/java/io/quarkiverse/openapi/generator/it/auth/TokenExternalServicesMock.java @@ -0,0 +1,72 @@ +package io.quarkiverse.openapi.generator.it.auth; + +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.auth.KeycloakServiceMock.KEYCLOAK_ACCESS_TOKEN; +import static jakarta.ws.rs.core.HttpHeaders.CONTENT_TYPE; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; + +import java.util.Map; + +import jakarta.ws.rs.core.HttpHeaders; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.tomakehurst.wiremock.WireMockServer; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; + +public class TokenExternalServicesMock implements QuarkusTestResourceLifecycleManager { + + public static final String AUTHORIZATION_TOKEN = "AUTHORIZATION_TOKEN"; + public static final String SERVICE3_AUTHORIZATION_TOKEN = "BEARER_TOKEN"; + public static final String TOKEN_EXTERNAL_SERVICE_MOCK_URL = "propagation-external-service-mock.url"; + private static final String BEARER = "Bearer "; + private static final Logger LOGGER = LoggerFactory.getLogger(TokenExternalServicesMock.class); + private WireMockServer wireMockServer; + + 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 Map start() { + wireMockServer = new WireMockServer(options().dynamicPort()); + wireMockServer.start(); + configureFor(wireMockServer.port()); + LOGGER.info("Mocked Server started at {}", wireMockServer.baseUrl()); + + // stub the token-external-service1 invocation with the expected token + stubForExternalService("/token-external-service1/executeQuery1", AUTHORIZATION_TOKEN); + + // stub the token-external-service2 invocation with the expected token + stubForExternalService("/token-external-service2/executeQuery2", AUTHORIZATION_TOKEN); + + // stub the token-external-service3 invocation with the expected token taken from the + // application.properties and overridden by the custom credential provider + stubForExternalService("/token-external-service3/executeQuery3", SERVICE3_AUTHORIZATION_TOKEN + "_TEST"); + + // stub the token-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. The token will be overridden by the custom credential provider + stubForExternalService("/token-external-service5/executeQuery5", KEYCLOAK_ACCESS_TOKEN + "_TEST"); + + return Map.of(TOKEN_EXTERNAL_SERVICE_MOCK_URL, wireMockServer.baseUrl()); + } + + @Override + public void stop() { + if (wireMockServer != null) { + wireMockServer.stop(); + } + } +} diff --git a/client/integration-tests/auth-provider/src/test/java/io/quarkiverse/openapi/generator/it/auth/TokenWithCustomCredentialProviderTest.java b/client/integration-tests/auth-provider/src/test/java/io/quarkiverse/openapi/generator/it/auth/TokenWithCustomCredentialProviderTest.java new file mode 100644 index 000000000..9fd525475 --- /dev/null +++ b/client/integration-tests/auth-provider/src/test/java/io/quarkiverse/openapi/generator/it/auth/TokenWithCustomCredentialProviderTest.java @@ -0,0 +1,35 @@ +package io.quarkiverse.openapi.generator.it.auth; + +import static io.quarkiverse.openapi.generator.it.auth.TokenExternalServicesMock.AUTHORIZATION_TOKEN; +import static io.restassured.RestAssured.given; + +import java.util.Map; + +import jakarta.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(TokenExternalServicesMock.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 TokenWithCustomCredentialProviderTest { + + @ParameterizedTest + @ValueSource(strings = { "service1", "service2", "service3", "service5" }) + void testService(String service) { + Map headers = Map.of(HttpHeaders.AUTHORIZATION, AUTHORIZATION_TOKEN); + + given() + .headers(headers) + .post("/token_server/" + service) + .then() + .statusCode(200); + } +} diff --git a/client/integration-tests/override-credential-provider/src/main/java/io/quarkiverse/openapi/generator/it/creds/CustomCredentialsProvider.java b/client/integration-tests/override-credential-provider/src/main/java/io/quarkiverse/openapi/generator/it/creds/CustomCredentialsProvider.java index 0ac9e2282..00a445982 100644 --- a/client/integration-tests/override-credential-provider/src/main/java/io/quarkiverse/openapi/generator/it/creds/CustomCredentialsProvider.java +++ b/client/integration-tests/override-credential-provider/src/main/java/io/quarkiverse/openapi/generator/it/creds/CustomCredentialsProvider.java @@ -1,14 +1,16 @@ package io.quarkiverse.openapi.generator.it.creds; +import java.util.Optional; + import jakarta.annotation.Priority; import jakarta.enterprise.context.Dependent; import jakarta.enterprise.inject.Alternative; -import jakarta.ws.rs.client.ClientRequestContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.quarkiverse.openapi.generator.providers.ConfigCredentialsProvider; +import io.quarkiverse.openapi.generator.providers.CredentialsContext; @Dependent @Alternative @@ -19,8 +21,8 @@ public class CustomCredentialsProvider extends ConfigCredentialsProvider { public static String TOKEN = "FIXED_TEST_TOKEN"; @Override - public String getBearerToken(ClientRequestContext requestContext, String openApiSpecId, String authName) { + public Optional getBearerToken(CredentialsContext input) { LOGGER.info("========> getBearerToken from CustomCredentialsProvider"); - return TOKEN; + return Optional.of(TOKEN); } -} \ No newline at end of file +} diff --git a/client/integration-tests/pom.xml b/client/integration-tests/pom.xml index 362a3a38c..661f2ec90 100644 --- a/client/integration-tests/pom.xml +++ b/client/integration-tests/pom.xml @@ -12,6 +12,7 @@ pom additional-properties + auth-provider array-enum bean-validation beanparam diff --git a/client/integration-tests/security/src/main/java/io/quarkiverse/openapi/generator/it/security/auth/DummyApiKeyAuthenticationProvider.java b/client/integration-tests/security/src/main/java/io/quarkiverse/openapi/generator/it/security/auth/DummyApiKeyAuthenticationProvider.java index 1096cca18..2d60e0e44 100644 --- a/client/integration-tests/security/src/main/java/io/quarkiverse/openapi/generator/it/security/auth/DummyApiKeyAuthenticationProvider.java +++ b/client/integration-tests/security/src/main/java/io/quarkiverse/openapi/generator/it/security/auth/DummyApiKeyAuthenticationProvider.java @@ -12,6 +12,7 @@ import io.quarkiverse.openapi.generator.providers.ApiKeyAuthenticationProvider; import io.quarkiverse.openapi.generator.providers.ApiKeyIn; import io.quarkiverse.openapi.generator.providers.AuthProvider; +import io.quarkiverse.openapi.generator.providers.ConfigCredentialsProvider; @Priority(Priorities.AUTHENTICATION) public class DummyApiKeyAuthenticationProvider implements ClientRequestFilter { @@ -21,7 +22,7 @@ public class DummyApiKeyAuthenticationProvider implements ClientRequestFilter { @PostConstruct public void init() { authProvider = new ApiKeyAuthenticationProvider("open_weather_custom_security_yaml", "app_id", ApiKeyIn.query, "appid", - List.of()); + List.of(), new ConfigCredentialsProvider()); } @Override diff --git a/client/integration-tests/security/src/test/java/io/quarkiverse/openapi/generator/it/security/KeycloakServiceMock.java b/client/integration-tests/security/src/test/java/io/quarkiverse/openapi/generator/it/security/KeycloakServiceMock.java index 06fd6aa57..264b34bea 100644 --- a/client/integration-tests/security/src/test/java/io/quarkiverse/openapi/generator/it/security/KeycloakServiceMock.java +++ b/client/integration-tests/security/src/test/java/io/quarkiverse/openapi/generator/it/security/KeycloakServiceMock.java @@ -35,13 +35,13 @@ public class KeycloakServiceMock implements QuarkusTestResourceLifecycleManager public static final String AUTH_REQUEST_BODY = "grant_type=client_credentials"; - private WireMockServer wireMockServer; + private static final ThreadLocal wireMockServer = new ThreadLocal<>(); @Override public Map start() { - wireMockServer = new WireMockServer(options().dynamicPort()); - wireMockServer.start(); - configureFor(wireMockServer.port()); + wireMockServer.set(new WireMockServer(options().dynamicPort())); + wireMockServer.get().start(); + configureFor(wireMockServer.get().port()); stubFor(post(KEY_CLOAK_SERVICE_TOKEN_PATH_VALUE) .withHeader(CONTENT_TYPE, equalTo(APPLICATION_FORM_URLENCODED)) @@ -52,28 +52,31 @@ public Map start() { .withBody(getTokenResult()))); Map properties = new HashMap<>(); - properties.put(KEY_CLOAK_SERVICE_URL, wireMockServer.baseUrl()); + properties.put(KEY_CLOAK_SERVICE_URL, wireMockServer.get().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" + - "}"; + return """ + { + "access_token": "%s", + "expires_in": 300, + "refresh_expires_in": 1800, + "refresh_token": "%s", + "token_type": "bearer", + "not-before-policy": 0, + "session_state": "%s", + "scope": "email profile" + } + """.formatted(KEYCLOAK_ACCESS_TOKEN, KEYCLOAK_REFRESH_TOKEN, KEYCLOAK_SESSION_STATE); } @Override public void stop() { - if (wireMockServer != null) { - wireMockServer.stop(); + if (wireMockServer.get() != null) { + wireMockServer.get().stop(); + wireMockServer.remove(); } } } diff --git a/client/oidc/pom.xml b/client/oidc/pom.xml index 078a5ea01..3da32f6bb 100644 --- a/client/oidc/pom.xml +++ b/client/oidc/pom.xml @@ -27,6 +27,31 @@ quarkus-resteasy-client-oidc-filter provided + + org.assertj + assertj-core + test + + + org.mockito + mockito-core + test + + + org.mockito + mockito-junit-jupiter + test + + + io.quarkus + quarkus-junit5 + test + + + org.jboss.resteasy + resteasy-core + test + \ No newline at end of file diff --git a/client/oidc/src/main/java/io/quarkiverse/openapi/generator/oidc/ClassicOidcClientRequestFilterDelegate.java b/client/oidc/src/main/java/io/quarkiverse/openapi/generator/oidc/ClassicOidcClientRequestFilterDelegate.java index f37895c8a..677ee5b48 100644 --- a/client/oidc/src/main/java/io/quarkiverse/openapi/generator/oidc/ClassicOidcClientRequestFilterDelegate.java +++ b/client/oidc/src/main/java/io/quarkiverse/openapi/generator/oidc/ClassicOidcClientRequestFilterDelegate.java @@ -7,6 +7,7 @@ import jakarta.ws.rs.Priorities; import jakarta.ws.rs.client.ClientRequestContext; import jakarta.ws.rs.client.ClientRequestFilter; +import jakarta.ws.rs.core.HttpHeaders; import org.jboss.logging.Logger; @@ -41,7 +42,7 @@ protected java.util.Optional clientId() { public void filter(ClientRequestContext requestContext) throws IOException { try { String accessToken = this.getAccessToken(); - requestContext.getHeaders().add("Authorization", "Bearer " + accessToken); + requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken); } catch (DisabledOidcClientException ex) { LOG.debug("Client is disabled, acquiring and propagating the token is not necessary"); } catch (RuntimeException ex) { diff --git a/client/oidc/src/main/java/io/quarkiverse/openapi/generator/oidc/providers/OAuth2AuthenticationProvider.java b/client/oidc/src/main/java/io/quarkiverse/openapi/generator/oidc/providers/OAuth2AuthenticationProvider.java index 4b5a9ea39..676f77876 100644 --- a/client/oidc/src/main/java/io/quarkiverse/openapi/generator/oidc/providers/OAuth2AuthenticationProvider.java +++ b/client/oidc/src/main/java/io/quarkiverse/openapi/generator/oidc/providers/OAuth2AuthenticationProvider.java @@ -4,16 +4,19 @@ import java.io.IOException; import java.util.List; +import java.util.Optional; import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.core.HttpHeaders; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.quarkiverse.openapi.generator.providers.AbstractAuthProvider; +import io.quarkiverse.openapi.generator.providers.AuthUtils; +import io.quarkiverse.openapi.generator.providers.CredentialsContext; import io.quarkiverse.openapi.generator.providers.CredentialsProvider; import io.quarkiverse.openapi.generator.providers.OperationAuthInfo; -import io.quarkus.oidc.common.runtime.OidcConstants; public class OAuth2AuthenticationProvider extends AbstractAuthProvider { @@ -31,16 +34,38 @@ public OAuth2AuthenticationProvider(String name, @Override public void filter(ClientRequestContext requestContext) throws IOException { - if (isTokenPropagation()) { - String bearerToken = sanitizeBearerToken(getTokenForPropagation(requestContext.getHeaders())); - if (!isEmptyOrBlank(bearerToken)) { - addAuthorizationHeader(requestContext.getHeaders(), OidcConstants.BEARER_SCHEME + " " + bearerToken); - } else { - LOGGER.debug("No oauth2 bearer token was found to propagate for the security scheme: {}." + - " You must verify that the request header: {} is set.", getName(), getHeaderForPropagation()); + String bearerToken = ""; + + if (this.isTokenPropagation()) { + bearerToken = this.getTokenForPropagation(requestContext.getHeaders()); + if (isEmptyOrBlank(bearerToken)) { + LOGGER.debug( + "Token propagation for OAUTH2 is enabled but the configured propagation header defined by {} is not present", + getHeaderForPropagation(getOpenApiSpecId(), getName())); } } else { - delegate.filter(requestContext); + Optional optionalBearerToken = this.getCredentialsProvider() + .getOauth2BearerToken(CredentialsContext.builder() + .requestContext(requestContext) + .openApiSpecId(getOpenApiSpecId()) + .authName(getName()) + .build()); + if (optionalBearerToken.isPresent()) { + bearerToken = optionalBearerToken.get(); + if (isEmptyOrBlank(bearerToken)) { + LOGGER.debug("The CredentialProvider implementation returned an empty OAUTH2 bearer"); + } + } else { + LOGGER.debug( + "There is no custom CredentialProvider implementation, the {} header will be set using delegate's filter. ", + HttpHeaders.AUTHORIZATION); + delegate.filter(requestContext); + } + } + + if (!isEmptyOrBlank(bearerToken)) { + addAuthorizationHeader(requestContext.getHeaders(), + AuthUtils.authTokenOrBearer("Bearer", AbstractAuthProvider.sanitizeBearerToken(bearerToken))); } } @@ -55,4 +80,4 @@ private void validateConfig() { public interface OidcClientRequestFilterDelegate { void filter(ClientRequestContext requestContext) throws IOException; } -} +} \ No newline at end of file diff --git a/client/oidc/src/test/java/io/quarkiverse/openapi/generator/oidc/OAuth2AuthenticationProviderTest.java b/client/oidc/src/test/java/io/quarkiverse/openapi/generator/oidc/OAuth2AuthenticationProviderTest.java new file mode 100644 index 000000000..aa54b54b7 --- /dev/null +++ b/client/oidc/src/test/java/io/quarkiverse/openapi/generator/oidc/OAuth2AuthenticationProviderTest.java @@ -0,0 +1,126 @@ +package io.quarkiverse.openapi.generator.oidc; + +import static io.quarkiverse.openapi.generator.providers.AbstractAuthenticationPropagationHeadersFactory.propagationHeaderName; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MultivaluedHashMap; +import jakarta.ws.rs.core.MultivaluedMap; + +import org.assertj.core.api.Assertions; +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import io.quarkiverse.openapi.generator.AuthConfig; +import io.quarkiverse.openapi.generator.oidc.providers.OAuth2AuthenticationProvider; +import io.quarkiverse.openapi.generator.providers.ConfigCredentialsProvider; +import io.quarkus.oidc.client.Tokens; + +@ExtendWith(MockitoExtension.class) +public class OAuth2AuthenticationProviderTest { + private static final String OPEN_API_FILE_SPEC_ID = "open_api_file_spec_id_json"; + private static final String AUTH_SCHEME_NAME = "auth_scheme_name"; + + private static final String ACCESS_TOKEN = "ACCESS_TOKEN"; + + private static final String PROPAGATED_TOKEN = "PROPAGATED_TOKEN"; + + private static final String HEADER_NAME = "HEADER_NAME"; + + @Mock + private ClientRequestContext requestContext; + + private MultivaluedMap headers; + + private ClassicOidcClientRequestFilterDelegate classicDelegate; + + private static final Tokens token = new Tokens(ACCESS_TOKEN, Long.MAX_VALUE, null, "", Long.MAX_VALUE, null, ""); + + private OAuth2AuthenticationProvider provider; + + @BeforeEach + void setUp() { + headers = new MultivaluedHashMap<>(); + Mockito.lenient().doReturn(headers).when(requestContext).getHeaders(); + + classicDelegate = Mockito.mock(ClassicOidcClientRequestFilterDelegate.class); + Mockito.lenient().when(classicDelegate.awaitTokens()).thenReturn(token); + try { + Mockito.lenient().doCallRealMethod().when(classicDelegate).filter(requestContext); + } catch (IOException e) { + throw new RuntimeException(e); + } + provider = createClassicProvider(); + + } + + private OAuth2AuthenticationProvider createClassicProvider() { + return new OAuth2AuthenticationProvider(AUTH_SCHEME_NAME, OPEN_API_FILE_SPEC_ID, classicDelegate, List.of(), + new ConfigCredentialsProvider()); + } + + private void assertHeader(MultivaluedMap headers, String headerName, String value) { + Assertions.assertThat(headers.getFirst(headerName)) + .isNotNull() + .isEqualTo(value); + } + + static Stream filterWithPropagationTestValues() { + return Stream.of( + Arguments.of(null, "Bearer " + PROPAGATED_TOKEN), + Arguments.of(HEADER_NAME, "Bearer " + PROPAGATED_TOKEN)); + } + + @Test + void filterClassic() throws IOException { + filter(provider, "Bearer " + ACCESS_TOKEN); + } + + private void filter(OAuth2AuthenticationProvider provider, String expectedAuthorizationHeader) throws IOException { + provider.filter(requestContext); + assertHeader(headers, HttpHeaders.AUTHORIZATION, expectedAuthorizationHeader); + } + + @ParameterizedTest + @MethodSource("filterWithPropagationTestValues") + void filterWithPropagation(String headerName, + String expectedAuthorizationHeader) throws IOException { + String propagatedHeaderName; + if (headerName == null) { + propagatedHeaderName = propagationHeaderName(OPEN_API_FILE_SPEC_ID, AUTH_SCHEME_NAME, + HttpHeaders.AUTHORIZATION); + } else { + propagatedHeaderName = propagationHeaderName(OPEN_API_FILE_SPEC_ID, AUTH_SCHEME_NAME, + HEADER_NAME); + } + try (MockedStatic configProviderMocked = Mockito.mockStatic(ConfigProvider.class)) { + Config mockedConfig = Mockito.mock(Config.class); + configProviderMocked.when(ConfigProvider::getConfig).thenReturn(mockedConfig); + + when(mockedConfig.getOptionalValue(provider.getCanonicalAuthConfigPropertyName(AuthConfig.TOKEN_PROPAGATION), + Boolean.class)).thenReturn(Optional.of(true)); + when(mockedConfig.getOptionalValue(provider.getCanonicalAuthConfigPropertyName(AuthConfig.HEADER_NAME), + String.class)).thenReturn(Optional.of(headerName == null ? HttpHeaders.AUTHORIZATION : headerName)); + + headers.putSingle(propagatedHeaderName, PROPAGATED_TOKEN); + filter(provider, expectedAuthorizationHeader); + } + } + +} diff --git a/client/oidc/src/test/java/io/quarkiverse/openapi/generator/oidc/ReactiveOAuth2AuthenticationProviderTest.java b/client/oidc/src/test/java/io/quarkiverse/openapi/generator/oidc/ReactiveOAuth2AuthenticationProviderTest.java new file mode 100644 index 000000000..a450f3cb2 --- /dev/null +++ b/client/oidc/src/test/java/io/quarkiverse/openapi/generator/oidc/ReactiveOAuth2AuthenticationProviderTest.java @@ -0,0 +1,138 @@ +package io.quarkiverse.openapi.generator.oidc; + +import static io.quarkiverse.openapi.generator.providers.AbstractAuthenticationPropagationHeadersFactory.propagationHeaderName; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MultivaluedHashMap; +import jakarta.ws.rs.core.MultivaluedMap; + +import org.assertj.core.api.Assertions; +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; +import org.jboss.resteasy.reactive.client.impl.ClientRequestContextImpl; +import org.jboss.resteasy.reactive.client.impl.RestClientRequestContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import io.quarkiverse.openapi.generator.AuthConfig; +import io.quarkiverse.openapi.generator.oidc.providers.OAuth2AuthenticationProvider; +import io.quarkiverse.openapi.generator.providers.ConfigCredentialsProvider; +import io.quarkus.oidc.client.Tokens; +import io.smallrye.mutiny.Uni; + +@ExtendWith(MockitoExtension.class) +public class ReactiveOAuth2AuthenticationProviderTest { + private static final String OPEN_API_FILE_SPEC_ID = "open_api_file_spec_id_json"; + private static final String AUTH_SCHEME_NAME = "auth_scheme_name"; + + private static final String ACCESS_TOKEN = "REACTIVE_ACCESS_TOKEN"; + + private static final String PROPAGATED_TOKEN = "PROPAGATED_TOKEN"; + + private static final String HEADER_NAME = "HEADER_NAME"; + @Mock + private ClientRequestContextImpl reactiveRequestContext; + + @Mock + private RestClientRequestContext restClientRequestContext; + private MultivaluedMap headers; + + private ReactiveOidcClientRequestFilterDelegate reactiveDelegate; + private static final Tokens token = new Tokens(ACCESS_TOKEN, Long.MAX_VALUE, null, "", Long.MAX_VALUE, null, ""); + private static final Uni uniToken = Uni.createFrom().item(token); + + private OAuth2AuthenticationProvider provider; + + @BeforeEach + void setUp() { + headers = new MultivaluedHashMap<>(); + Mockito.lenient().doReturn(headers).when(reactiveRequestContext).getHeaders(); + Mockito.lenient().doReturn(restClientRequestContext).when(reactiveRequestContext).getRestClientRequestContext(); + Mockito.lenient().doAnswer(invocationOnMock -> restClientRequestContext.setSuspended(true)) + .when(restClientRequestContext).suspend(); + Mockito.lenient().doAnswer(invocationOnMock -> restClientRequestContext.setSuspended(false)) + .when(restClientRequestContext).resume(); + reactiveDelegate = Mockito.mock(ReactiveOidcClientRequestFilterDelegate.class); + try { + Mockito.lenient().doCallRealMethod().when(reactiveDelegate).filter(Mockito.any(ClientRequestContext.class)); + } catch (IOException e) { + throw new RuntimeException(e); + } + Mockito.lenient().doCallRealMethod().when(reactiveDelegate).filter(reactiveRequestContext); + Mockito.lenient().when(reactiveDelegate.getTokens()).thenReturn(uniToken); + + provider = createReactiveProvider(); + } + + protected OAuth2AuthenticationProvider createReactiveProvider() { + return new OAuth2AuthenticationProvider(AUTH_SCHEME_NAME, OPEN_API_FILE_SPEC_ID, reactiveDelegate, List.of(), + new ConfigCredentialsProvider()); + } + + protected void assertHeader(MultivaluedMap headers, String headerName, String value) { + Assertions.assertThat(headers.getFirst(headerName)) + .isNotNull() + .isEqualTo(value); + } + + static Stream filterWithPropagationTestValues() { + return Stream.of( + Arguments.of(null, "Bearer " + PROPAGATED_TOKEN), + Arguments.of(HEADER_NAME, "Bearer " + PROPAGATED_TOKEN)); + } + + @Test + void filterReactive() throws IOException, InterruptedException { + filter(provider, "Bearer " + ACCESS_TOKEN); + } + + private void filter(OAuth2AuthenticationProvider provider, String expectedAuthorizationHeader) + throws IOException, InterruptedException { + provider.filter(reactiveRequestContext); + while (reactiveRequestContext.getRestClientRequestContext().isSuspended()) { + Thread.sleep(1000); + } + assertHeader(headers, HttpHeaders.AUTHORIZATION, expectedAuthorizationHeader); + } + + @ParameterizedTest + @MethodSource("filterWithPropagationTestValues") + void filterWithPropagation(String headerName, + String expectedAuthorizationHeader) throws IOException, InterruptedException { + String propagatedHeaderName; + if (headerName == null) { + propagatedHeaderName = propagationHeaderName(OPEN_API_FILE_SPEC_ID, AUTH_SCHEME_NAME, + HttpHeaders.AUTHORIZATION); + } else { + propagatedHeaderName = propagationHeaderName(OPEN_API_FILE_SPEC_ID, AUTH_SCHEME_NAME, + HEADER_NAME); + } + try (MockedStatic configProviderMocked = Mockito.mockStatic(ConfigProvider.class)) { + Config mockedConfig = Mockito.mock(Config.class); + configProviderMocked.when(ConfigProvider::getConfig).thenReturn(mockedConfig); + + when(mockedConfig.getOptionalValue(provider.getCanonicalAuthConfigPropertyName(AuthConfig.TOKEN_PROPAGATION), + Boolean.class)).thenReturn(Optional.of(true)); + when(mockedConfig.getOptionalValue(provider.getCanonicalAuthConfigPropertyName(AuthConfig.HEADER_NAME), + String.class)).thenReturn(Optional.of(headerName == null ? HttpHeaders.AUTHORIZATION : headerName)); + + headers.putSingle(propagatedHeaderName, PROPAGATED_TOKEN); + filter(provider, expectedAuthorizationHeader); + } + } +} diff --git a/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/AbstractAuthProvider.java b/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/AbstractAuthProvider.java index ff738a16e..cc4acd5d9 100644 --- a/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/AbstractAuthProvider.java +++ b/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/AbstractAuthProvider.java @@ -61,18 +61,23 @@ public String getName() { } public boolean isTokenPropagation() { - return ConfigProvider.getConfig() - .getOptionalValue(getCanonicalAuthConfigPropertyName(AuthConfig.TOKEN_PROPAGATION), Boolean.class) - .orElse(false); + return isTokenPropagation(getOpenApiSpecId(), getName()); } - public String getTokenForPropagation(MultivaluedMap httpHeaders) { - String propagatedHeaderName = propagationHeaderName(getOpenApiSpecId(), getName(), getHeaderForPropagation()); + public static String getTokenForPropagation(MultivaluedMap httpHeaders, String openApiSpecId, + String authName) { + String headerName = getHeaderForPropagation(openApiSpecId, authName); + String propagatedHeaderName = propagationHeaderName(openApiSpecId, authName, headerName); return Objects.toString(httpHeaders.getFirst(propagatedHeaderName), null); } - public String getHeaderForPropagation() { - return getHeaderName() != null ? getHeaderName() : HttpHeaders.AUTHORIZATION; + public String getTokenForPropagation(MultivaluedMap httpHeaders) { + return getTokenForPropagation(httpHeaders, getOpenApiSpecId(), getName()); + } + + public static String getHeaderForPropagation(String openApiSpecId, String authName) { + return getHeaderName(openApiSpecId, authName) != null ? getHeaderName(openApiSpecId, authName) + : HttpHeaders.AUTHORIZATION; } public String getHeaderName() { @@ -80,6 +85,13 @@ public String getHeaderName() { .getOptionalValue(getCanonicalAuthConfigPropertyName(AuthConfig.HEADER_NAME), String.class).orElse(null); } + public static String getHeaderName(String openApiSpecId, String authName) { + return ConfigProvider.getConfig() + .getOptionalValue(getCanonicalAuthConfigPropertyName(AuthConfig.HEADER_NAME, openApiSpecId, authName), + String.class) + .orElse(null); + } + @Override public List operationsToFilter() { return applyToOperations; @@ -93,6 +105,17 @@ public static String getCanonicalAuthConfigPropertyName(String authPropertyName, return String.format(CANONICAL_AUTH_CONFIG_PROPERTY_NAME, openApiSpecId, authName, authPropertyName); } + public static boolean isTokenPropagation(String openApiSpecId, String authName) { + return ConfigProvider.getConfig() + .getOptionalValue(getCanonicalAuthConfigPropertyName(AuthConfig.TOKEN_PROPAGATION, openApiSpecId, authName), + Boolean.class) + .orElse(false); + } + + public CredentialsProvider getCredentialsProvider() { + return credentialsProvider; + } + protected void addAuthorizationHeader(MultivaluedMap headers, String value) { headers.put(HttpHeaders.AUTHORIZATION, Collections.singletonList(value)); } diff --git a/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/AbstractAuthenticationPropagationHeadersFactory.java b/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/AbstractAuthenticationPropagationHeadersFactory.java index 2f9c1a04e..069c7cffb 100644 --- a/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/AbstractAuthenticationPropagationHeadersFactory.java +++ b/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/AbstractAuthenticationPropagationHeadersFactory.java @@ -7,6 +7,8 @@ import jakarta.ws.rs.core.MultivaluedMap; import org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import io.quarkiverse.openapi.generator.OpenApiGeneratorConfig; @@ -23,6 +25,8 @@ public abstract class AbstractAuthenticationPropagationHeadersFactory implements protected OpenApiGeneratorConfig generatorConfig; protected HeadersProvider headersProvider; + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractAuthenticationPropagationHeadersFactory.class); + protected AbstractAuthenticationPropagationHeadersFactory(BaseCompositeAuthenticationProvider compositeProvider, OpenApiGeneratorConfig generatorConfig, HeadersProvider headersProvider) { @@ -36,6 +40,11 @@ public MultivaluedMap update(MultivaluedMap inco MultivaluedMap clientOutgoingHeaders) { MultivaluedMap propagatedHeaders = new MultivaluedHashMap<>(); MultivaluedMap providedHeaders = headersProvider.getStringHeaders(generatorConfig); + + LOGGER.debug("Incoming headers keys{}", incomingHeaders.keySet()); + LOGGER.debug("Outgoing headers keys{}", clientOutgoingHeaders.keySet()); + LOGGER.debug("Provided headers keys{}", providedHeaders.keySet()); + compositeProvider.getAuthenticationProviders().stream() .filter(AbstractAuthProvider.class::isInstance) .map(AbstractAuthProvider.class::cast) @@ -46,9 +55,14 @@ public MultivaluedMap update(MultivaluedMap inco // get priority to the headers coming from the headers provider. List headerValue = providedHeaders.get(headerName); if (headerValue == null) { - // lastly look into the incoming headers. + // next, look into the incoming headers. headerValue = incomingHeaders.get(headerName); } + if (headerValue == null) { + // lastly look into the outgoing headers. + headerValue = clientOutgoingHeaders.get(headerName); + } + if (headerValue != null) { propagatedHeaders.put(propagationHeaderName(authProvider.getOpenApiSpecId(), authProvider.getName(), diff --git a/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/ApiKeyAuthenticationProvider.java b/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/ApiKeyAuthenticationProvider.java index 2f3db8393..d3833089f 100644 --- a/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/ApiKeyAuthenticationProvider.java +++ b/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/ApiKeyAuthenticationProvider.java @@ -11,6 +11,8 @@ import jakarta.ws.rs.core.UriBuilder; import org.eclipse.microprofile.config.ConfigProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import io.quarkiverse.openapi.generator.OpenApiGeneratorException; @@ -23,6 +25,8 @@ public class ApiKeyAuthenticationProvider extends AbstractAuthProvider { private final ApiKeyIn apiKeyIn; private final String apiKeyName; + private static final Logger LOGGER = LoggerFactory.getLogger(ApiKeyAuthenticationProvider.class); + public ApiKeyAuthenticationProvider(final String openApiSpecId, final String name, final ApiKeyIn apiKeyIn, final String apiKeyName, List operations, CredentialsProvider credentialsProvider) { super(name, openApiSpecId, operations, credentialsProvider); @@ -31,11 +35,6 @@ public ApiKeyAuthenticationProvider(final String openApiSpecId, final String nam validateConfig(); } - public ApiKeyAuthenticationProvider(final String openApiSpecId, final String name, final ApiKeyIn apiKeyIn, - final String apiKeyName, List operations) { - this(openApiSpecId, name, apiKeyIn, apiKeyName, operations, new ConfigCredentialsProvider()); - } - @Override public void filter(ClientRequestContext requestContext) throws IOException { switch (apiKeyIn) { @@ -59,7 +58,20 @@ && isUseAuthorizationHeaderValue()) { } private String getApiKey(ClientRequestContext requestContext) { - return credentialsProvider.getApiKey(requestContext, getOpenApiSpecId(), getName()); + final String key = credentialsProvider.getApiKey(CredentialsContext.builder() + .requestContext(requestContext) + .openApiSpecId(getOpenApiSpecId()) + .authName(getName()) + .build()).orElse(""); + + if (key.isEmpty()) { + LOGGER.warn("configured {} property (see application.properties) is empty. hint: configure it.", + AbstractAuthProvider.getCanonicalAuthConfigPropertyName(ConfigCredentialsProvider.API_KEY, + getOpenApiSpecId(), + getName())); + } + + return key; } private boolean isUseAuthorizationHeaderValue() { diff --git a/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/BasicAuthenticationProvider.java b/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/BasicAuthenticationProvider.java index 343b2e18d..f39622bff 100644 --- a/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/BasicAuthenticationProvider.java +++ b/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/BasicAuthenticationProvider.java @@ -24,16 +24,20 @@ public BasicAuthenticationProvider(final String openApiSpecId, String name, List super(name, openApiSpecId, operations, credentialsProvider); } - public BasicAuthenticationProvider(final String openApiSpecId, String name, List operations) { - this(openApiSpecId, name, operations, new ConfigCredentialsProvider()); - } - private String getUsername(ClientRequestContext requestContext) { - return credentialsProvider.getBasicUsername(requestContext, getOpenApiSpecId(), getName()); + return credentialsProvider.getBasicUsername(CredentialsContext.builder() + .requestContext(requestContext) + .openApiSpecId(getOpenApiSpecId()) + .authName(getName()) + .build()).orElse(""); } private String getPassword(ClientRequestContext requestContext) { - return credentialsProvider.getBasicPassword(requestContext, getOpenApiSpecId(), getName()); + return credentialsProvider.getBasicPassword(CredentialsContext.builder() + .requestContext(requestContext) + .openApiSpecId(getOpenApiSpecId()) + .authName(getName()) + .build()).orElse(""); } @Override @@ -53,7 +57,7 @@ public void filter(ClientRequestContext requestContext) throws IOException { " You must verify that the properties: {} and {} are properly configured, or the request header: {} is set when the token propagation is enabled.", getName(), getCanonicalAuthConfigPropertyName(USER_NAME, getOpenApiSpecId(), getName()), getCanonicalAuthConfigPropertyName(PASSWORD, getOpenApiSpecId(), getName()), - getHeaderForPropagation()); + getHeaderForPropagation(getOpenApiSpecId(), getName())); } } } diff --git a/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/BearerAuthenticationProvider.java b/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/BearerAuthenticationProvider.java index d5dae2a74..91742cc16 100644 --- a/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/BearerAuthenticationProvider.java +++ b/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/BearerAuthenticationProvider.java @@ -17,9 +17,8 @@ */ public class BearerAuthenticationProvider extends AbstractAuthProvider { - private static final Logger LOGGER = LoggerFactory.getLogger(BearerAuthenticationProvider.class); - private final String scheme; + private static final Logger LOGGER = LoggerFactory.getLogger(BearerAuthenticationProvider.class); public BearerAuthenticationProvider(final String openApiSpecId, final String name, final String scheme, List operations, CredentialsProvider credentialsProvider) { @@ -27,13 +26,9 @@ public BearerAuthenticationProvider(final String openApiSpecId, final String nam this.scheme = scheme; } - public BearerAuthenticationProvider(final String openApiSpecId, final String name, final String scheme, - List operations) { - this(openApiSpecId, name, scheme, operations, new ConfigCredentialsProvider()); - } - @Override public void filter(ClientRequestContext requestContext) throws IOException { + LOGGER.debug("Headers keys set in incoming requestContext: {}", requestContext.getHeaders().keySet()); String bearerToken = getBearerToken(requestContext); if (isTokenPropagation()) { @@ -46,11 +41,16 @@ public void filter(ClientRequestContext requestContext) throws IOException { LOGGER.debug("No bearer token was found for the security scheme: {}." + " You must verify that the property: {} is properly configured, or the request header: {} is set when the token propagation is enabled.", getName(), getCanonicalAuthConfigPropertyName(BEARER_TOKEN, getOpenApiSpecId(), getName()), - getHeaderForPropagation()); + getHeaderForPropagation(getOpenApiSpecId(), getName())); } + LOGGER.debug("Header keys set in filtered requestContext: {}", requestContext.getHeaders().keySet()); } private String getBearerToken(ClientRequestContext requestContext) { - return credentialsProvider.getBearerToken(requestContext, getOpenApiSpecId(), getName()); + return credentialsProvider.getBearerToken(CredentialsContext.builder() + .requestContext(requestContext) + .openApiSpecId(getOpenApiSpecId()) + .authName(getName()) + .build()).orElse(""); } } diff --git a/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/ConfigCredentialsProvider.java b/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/ConfigCredentialsProvider.java index 8f78ee1e8..294017c97 100644 --- a/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/ConfigCredentialsProvider.java +++ b/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/ConfigCredentialsProvider.java @@ -1,13 +1,12 @@ package io.quarkiverse.openapi.generator.providers; +import java.util.Optional; + import jakarta.annotation.Priority; import jakarta.enterprise.context.Dependent; import jakarta.enterprise.inject.Alternative; -import jakarta.ws.rs.client.ClientRequestContext; import org.eclipse.microprofile.config.ConfigProvider; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; @Dependent @Alternative @@ -19,48 +18,49 @@ public class ConfigCredentialsProvider implements CredentialsProvider { static final String BEARER_TOKEN = "bearer-token"; static final String API_KEY = "api-key"; - private static final Logger LOGGER = LoggerFactory.getLogger(ConfigCredentialsProvider.class); - - public ConfigCredentialsProvider() { - - } - @Override - public String getApiKey(ClientRequestContext requestContext, String openApiSpecId, String authName) { - final String key = ConfigProvider.getConfig() - .getOptionalValue(AbstractAuthProvider.getCanonicalAuthConfigPropertyName(API_KEY, openApiSpecId, authName), - String.class) - .orElse(""); - if (key.isEmpty()) { - LOGGER.warn("configured {} property (see application.properties) is empty. hint: configure it.", - AbstractAuthProvider.getCanonicalAuthConfigPropertyName(API_KEY, openApiSpecId, authName)); - } - return key; + public Optional getApiKey(CredentialsContext input) { + return ConfigProvider.getConfig() + .getOptionalValue( + AbstractAuthProvider.getCanonicalAuthConfigPropertyName(API_KEY, getConfigKey(input), + input.getAuthName()), + String.class); + } @Override - public String getBasicUsername(ClientRequestContext requestContext, String openApiSpecId, String authName) { + public Optional getBasicUsername(CredentialsContext input) { return ConfigProvider.getConfig() - .getOptionalValue(AbstractAuthProvider.getCanonicalAuthConfigPropertyName(USER_NAME, openApiSpecId, authName), - String.class) - .orElse(""); + .getOptionalValue( + AbstractAuthProvider.getCanonicalAuthConfigPropertyName(USER_NAME, getConfigKey(input), + input.getAuthName()), + String.class); } @Override - public String getBasicPassword(ClientRequestContext requestContext, String openApiSpecId, String authName) { + public Optional getBasicPassword(CredentialsContext input) { return ConfigProvider.getConfig() - .getOptionalValue(AbstractAuthProvider.getCanonicalAuthConfigPropertyName(PASSWORD, openApiSpecId, authName), - String.class) - .orElse(""); + .getOptionalValue( + AbstractAuthProvider.getCanonicalAuthConfigPropertyName(PASSWORD, getConfigKey(input), + input.getAuthName()), + String.class); } @Override - public String getBearerToken(ClientRequestContext requestContext, String openApiSpecId, String authName) { + public Optional getBearerToken(CredentialsContext input) { return ConfigProvider.getConfig() .getOptionalValue( - AbstractAuthProvider.getCanonicalAuthConfigPropertyName(BEARER_TOKEN, openApiSpecId, authName), - String.class) - .orElse(""); + AbstractAuthProvider.getCanonicalAuthConfigPropertyName(BEARER_TOKEN, getConfigKey(input), + input.getAuthName()), + String.class); } + protected String getConfigKey(CredentialsContext input) { + return input.getOpenApiSpecId(); + } + + @Override + public Optional getOauth2BearerToken(CredentialsContext input) { + return Optional.empty(); + } } diff --git a/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/CredentialsContext.java b/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/CredentialsContext.java new file mode 100644 index 000000000..3c47b0923 --- /dev/null +++ b/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/CredentialsContext.java @@ -0,0 +1,59 @@ +package io.quarkiverse.openapi.generator.providers; + +import jakarta.ws.rs.client.ClientRequestContext; + +public class CredentialsContext { + // requestContext The current request context in which set the authorization header token + private ClientRequestContext requestContext; + // openApiSpecId the OpenAPI Spec identification as defined by the OpenAPI Extension + private String openApiSpecId; + // authName The security schema for this Bearer Token definition + private String authName; + + public CredentialsContext(ClientRequestContext requestContext, String openApiSpecId, String authName) { + this.requestContext = requestContext; + this.openApiSpecId = openApiSpecId; + this.authName = authName; + } + + public ClientRequestContext getRequestContext() { + return requestContext; + } + + public String getOpenApiSpecId() { + return openApiSpecId; + } + + public String getAuthName() { + return authName; + } + + public static CredentialsContextBuilder builder() { + return new CredentialsContextBuilder(); + } + + public static class CredentialsContextBuilder { + private ClientRequestContext requestContext; + private String openApiSpecId; + private String authName; + + public CredentialsContextBuilder requestContext(ClientRequestContext requestContext) { + this.requestContext = requestContext; + return this; + } + + public CredentialsContextBuilder openApiSpecId(String openApiSpecId) { + this.openApiSpecId = openApiSpecId; + return this; + } + + public CredentialsContextBuilder authName(String authName) { + this.authName = authName; + return this; + } + + public CredentialsContext build() { + return new CredentialsContext(requestContext, openApiSpecId, authName); + } + } +} diff --git a/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/CredentialsProvider.java b/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/CredentialsProvider.java index 3f15dd67c..6d6f13a10 100644 --- a/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/CredentialsProvider.java +++ b/client/runtime/src/main/java/io/quarkiverse/openapi/generator/providers/CredentialsProvider.java @@ -1,6 +1,6 @@ package io.quarkiverse.openapi.generator.providers; -import jakarta.ws.rs.client.ClientRequestContext; +import java.util.Optional; /** * Provider for security credentials. Clients can implement this interface to control how to provide security credentials in @@ -12,36 +12,40 @@ public interface CredentialsProvider { /** * Gets the API Key given the OpenAPI definition and security schema * - * @param openApiSpecId the OpenAPI Spec identification as defined by the OpenAPI Extension - * @param authName The security schema for this API Key definition + * @param input the input data available to the method * @return the API Key to use when filtering the request */ - String getApiKey(ClientRequestContext requestContext, String openApiSpecId, String authName); + Optional getApiKey(CredentialsContext input); /** * Gets the username given the OpenAPI definition and security schema * - * @param openApiSpecId the OpenAPI Spec identification as defined by the OpenAPI Extension - * @param authName The security schema for this Basic Authorization definition + * @param input the input data available to the method * @return the username to use when filtering the request */ - String getBasicUsername(ClientRequestContext requestContext, String openApiSpecId, String authName); + Optional getBasicUsername(CredentialsContext input); /** * Gets the password given the OpenAPI definition and security schema * - * @param openApiSpecId the OpenAPI Spec identification as defined by the OpenAPI Extension - * @param authName The security schema for this Basic Authorization definition + * @param input the input data available to the method * @return the password to use when filtering the request */ - String getBasicPassword(ClientRequestContext requestContext, String openApiSpecId, String authName); + Optional getBasicPassword(CredentialsContext input); /** * Gets the Bearer Token given the OpenAPI definition and security schema * - * @param openApiSpecId the OpenAPI Spec identification as defined by the OpenAPI Extension - * @param authName The security schema for this Bearer Token definition + * @param input the input data available to the method * @return the Bearer Token to use when filtering the request */ - String getBearerToken(ClientRequestContext requestContext, String openApiSpecId, String authName); + Optional getBearerToken(CredentialsContext input); + + /** + * Gets the OAuth2 Bearer Token given the OpenAPI definition and security schema + * + * @param input the input data available to the method + * @return the Bearer Token to use when filtering the request + */ + Optional getOauth2BearerToken(CredentialsContext input); } diff --git a/client/runtime/src/test/java/io/quarkiverse/openapi/generator/providers/ApiKeyOpenApiSpecProviderTest.java b/client/runtime/src/test/java/io/quarkiverse/openapi/generator/providers/ApiKeyOpenApiSpecProviderTest.java index d6ffd6fab..d8060fb4f 100644 --- a/client/runtime/src/test/java/io/quarkiverse/openapi/generator/providers/ApiKeyOpenApiSpecProviderTest.java +++ b/client/runtime/src/test/java/io/quarkiverse/openapi/generator/providers/ApiKeyOpenApiSpecProviderTest.java @@ -42,7 +42,7 @@ class ApiKeyOpenApiSpecProviderTest extends AbstractOpenApiSpecProviderTest headers = new MultivaluedTreeMap<>(); doReturn(headers).when(requestContext).getHeaders(); provider = new ApiKeyAuthenticationProvider(OPEN_API_FILE_SPEC_ID, AUTH_SCHEME_NAME, ApiKeyIn.cookie, API_KEY_NAME, - List.of()); + List.of(), new ConfigCredentialsProvider()); provider.filter(requestContext); final List cookies = headers.get(HttpHeaders.COOKIE); assertThat(cookies).singleElement().satisfies(cookie -> assertCookie(cookie, API_KEY_NAME, API_KEY_VALUE)); @@ -114,7 +114,7 @@ void filterCookieCaseExisting() throws IOException { headers.add(HttpHeaders.COOKIE, existingCookie); doReturn(headers).when(requestContext).getHeaders(); provider = new ApiKeyAuthenticationProvider(OPEN_API_FILE_SPEC_ID, AUTH_SCHEME_NAME, ApiKeyIn.cookie, API_KEY_NAME, - List.of()); + List.of(), new ConfigCredentialsProvider()); provider.filter(requestContext); final List cookies = headers.get(HttpHeaders.COOKIE); assertThat(cookies).satisfiesExactlyInAnyOrder(cookie -> assertCookie(cookie, existingCookieName, existingCookieValue), @@ -130,7 +130,8 @@ void tokenPropagationNotSupported() { Boolean.class)).thenReturn(Optional.of(true)); assertThatThrownBy(() -> new ApiKeyAuthenticationProvider(OPEN_API_FILE_SPEC_ID, AUTH_SCHEME_NAME, ApiKeyIn.header, - API_KEY_NAME, List.of())).hasMessageContaining("quarkus.openapi-generator.%s.auth.%s.token-propagation", + API_KEY_NAME, List.of(), new ConfigCredentialsProvider())) + .hasMessageContaining("quarkus.openapi-generator.%s.auth.%s.token-propagation", OPEN_API_FILE_SPEC_ID, AUTH_SCHEME_NAME); } } diff --git a/client/runtime/src/test/java/io/quarkiverse/openapi/generator/providers/BasicOpenApiSpecProviderTest.java b/client/runtime/src/test/java/io/quarkiverse/openapi/generator/providers/BasicOpenApiSpecProviderTest.java index 20a26d577..2677a7786 100644 --- a/client/runtime/src/test/java/io/quarkiverse/openapi/generator/providers/BasicOpenApiSpecProviderTest.java +++ b/client/runtime/src/test/java/io/quarkiverse/openapi/generator/providers/BasicOpenApiSpecProviderTest.java @@ -39,7 +39,8 @@ class BasicOpenApiSpecProviderTest extends AbstractOpenApiSpecProviderTest filterWithPropagationTestValues() { @Override protected BearerAuthenticationProvider createProvider() { return new BearerAuthenticationProvider(OPEN_API_FILE_SPEC_ID, AUTH_SCHEME_NAME, null, - List.of()); + List.of(), new ConfigCredentialsProvider()); } @Test @@ -64,7 +64,7 @@ void filterCustomSchemaCase() throws IOException { private void filter(String bearerScheme, String expectedAuthorizationHeader) throws IOException { provider = new BearerAuthenticationProvider(OPEN_API_FILE_SPEC_ID, AUTH_SCHEME_NAME, bearerScheme, - List.of()); + List.of(), new ConfigCredentialsProvider()); provider.filter(requestContext); assertHeader(headers, HttpHeaders.AUTHORIZATION, expectedAuthorizationHeader); } diff --git a/docs/modules/ROOT/pages/includes/custom-auth-provider.adoc b/docs/modules/ROOT/pages/includes/custom-auth-provider.adoc index 6d2c5b5a2..b5984c931 100644 --- a/docs/modules/ROOT/pages/includes/custom-auth-provider.adoc +++ b/docs/modules/ROOT/pages/includes/custom-auth-provider.adoc @@ -27,7 +27,7 @@ import io.quarkiverse.openapi.generator.providers.CredentialsProvider; @RequestScoped @Alternative -@Priority(10) // A higher priority than the default provider. +@Priority(200) // A higher priority than the default provider. public class RuntimeCredentialsProvider implements CredentialsProvider { @Override