From bbd3c5f04863f787ed49a3ddd8e5c92e1487084f Mon Sep 17 00:00:00 2001 From: gabriel-farache Date: Wed, 30 Apr 2025 09:38:24 +0200 Subject: [PATCH 1/2] Use CredentialProvider in OAUTH2 provider and update data structure for CrendentialProvider Signed-off-by: gabriel-farache --- .../deployment/GeneratorProcessor.java | 1 + .../integration-tests/auth-provider/pom.xml | 124 ++++++++++++++++ .../it/auth/TokenServerResource.java | 50 +++++++ .../provider/CustomCredentialsProvider.java | 27 ++++ .../main/openapi/token-external-service1.yaml | 19 +++ .../main/openapi/token-external-service2.yaml | 23 +++ .../main/openapi/token-external-service3.yaml | 19 +++ .../main/openapi/token-external-service5.yaml | 23 +++ .../src/main/resources/application.properties | 42 ++++++ .../it/auth/KeycloakServiceMock.java | 82 +++++++++++ .../it/auth/TokenExternalServicesMock.java | 72 ++++++++++ ...TokenWithCustomCredentialProviderTest.java | 35 +++++ .../it/creds/CustomCredentialsProvider.java | 3 +- client/integration-tests/pom.xml | 1 + .../it/security/KeycloakServiceMock.java | 37 ++--- client/oidc/pom.xml | 25 ++++ ...lassicOidcClientRequestFilterDelegate.java | 3 +- .../OAuth2AuthenticationProvider.java | 27 +++- .../OAuth2AuthenticationProviderTest.java | 124 ++++++++++++++++ ...ctiveOAuth2AuthenticationProviderTest.java | 136 ++++++++++++++++++ .../providers/AbstractAuthProvider.java | 34 ++++- .../ApiKeyAuthenticationProvider.java | 6 +- .../BasicAuthenticationProvider.java | 12 +- .../BearerAuthenticationProvider.java | 6 +- .../providers/ConfigCredentialsProvider.java | 32 +++-- .../providers/CredentialsProvider.java | 84 +++++++++-- 26 files changed, 990 insertions(+), 57 deletions(-) create mode 100644 client/integration-tests/auth-provider/pom.xml create mode 100644 client/integration-tests/auth-provider/src/main/java/io/quarkiverse/openapi/generator/it/auth/TokenServerResource.java create mode 100644 client/integration-tests/auth-provider/src/main/java/io/quarkiverse/openapi/generator/it/auth/provider/CustomCredentialsProvider.java create mode 100644 client/integration-tests/auth-provider/src/main/openapi/token-external-service1.yaml create mode 100644 client/integration-tests/auth-provider/src/main/openapi/token-external-service2.yaml create mode 100644 client/integration-tests/auth-provider/src/main/openapi/token-external-service3.yaml create mode 100644 client/integration-tests/auth-provider/src/main/openapi/token-external-service5.yaml create mode 100644 client/integration-tests/auth-provider/src/main/resources/application.properties create mode 100644 client/integration-tests/auth-provider/src/test/java/io/quarkiverse/openapi/generator/it/auth/KeycloakServiceMock.java create mode 100644 client/integration-tests/auth-provider/src/test/java/io/quarkiverse/openapi/generator/it/auth/TokenExternalServicesMock.java create mode 100644 client/integration-tests/auth-provider/src/test/java/io/quarkiverse/openapi/generator/it/auth/TokenWithCustomCredentialProviderTest.java create mode 100644 client/oidc/src/test/java/io/quarkiverse/openapi/generator/oidc/OAuth2AuthenticationProviderTest.java create mode 100644 client/oidc/src/test/java/io/quarkiverse/openapi/generator/oidc/ReactiveOAuth2AuthenticationProviderTest.java 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..8a0bddcc2 --- /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-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..e2bc1b8af --- /dev/null +++ b/client/integration-tests/auth-provider/src/main/java/io/quarkiverse/openapi/generator/it/auth/provider/CustomCredentialsProvider.java @@ -0,0 +1,27 @@ +package io.quarkiverse.openapi.generator.it.auth.provider; + +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; + +@Dependent +@Alternative +@Specializes +@Priority(200) +public class CustomCredentialsProvider extends ConfigCredentialsProvider { + public CustomCredentialsProvider() { + } + + @Override + public String getBearerToken(CredentialsContext input) { + return super.getBearerToken(input) + "_TEST"; + } + + @Override + public String getOauth2BearerToken(CredentialsContext input) { + return super.getOauth2BearerToken(input) + "_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..8bbc39b68 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 @@ -3,7 +3,6 @@ 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; @@ -19,7 +18,7 @@ public class CustomCredentialsProvider extends ConfigCredentialsProvider { public static String TOKEN = "FIXED_TEST_TOKEN"; @Override - public String getBearerToken(ClientRequestContext requestContext, String openApiSpecId, String authName) { + public String getBearerToken(CredentialsContext input) { LOGGER.info("========> getBearerToken from CustomCredentialsProvider"); return TOKEN; } diff --git a/client/integration-tests/pom.xml b/client/integration-tests/pom.xml index fa71bf7f8..2a5f0cd0e 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/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 0dfc506b4..c68eaf11c 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 c2dda5c80..42efbbf2a 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 @@ -12,9 +12,10 @@ import org.slf4j.LoggerFactory; import io.quarkiverse.openapi.generator.providers.AbstractAuthProvider; +import io.quarkiverse.openapi.generator.providers.AuthUtils; +import io.quarkiverse.openapi.generator.providers.ConfigCredentialsProvider; 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 { @@ -30,14 +31,30 @@ public OAuth2AuthenticationProvider(String name, validateConfig(); } + public OAuth2AuthenticationProvider(String name, + String openApiSpecId, OidcClientRequestFilterDelegate delegate, List operations) { + this(name, openApiSpecId, delegate, operations, new ConfigCredentialsProvider()); + } + @Override public void filter(ClientRequestContext requestContext) throws IOException { - if (isTokenPropagation()) { - String bearerToken = getTokenForPropagation(requestContext.getHeaders()); - bearerToken = sanitizeBearerToken(bearerToken); - requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, OidcConstants.BEARER_SCHEME + " " + bearerToken); + String bearerToken; + + if (this.isTokenPropagation()) { + bearerToken = this.getTokenForPropagation(requestContext.getHeaders()); } else { delegate.filter(requestContext); + bearerToken = this.getCredentialsProvider().getOauth2BearerToken(CredentialsProvider.CredentialsContext.builder() + .requestContext(requestContext) + .openApiSpecId(getOpenApiSpecId()) + .authName(getName()) + .build()); + } + + if (bearerToken != null && !bearerToken.isBlank()) { + requestContext.getHeaders().remove(HttpHeaders.AUTHORIZATION); + requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, + AuthUtils.authTokenOrBearer("Bearer", AbstractAuthProvider.sanitizeBearerToken(bearerToken))); } } 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..8eaa1e2dc --- /dev/null +++ b/client/oidc/src/test/java/io/quarkiverse/openapi/generator/oidc/OAuth2AuthenticationProviderTest.java @@ -0,0 +1,124 @@ +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.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()); + } + + 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..68169a3d8 --- /dev/null +++ b/client/oidc/src/test/java/io/quarkiverse/openapi/generator/oidc/ReactiveOAuth2AuthenticationProviderTest.java @@ -0,0 +1,136 @@ +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.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()); + } + + 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 d5a776fc1..d38a0d6fb 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 @@ -60,22 +60,33 @@ 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 headerName = getHeaderName() != null ? getHeaderName() : HttpHeaders.AUTHORIZATION; - String propagatedHeaderName = propagationHeaderName(getOpenApiSpecId(), getName(), headerName); + public static String getTokenForPropagation(MultivaluedMap httpHeaders, String openApiSpecId, + String authName) { + String headerName = getHeaderName(openApiSpecId, authName) != null ? getHeaderName(openApiSpecId, authName) + : HttpHeaders.AUTHORIZATION; + String propagatedHeaderName = propagationHeaderName(openApiSpecId, authName, headerName); return Objects.toString(httpHeaders.getFirst(propagatedHeaderName)); } + public String getTokenForPropagation(MultivaluedMap httpHeaders) { + return getTokenForPropagation(httpHeaders, getOpenApiSpecId(), getName()); + } + public String getHeaderName() { return ConfigProvider.getConfig() .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; @@ -88,4 +99,15 @@ public final String getCanonicalAuthConfigPropertyName(String authPropertyName) public static String getCanonicalAuthConfigPropertyName(String authPropertyName, String openApiSpecId, String authName) { 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; + } } 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..f2467eb14 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 @@ -59,7 +59,11 @@ && isUseAuthorizationHeaderValue()) { } private String getApiKey(ClientRequestContext requestContext) { - return credentialsProvider.getApiKey(requestContext, getOpenApiSpecId(), getName()); + return credentialsProvider.getApiKey(CredentialsProvider.CredentialsContext.builder() + .requestContext(requestContext) + .openApiSpecId(getOpenApiSpecId()) + .authName(getName()) + .build()); } 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 10011a9b3..0ec39ed28 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 @@ -27,11 +27,19 @@ public BasicAuthenticationProvider(final String openApiSpecId, String name, List } private String getUsername(ClientRequestContext requestContext) { - return credentialsProvider.getBasicUsername(requestContext, getOpenApiSpecId(), getName()); + return credentialsProvider.getBasicUsername(CredentialsProvider.CredentialsContext.builder() + .requestContext(requestContext) + .openApiSpecId(getOpenApiSpecId()) + .authName(getName()) + .build()); } private String getPassword(ClientRequestContext requestContext) { - return credentialsProvider.getBasicPassword(requestContext, getOpenApiSpecId(), getName()); + return credentialsProvider.getBasicPassword(CredentialsProvider.CredentialsContext.builder() + .requestContext(requestContext) + .openApiSpecId(getOpenApiSpecId()) + .authName(getName()) + .build()); } @Override 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 5454b1808..b51bf17b5 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 @@ -40,6 +40,10 @@ public void filter(ClientRequestContext requestContext) throws IOException { } private String getBearerToken(ClientRequestContext requestContext) { - return credentialsProvider.getBearerToken(requestContext, getOpenApiSpecId(), getName()); + return credentialsProvider.getBearerToken(CredentialsProvider.CredentialsContext.builder() + .requestContext(requestContext) + .openApiSpecId(getOpenApiSpecId()) + .authName(getName()) + .build()); } } 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..00ee1e142 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 @@ -3,7 +3,7 @@ import jakarta.annotation.Priority; import jakarta.enterprise.context.Dependent; import jakarta.enterprise.inject.Alternative; -import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.core.HttpHeaders; import org.eclipse.microprofile.config.ConfigProvider; import org.slf4j.Logger; @@ -26,41 +26,53 @@ public ConfigCredentialsProvider() { } @Override - public String getApiKey(ClientRequestContext requestContext, String openApiSpecId, String authName) { + public String getApiKey(CredentialsContext input) { final String key = ConfigProvider.getConfig() - .getOptionalValue(AbstractAuthProvider.getCanonicalAuthConfigPropertyName(API_KEY, openApiSpecId, authName), + .getOptionalValue( + AbstractAuthProvider.getCanonicalAuthConfigPropertyName(API_KEY, input.getOpenApiSpecId(), + input.getAuthName()), String.class) .orElse(""); if (key.isEmpty()) { LOGGER.warn("configured {} property (see application.properties) is empty. hint: configure it.", - AbstractAuthProvider.getCanonicalAuthConfigPropertyName(API_KEY, openApiSpecId, authName)); + AbstractAuthProvider.getCanonicalAuthConfigPropertyName(API_KEY, input.getOpenApiSpecId(), + input.getAuthName())); } return key; } @Override - public String getBasicUsername(ClientRequestContext requestContext, String openApiSpecId, String authName) { + public String getBasicUsername(CredentialsContext input) { return ConfigProvider.getConfig() - .getOptionalValue(AbstractAuthProvider.getCanonicalAuthConfigPropertyName(USER_NAME, openApiSpecId, authName), + .getOptionalValue( + AbstractAuthProvider.getCanonicalAuthConfigPropertyName(USER_NAME, input.getOpenApiSpecId(), + input.getAuthName()), String.class) .orElse(""); } @Override - public String getBasicPassword(ClientRequestContext requestContext, String openApiSpecId, String authName) { + public String getBasicPassword(CredentialsContext input) { return ConfigProvider.getConfig() - .getOptionalValue(AbstractAuthProvider.getCanonicalAuthConfigPropertyName(PASSWORD, openApiSpecId, authName), + .getOptionalValue( + AbstractAuthProvider.getCanonicalAuthConfigPropertyName(PASSWORD, input.getOpenApiSpecId(), + input.getAuthName()), String.class) .orElse(""); } @Override - public String getBearerToken(ClientRequestContext requestContext, String openApiSpecId, String authName) { + public String getBearerToken(CredentialsContext input) { return ConfigProvider.getConfig() .getOptionalValue( - AbstractAuthProvider.getCanonicalAuthConfigPropertyName(BEARER_TOKEN, openApiSpecId, authName), + AbstractAuthProvider.getCanonicalAuthConfigPropertyName(BEARER_TOKEN, input.getOpenApiSpecId(), + input.getAuthName()), String.class) .orElse(""); } + @Override + public String getOauth2BearerToken(CredentialsContext input) { + return input.getRequestContext().getHeaderString(HttpHeaders.AUTHORIZATION); + } } 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..02c4fa787 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 @@ -12,36 +12,96 @@ 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); + String 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); + String 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); + String 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); + String 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 + */ + String getOauth2BearerToken(CredentialsContext input); + + 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); + } + } + } } From bbc907fc16f0d13f9d4c584d0311385683b69a94 Mon Sep 17 00:00:00 2001 From: gabriel-farache Date: Fri, 6 Jun 2025 10:51:19 +0200 Subject: [PATCH 2/2] Fix Custom auth provider doc Signed-off-by: gabriel-farache --- docs/modules/ROOT/pages/includes/custom-auth-provider.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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