diff --git a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc index 86c625ca2b5ba..1b06ad4acaecc 100644 --- a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc @@ -419,6 +419,22 @@ public interface ProtectedResourceService { ---- `OidcClientRequestReactiveFilter` uses a default `OidcClient` by default. A named `OidcClient` can be selected with a `quarkus.oidc-client-reactive-filter.client-name` configuration property. +You can also select `OidcClient` by setting `value` attribute of the `@OidcClientFilter` annotation. The client name set via annotation has higher priority than the `quarkus.oidc-client-reactive-filter.client-name` configuration property. + +[source,java] +---- +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import io.quarkus.oidc.client.filter.OidcClientFilter; + +@RegisterRestClient +@OidcClientFilter("my-client") +@Path("/") +public interface ProtectedResourceService { + + @GET + String getUserName(); +} +---- [[oidc-client-filter]] @@ -478,6 +494,22 @@ public interface ProtectedResourceService { Alternatively, `OidcClientRequestFilter` can be registered automatically with all MP Rest or JAX-RS clients if `quarkus.oidc-client-filter.register-filter=true` property is set. `OidcClientRequestFilter` uses a default `OidcClient` by default. A named `OidcClient` can be selected with a `quarkus.oidc-client-filter.client-name` configuration property. +You can also select `OidcClient` by setting `value` attribute of the `@OidcClientFilter` annotation. The client name set via annotation has higher priority than the `quarkus.oidc-client-filter.client-name` configuration property. + +[source,java] +---- +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import io.quarkus.oidc.client.filter.OidcClientFilter; + +@RegisterRestClient +@OidcClientFilter("my-client") +@Path("/") +public interface ProtectedResourceService { + + @GET + String getUserName(); +} +---- === Use Custom RestClient ClientFilter diff --git a/extensions/oidc-client-filter/deployment/src/main/java/io/quarkus/oidc/client/filter/deployment/OidcClientFilterBuildStep.java b/extensions/oidc-client-filter/deployment/src/main/java/io/quarkus/oidc/client/filter/deployment/OidcClientFilterBuildStep.java index 5d1ce05cdbd40..f145757376c8b 100644 --- a/extensions/oidc-client-filter/deployment/src/main/java/io/quarkus/oidc/client/filter/deployment/OidcClientFilterBuildStep.java +++ b/extensions/oidc-client-filter/deployment/src/main/java/io/quarkus/oidc/client/filter/deployment/OidcClientFilterBuildStep.java @@ -1,16 +1,28 @@ package io.quarkus.oidc.client.filter.deployment; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import javax.inject.Singleton; + import org.jboss.jandex.DotName; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.oidc.client.deployment.OidcClientBuildStep.IsEnabled; +import io.quarkus.oidc.client.deployment.OidcClientFilterClientNamesMapBuildItem; import io.quarkus.oidc.client.filter.OidcClientFilter; import io.quarkus.oidc.client.filter.OidcClientRequestFilter; import io.quarkus.oidc.client.filter.runtime.OidcClientFilterConfig; +import io.quarkus.oidc.client.filter.runtime.OidcClientFilterRecorder; +import io.quarkus.oidc.client.runtime.AbstractTokensProducer; import io.quarkus.restclient.deployment.RestClientAnnotationProviderBuildItem; import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem; @@ -35,4 +47,57 @@ void registerProvider(BuildProducer additionalBeans, OidcClientRequestFilter.class)); } } + + @Record(ExecutionTime.STATIC_INIT) + @BuildStep + void setClientInvokerToName(OidcClientFilterRecorder recorder, + Optional clientNamesMap) { + if (clientNamesMap.isPresent()) { + // we collected all client names specified via @OidcClientFilter("clientId") + // and annotated classes as map; + // now we record the map so that OidcClientRequestFilter can match each request with OidcClient + if (config.clientName.isPresent()) { + // don't create TokensProducer for OidcClient configured via property as there is no advantage + // OidcClientRequestFilter will use configured OidcClient by default + final String configClientName = config.clientName.get(); + final Map invokerClassToClientName = new HashMap<>(); + for (Map.Entry entry : clientNamesMap.get().getClientInvokerClassToName().entrySet()) { + final String clientName = entry.getValue(); + // by ignoring config name we ensure missing bean is not looked up + if (!configClientName.equals(clientName)) { + invokerClassToClientName.put(entry.getKey(), clientName); + } + } + if (!invokerClassToClientName.isEmpty()) { + recorder.setClientInvokerToName(invokerClassToClientName); + } + } else { + recorder.setClientInvokerToName(clientNamesMap.get().getClientInvokerClassToName()); + } + } + } + + @Record(ExecutionTime.RUNTIME_INIT) + @BuildStep + void createTokensProducerForNonDefaultOidcClients(OidcClientFilterRecorder recorder, + Optional scanningResult, + BuildProducer beanProducer) { + if (scanningResult.isPresent()) { + // register Tokens producer for each annotation instance like @OidcClientFilter(clientName = "myClientName") + for (String clientName : scanningResult.get().getClientInvokerClassToName().values()) { + if (!clientName.equals(config.clientName.orElse(null))) { + beanProducer.produce(SyntheticBeanBuildItem + .configure(AbstractTokensProducer.class) + .unremovable() + .types(AbstractTokensProducer.class) + .supplier(recorder.createTokensProducer(clientName)) + .scope(Singleton.class) + .unremovable() + .named(OidcClientFilterRecorder.toTokensProducerBeanName(clientName)) + .setRuntimeInit() + .done()); + } + } + } + } } diff --git a/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/ConfigPropertyOidcClientResource.java b/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/ConfigPropertyOidcClientResource.java new file mode 100644 index 0000000000000..4ec506a74dd40 --- /dev/null +++ b/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/ConfigPropertyOidcClientResource.java @@ -0,0 +1,21 @@ +package io.quarkus.oidc.client.filter; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RestClient; + +@Path("/config-property-oidc-client") +public class ConfigPropertyOidcClientResource { + + @Inject + @RestClient + ProtectedResourceServiceConfigPropertyOidcClient protectedResourceServiceConfigPropertyOidcClient; + + @GET + @Path("user-name") + public String userName() { + return protectedResourceServiceConfigPropertyOidcClient.getUserName(); + } +} diff --git a/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/NamedOidcClientFilterDevModeTest.java b/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/NamedOidcClientFilterDevModeTest.java new file mode 100644 index 0000000000000..e4ec94d279172 --- /dev/null +++ b/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/NamedOidcClientFilterDevModeTest.java @@ -0,0 +1,45 @@ +package io.quarkus.oidc.client.filter; + +import static org.hamcrest.Matchers.equalTo; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager; +import io.restassured.RestAssured; + +@QuarkusTestResource(KeycloakTestResourceLifecycleManager.class) +public class NamedOidcClientFilterDevModeTest { + + private static final Class[] testClasses = { + ProtectedResource.class, + ProtectedResourceServiceNamedOidcClient.class, + ProtectedResourceServiceConfigPropertyOidcClient.class, + NamedOidcClientResource.class, + ConfigPropertyOidcClientResource.class + }; + + @RegisterExtension + static final QuarkusDevModeTest test = new QuarkusDevModeTest() + .withApplicationRoot((jar) -> jar + .addClasses(testClasses) + .addAsResource("application-named-oidc-client-filter.properties", "application.properties")); + + @Test + public void testGerUserConfigPropertyAndAnnotation() { + // OidcClient selected via @OidcClient("clientName") + RestAssured.when().get("/named-oidc-client/user-name") + .then() + .statusCode(200) + .body(equalTo("jdoe")); + + // OidcClient selected via `quarkus.oidc-client-filter.client-name=config-property` + RestAssured.when().get("/config-property-oidc-client/user-name") + .then() + .statusCode(200) + .body(equalTo("alice")); + } + +} diff --git a/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/NamedOidcClientResource.java b/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/NamedOidcClientResource.java new file mode 100644 index 0000000000000..5419cddb7598c --- /dev/null +++ b/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/NamedOidcClientResource.java @@ -0,0 +1,21 @@ +package io.quarkus.oidc.client.filter; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RestClient; + +@Path("/named-oidc-client") +public class NamedOidcClientResource { + + @Inject + @RestClient + ProtectedResourceServiceNamedOidcClient protectedResourceServiceNamedOidcClient; + + @GET + @Path("user-name") + public String userName() { + return protectedResourceServiceNamedOidcClient.getUserName(); + } +} diff --git a/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/OidcClientFilterDevModeTest.java b/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/OidcClientFilterDevModeTest.java index 68e1606c09087..97cbbb8d7868c 100644 --- a/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/OidcClientFilterDevModeTest.java +++ b/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/OidcClientFilterDevModeTest.java @@ -26,10 +26,12 @@ @QuarkusTestResource(KeycloakTestResourceLifecycleManager.class) public class OidcClientFilterDevModeTest { - private static Class[] testClasses = { + private static final Class[] testClasses = { FrontendResource.class, ProtectedResource.class, - ProtectedResourceService.class + ProtectedResourceService.class, + ProtectedResourceServiceNamedOidcClient.class, + NamedOidcClientResource.class }; @RegisterExtension @@ -60,6 +62,13 @@ public void testGetUserName() { .statusCode(200) .body(equalTo("alice")); checkLog(); + + // here we test that user can optionally select named OidcClient like this @OidcClient("clientName") + // even though 'quarkus.oidc-client-filter.register-filter' is enabled + RestAssured.when().get("/named-oidc-client/user-name") + .then() + .statusCode(200) + .body(equalTo("jdoe")); } private void checkLog() { diff --git a/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/ProtectedResourceServiceConfigPropertyOidcClient.java b/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/ProtectedResourceServiceConfigPropertyOidcClient.java new file mode 100644 index 0000000000000..929f35e34e3d4 --- /dev/null +++ b/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/ProtectedResourceServiceConfigPropertyOidcClient.java @@ -0,0 +1,15 @@ +package io.quarkus.oidc.client.filter; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +@OidcClientFilter +@RegisterRestClient +@Path("/") +public interface ProtectedResourceServiceConfigPropertyOidcClient { + + @GET + String getUserName(); +} diff --git a/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/ProtectedResourceServiceNamedOidcClient.java b/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/ProtectedResourceServiceNamedOidcClient.java new file mode 100644 index 0000000000000..efb484c0630d8 --- /dev/null +++ b/extensions/oidc-client-filter/deployment/src/test/java/io/quarkus/oidc/client/filter/ProtectedResourceServiceNamedOidcClient.java @@ -0,0 +1,15 @@ +package io.quarkus.oidc.client.filter; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +@OidcClientFilter("named") +@RegisterRestClient +@Path("/") +public interface ProtectedResourceServiceNamedOidcClient { + + @GET + String getUserName(); +} diff --git a/extensions/oidc-client-filter/deployment/src/test/resources/application-named-oidc-client-filter.properties b/extensions/oidc-client-filter/deployment/src/test/resources/application-named-oidc-client-filter.properties new file mode 100644 index 0000000000000..9cb7a6ead4064 --- /dev/null +++ b/extensions/oidc-client-filter/deployment/src/test/resources/application-named-oidc-client-filter.properties @@ -0,0 +1,23 @@ +quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus/ +quarkus.oidc.client-id=quarkus-service-app +quarkus.oidc.credentials.secret=secret + +quarkus.oidc-client-filter.client-name=config-property +quarkus.oidc-client.config-property.auth-server-url=${quarkus.oidc.auth-server-url} +quarkus.oidc-client.config-property.client-id=${quarkus.oidc.client-id} +quarkus.oidc-client.config-property.credentials.client-secret.value=${quarkus.oidc.credentials.secret} +quarkus.oidc-client.config-property.credentials.client-secret.method=POST +quarkus.oidc-client.config-property.grant.type=password +quarkus.oidc-client.config-property.grant-options.password.username=alice +quarkus.oidc-client.config-property.grant-options.password.password=alice + +quarkus.oidc-client.named.auth-server-url=${quarkus.oidc.auth-server-url} +quarkus.oidc-client.named.client-id=${quarkus.oidc.client-id} +quarkus.oidc-client.named.credentials.client-secret.value=${quarkus.oidc.credentials.secret} +quarkus.oidc-client.named.credentials.client-secret.method=POST +quarkus.oidc-client.named.grant.type=password +quarkus.oidc-client.named.grant-options.password.username=jdoe +quarkus.oidc-client.named.grant-options.password.password=jdoe + +io.quarkus.oidc.client.filter.ProtectedResourceServiceNamedOidcClient/mp-rest/url=http://localhost:8080/protected +io.quarkus.oidc.client.filter.ProtectedResourceServiceConfigPropertyOidcClient/mp-rest/url=http://localhost:8080/protected \ No newline at end of file diff --git a/extensions/oidc-client-filter/deployment/src/test/resources/application-oidc-client-filter.properties b/extensions/oidc-client-filter/deployment/src/test/resources/application-oidc-client-filter.properties index 830a0ef048dfd..e633ad9ba461c 100644 --- a/extensions/oidc-client-filter/deployment/src/test/resources/application-oidc-client-filter.properties +++ b/extensions/oidc-client-filter/deployment/src/test/resources/application-oidc-client-filter.properties @@ -10,10 +10,19 @@ quarkus.oidc-client.grant.type=password quarkus.oidc-client.grant-options.password.username=alice quarkus.oidc-client.grant-options.password.password=alice +quarkus.oidc-client.named.auth-server-url=${quarkus.oidc.auth-server-url} +quarkus.oidc-client.named.client-id=${quarkus.oidc.client-id} +quarkus.oidc-client.named.credentials.client-secret.value=${quarkus.oidc.credentials.secret} +quarkus.oidc-client.named.credentials.client-secret.method=POST +quarkus.oidc-client.named.grant.type=password +quarkus.oidc-client.named.grant-options.password.username=jdoe +quarkus.oidc-client.named.grant-options.password.password=jdoe + #quarkus.oidc-client-filter.register-filter=true quarkus.oidc-client.refresh-token-time-skew=5S io.quarkus.oidc.client.filter.ProtectedResourceService/mp-rest/url=http://localhost:8080/protected +io.quarkus.oidc.client.filter.ProtectedResourceServiceNamedOidcClient/mp-rest/url=http://localhost:8080/protected quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientImpl".min-level=TRACE quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientImpl".level=TRACE quarkus.log.file.enable=true diff --git a/extensions/oidc-client-filter/runtime/src/main/java/io/quarkus/oidc/client/filter/OidcClientRequestFilter.java b/extensions/oidc-client-filter/runtime/src/main/java/io/quarkus/oidc/client/filter/OidcClientRequestFilter.java index 5da589248d305..743fac829e683 100644 --- a/extensions/oidc-client-filter/runtime/src/main/java/io/quarkus/oidc/client/filter/OidcClientRequestFilter.java +++ b/extensions/oidc-client-filter/runtime/src/main/java/io/quarkus/oidc/client/filter/OidcClientRequestFilter.java @@ -1,5 +1,7 @@ package io.quarkus.oidc.client.filter; +import static io.quarkus.oidc.client.filter.runtime.OidcClientFilterRecorder.toTokensProducerBeanName; + import java.io.IOException; import java.util.Optional; @@ -14,8 +16,11 @@ import javax.ws.rs.ext.Provider; import org.jboss.logging.Logger; +import org.jboss.resteasy.client.jaxrs.internal.ClientRequestContextImpl; +import io.quarkus.arc.Arc; import io.quarkus.oidc.client.filter.runtime.OidcClientFilterConfig; +import io.quarkus.oidc.client.filter.runtime.OidcClientFilterRecorder; import io.quarkus.oidc.client.runtime.AbstractTokensProducer; import io.quarkus.oidc.client.runtime.DisabledOidcClientException; import io.quarkus.oidc.common.runtime.OidcConstants; @@ -26,13 +31,18 @@ public class OidcClientRequestFilter extends AbstractTokensProducer implements ClientRequestFilter { private static final Logger LOG = Logger.getLogger(OidcClientRequestFilter.class); private static final String BEARER_SCHEME_WITH_SPACE = OidcConstants.BEARER_SCHEME + " "; + private final boolean searchForNamedOidcClients; @Inject OidcClientFilterConfig oidcClientFilterConfig; + public OidcClientRequestFilter() { + this.searchForNamedOidcClients = !OidcClientFilterRecorder.getClientInvokerToName().isEmpty(); + } + @Override public void filter(ClientRequestContext requestContext) throws IOException { try { - final String accessToken = getAccessToken(); + final String accessToken = getAccessToken(requestContext); requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, BEARER_SCHEME_WITH_SPACE + accessToken); } catch (DisabledOidcClientException ex) { requestContext.abortWith(Response.status(500).build()); @@ -42,9 +52,26 @@ public void filter(ClientRequestContext requestContext) throws IOException { } } - private String getAccessToken() { - // It should be reactive when run with Resteasy Reactive - return awaitTokens().getAccessToken(); + private String getAccessToken(ClientRequestContext requestContext) { + return getTokensProducer(requestContext).awaitTokens().getAccessToken(); + } + + private AbstractTokensProducer getTokensProducer(ClientRequestContext requestContext) { + // check if we can link the request with named OidcClient + if (searchForNamedOidcClients && requestContext instanceof ClientRequestContextImpl) { + final var invocation = ((ClientRequestContextImpl) requestContext).getInvocation(); + if (invocation != null && invocation.getClientInvoker().getDeclaring() != null) { + final String clientId = OidcClientFilterRecorder.getClientInvokerToName() + .get(invocation.getClientInvoker().getDeclaring().getName()); + if (clientId != null) { + // request have been invoked for a Rest client annotated with @OidcClient("clientName") + return Arc.container() + . instance(toTokensProducerBeanName(clientId)) + .get(); + } + } + } + return this; } protected Optional clientId() { diff --git a/extensions/oidc-client-filter/runtime/src/main/java/io/quarkus/oidc/client/filter/runtime/OidcClientFilterRecorder.java b/extensions/oidc-client-filter/runtime/src/main/java/io/quarkus/oidc/client/filter/runtime/OidcClientFilterRecorder.java new file mode 100644 index 0000000000000..08262c9de86b8 --- /dev/null +++ b/extensions/oidc-client-filter/runtime/src/main/java/io/quarkus/oidc/client/filter/runtime/OidcClientFilterRecorder.java @@ -0,0 +1,46 @@ +package io.quarkus.oidc.client.filter.runtime; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; + +import io.quarkus.oidc.client.runtime.AbstractTokensProducer; +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class OidcClientFilterRecorder { + + private static volatile Map clientInvokerToName = Map.of(); + + public void setClientInvokerToName(Map clientInvokerToName) { + OidcClientFilterRecorder.clientInvokerToName = Map.copyOf(clientInvokerToName); + } + + public static Map getClientInvokerToName() { + return clientInvokerToName; + } + + public Supplier createTokensProducer(String clientId) { + final Optional clientIdOpt = Optional.ofNullable(clientId); + return new Supplier() { + @Override + public AbstractTokensProducer get() { + final var tokensProducer = new AbstractTokensProducer() { + @Override + protected Optional clientId() { + return clientIdOpt; + } + }; + tokensProducer.init(); + return tokensProducer; + } + }; + } + + public static String toTokensProducerBeanName(String clientId) { + // beans created with OidcClientFilterRecorder#createTokensProducer are going to have this name + return AbstractTokensProducer.class.getName() + "$$" + Objects.requireNonNull(clientId); + } + +} diff --git a/extensions/oidc-client-reactive-filter/deployment/pom.xml b/extensions/oidc-client-reactive-filter/deployment/pom.xml index 171436bc7a5c3..b547995beab70 100644 --- a/extensions/oidc-client-reactive-filter/deployment/pom.xml +++ b/extensions/oidc-client-reactive-filter/deployment/pom.xml @@ -26,6 +26,38 @@ io.quarkus quarkus-rest-client-reactive-deployment + + + io.quarkus + quarkus-junit5-internal + test + + + io.rest-assured + rest-assured + test + + + io.quarkus + quarkus-test-keycloak-server + test + + + junit + junit + + + + + io.quarkus + quarkus-oidc-deployment + test + + + io.quarkus + quarkus-resteasy-reactive-deployment + test + @@ -56,4 +88,28 @@ + + + test-keycloak + + + test-containers + + + + + + maven-surefire-plugin + + false + + ${keycloak.docker.legacy.image} + false + + + + + + + diff --git a/extensions/oidc-client-reactive-filter/deployment/src/main/java/io/quarkus/oidc/client/reactive/filter/deployment/OidcClientReactiveFilterBuildStep.java b/extensions/oidc-client-reactive-filter/deployment/src/main/java/io/quarkus/oidc/client/reactive/filter/deployment/OidcClientReactiveFilterBuildStep.java index 1b8579b71eada..60481bc994add 100644 --- a/extensions/oidc-client-reactive-filter/deployment/src/main/java/io/quarkus/oidc/client/reactive/filter/deployment/OidcClientReactiveFilterBuildStep.java +++ b/extensions/oidc-client-reactive-filter/deployment/src/main/java/io/quarkus/oidc/client/reactive/filter/deployment/OidcClientReactiveFilterBuildStep.java @@ -1,7 +1,14 @@ package io.quarkus.oidc.client.reactive.filter.deployment; +import static io.quarkus.oidc.client.reactive.filter.runtime.TokensProducerRegistry.DEFAULT_TOKENS_PRODUCER; + +import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Map; +import java.util.Optional; + +import javax.inject.Singleton; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationValue; @@ -9,15 +16,22 @@ import org.jboss.jandex.Type; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.AdditionalIndexedClassesBuildItem; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.oidc.client.deployment.OidcClientBuildStep.IsEnabled; +import io.quarkus.oidc.client.deployment.OidcClientFilterClientNamesMapBuildItem; import io.quarkus.oidc.client.filter.OidcClientFilter; import io.quarkus.oidc.client.reactive.filter.OidcClientRequestReactiveFilter; +import io.quarkus.oidc.client.reactive.filter.runtime.OidcClientReactiveFilterConfig; +import io.quarkus.oidc.client.reactive.filter.runtime.OidcClientReactiveFilterRecorder; +import io.quarkus.oidc.client.reactive.filter.runtime.TokensProducer; import io.quarkus.rest.client.reactive.deployment.DotNames; import io.quarkus.rest.client.reactive.deployment.RegisterProviderAnnotationInstanceBuildItem; @@ -50,4 +64,56 @@ void registerProvider(BuildProducer additionalBeans, .produce(new AdditionalIndexedClassesBuildItem(OidcClientRequestReactiveFilter.class.getName())); reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, OidcClientRequestReactiveFilter.class)); } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + List createTokensProducers(Optional scanningResult, + OidcClientReactiveFilterConfig config, + OidcClientReactiveFilterRecorder recorder) { + List result = new ArrayList<>(); + final String configClientName = config.clientName.orElse(null); + + // register Tokens producer for each annotation instance like @OidcClientFilter(clientName = "myClientName") + if (scanningResult.isPresent()) { + boolean additionalProducerCreated = false; + for (Map.Entry invokerClassToClientName : scanningResult.get().getClientInvokerClassToName() + .entrySet()) { + final String invokerClass = invokerClassToClientName.getKey(); + final String clientName = invokerClassToClientName.getValue(); + + if (!clientName.equals(configClientName)) { + result.add(SyntheticBeanBuildItem + .configure(TokensProducer.class) + .unremovable() + .types(TokensProducer.class) + .supplier(recorder.createTokensProducer(clientName, invokerClass)) + .scope(Singleton.class) + .unremovable() + .named(clientName) + .setRuntimeInit() + .done()); + if (!additionalProducerCreated) { + additionalProducerCreated = true; + } + } + } + if (additionalProducerCreated) { + recorder.searchForNonDefaultProducers(); + } + } + + // register default (primary) Tokens producer + result.add(SyntheticBeanBuildItem + .configure(TokensProducer.class) + .unremovable() + .types(TokensProducer.class) + .supplier(recorder.createTokensProducer(configClientName, null)) + .scope(Singleton.class) + .named(DEFAULT_TOKENS_PRODUCER) + .unremovable() + .setRuntimeInit() + .done()); + + return result; + } } diff --git a/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/NamedOidcClientFilterDevModeTest.java b/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/NamedOidcClientFilterDevModeTest.java new file mode 100644 index 0000000000000..bb4a0c3f4cd90 --- /dev/null +++ b/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/NamedOidcClientFilterDevModeTest.java @@ -0,0 +1,46 @@ +package io.quarkus.oidc.client.reactive.filter; + +import static org.hamcrest.Matchers.equalTo; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager; +import io.restassured.RestAssured; + +@QuarkusTestResource(KeycloakTestResourceLifecycleManager.class) +public class NamedOidcClientFilterDevModeTest { + + private static final Class[] testClasses = { + ProtectedResource.class, + ProtectedResourceServiceAnnotationOidcClient.class, + ProtectedResourceServiceConfigPropertyOidcClient.class, + OidcClientResource.class + }; + + @RegisterExtension + static final QuarkusDevModeTest test = new QuarkusDevModeTest() + .withApplicationRoot((jar) -> jar + .addClasses(testClasses) + .addAsResource("application-oidc-client-reactive-filter.properties", "application.properties")); + + @Test + public void testGerUserConfigPropertyAndAnnotation() { + // test OidcClientFilter with OidcClient selected via annotation or config-property + + // OidcClient selected via @OidcClient("clientName") + RestAssured.when().get("/oidc-client/annotation/user-name") + .then() + .statusCode(200) + .body(equalTo("jdoe")); + + // OidcClient selected via `quarkus.oidc-client-filter.client-name=config-property` + RestAssured.when().get("/oidc-client/config-property/user-name") + .then() + .statusCode(200) + .body(equalTo("alice")); + } + +} diff --git a/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/OidcClientResource.java b/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/OidcClientResource.java new file mode 100644 index 0000000000000..e88bf2bf74db2 --- /dev/null +++ b/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/OidcClientResource.java @@ -0,0 +1,31 @@ +package io.quarkus.oidc.client.reactive.filter; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RestClient; + +@Path("/oidc-client") +public class OidcClientResource { + + @Inject + @RestClient + ProtectedResourceServiceAnnotationOidcClient protectedResourceServiceAnnotationOidcClient; + + @Inject + @RestClient + ProtectedResourceServiceConfigPropertyOidcClient protectedResourceServiceConfigPropertyOidcClient; + + @GET + @Path("/annotation/user-name") + public String annotationUserName() { + return protectedResourceServiceAnnotationOidcClient.getUserName(); + } + + @GET + @Path("/config-property/user-name") + public String configPropertyUserName() { + return protectedResourceServiceConfigPropertyOidcClient.getUserName(); + } +} diff --git a/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/ProtectedResource.java b/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/ProtectedResource.java new file mode 100644 index 0000000000000..af91c7906f4bb --- /dev/null +++ b/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/ProtectedResource.java @@ -0,0 +1,24 @@ +package io.quarkus.oidc.client.reactive.filter; + +import java.security.Principal; + +import javax.annotation.security.RolesAllowed; +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import io.quarkus.security.Authenticated; + +@Path("/protected") +@Authenticated +public class ProtectedResource { + + @Inject + Principal principal; + + @GET + @RolesAllowed("user") + public String principalName() { + return principal.getName(); + } +} diff --git a/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/ProtectedResourceServiceAnnotationOidcClient.java b/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/ProtectedResourceServiceAnnotationOidcClient.java new file mode 100644 index 0000000000000..d765aa3b7ebb2 --- /dev/null +++ b/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/ProtectedResourceServiceAnnotationOidcClient.java @@ -0,0 +1,17 @@ +package io.quarkus.oidc.client.reactive.filter; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import io.quarkus.oidc.client.filter.OidcClientFilter; + +@OidcClientFilter("annotation") +@RegisterRestClient +@Path("/") +public interface ProtectedResourceServiceAnnotationOidcClient { + + @GET + String getUserName(); +} diff --git a/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/ProtectedResourceServiceConfigPropertyOidcClient.java b/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/ProtectedResourceServiceConfigPropertyOidcClient.java new file mode 100644 index 0000000000000..98ef7a2ce58c3 --- /dev/null +++ b/extensions/oidc-client-reactive-filter/deployment/src/test/java/io/quarkus/oidc/client/reactive/filter/ProtectedResourceServiceConfigPropertyOidcClient.java @@ -0,0 +1,17 @@ +package io.quarkus.oidc.client.reactive.filter; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import io.quarkus.oidc.client.filter.OidcClientFilter; + +@OidcClientFilter +@RegisterRestClient +@Path("/") +public interface ProtectedResourceServiceConfigPropertyOidcClient { + + @GET + String getUserName(); +} diff --git a/extensions/oidc-client-reactive-filter/deployment/src/test/resources/application-oidc-client-reactive-filter.properties b/extensions/oidc-client-reactive-filter/deployment/src/test/resources/application-oidc-client-reactive-filter.properties new file mode 100644 index 0000000000000..008e89aaa1e13 --- /dev/null +++ b/extensions/oidc-client-reactive-filter/deployment/src/test/resources/application-oidc-client-reactive-filter.properties @@ -0,0 +1,23 @@ +quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus/ +quarkus.oidc.client-id=quarkus-service-app +quarkus.oidc.credentials.secret=secret + +quarkus.oidc-client-reactive-filter.client-name=config-property +quarkus.oidc-client.config-property.auth-server-url=${quarkus.oidc.auth-server-url} +quarkus.oidc-client.config-property.client-id=${quarkus.oidc.client-id} +quarkus.oidc-client.config-property.credentials.client-secret.value=${quarkus.oidc.credentials.secret} +quarkus.oidc-client.config-property.credentials.client-secret.method=POST +quarkus.oidc-client.config-property.grant.type=password +quarkus.oidc-client.config-property.grant-options.password.username=alice +quarkus.oidc-client.config-property.grant-options.password.password=alice + +quarkus.oidc-client.annotation.auth-server-url=${quarkus.oidc.auth-server-url} +quarkus.oidc-client.annotation.client-id=${quarkus.oidc.client-id} +quarkus.oidc-client.annotation.credentials.client-secret.value=${quarkus.oidc.credentials.secret} +quarkus.oidc-client.annotation.credentials.client-secret.method=POST +quarkus.oidc-client.annotation.grant.type=password +quarkus.oidc-client.annotation.grant-options.password.username=jdoe +quarkus.oidc-client.annotation.grant-options.password.password=jdoe + +io.quarkus.oidc.client.reactive.filter.ProtectedResourceServiceAnnotationOidcClient/mp-rest/url=http://localhost:8080/protected +io.quarkus.oidc.client.reactive.filter.ProtectedResourceServiceConfigPropertyOidcClient/mp-rest/url=http://localhost:8080/protected \ No newline at end of file diff --git a/extensions/oidc-client-reactive-filter/runtime/src/main/java/io/quarkus/oidc/client/reactive/filter/OidcClientRequestReactiveFilter.java b/extensions/oidc-client-reactive-filter/runtime/src/main/java/io/quarkus/oidc/client/reactive/filter/OidcClientRequestReactiveFilter.java index 41e1ed94fc158..b92a8e1bd6b1b 100644 --- a/extensions/oidc-client-reactive-filter/runtime/src/main/java/io/quarkus/oidc/client/reactive/filter/OidcClientRequestReactiveFilter.java +++ b/extensions/oidc-client-reactive-filter/runtime/src/main/java/io/quarkus/oidc/client/reactive/filter/OidcClientRequestReactiveFilter.java @@ -1,6 +1,5 @@ package io.quarkus.oidc.client.reactive.filter; -import java.util.Optional; import java.util.function.Consumer; import javax.annotation.Priority; @@ -8,35 +7,25 @@ import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; -import org.eclipse.microprofile.config.inject.ConfigProperty; import org.jboss.logging.Logger; import org.jboss.resteasy.reactive.client.spi.ResteasyReactiveClientRequestContext; import org.jboss.resteasy.reactive.client.spi.ResteasyReactiveClientRequestFilter; import io.quarkus.oidc.client.Tokens; -import io.quarkus.oidc.client.runtime.AbstractTokensProducer; +import io.quarkus.oidc.client.reactive.filter.runtime.TokensProducerRegistry; import io.quarkus.oidc.client.runtime.DisabledOidcClientException; import io.quarkus.oidc.common.runtime.OidcConstants; @Priority(Priorities.AUTHENTICATION) -public class OidcClientRequestReactiveFilter extends AbstractTokensProducer implements ResteasyReactiveClientRequestFilter { +public class OidcClientRequestReactiveFilter extends TokensProducerRegistry implements ResteasyReactiveClientRequestFilter { private static final Logger LOG = Logger.getLogger(OidcClientRequestReactiveFilter.class); private static final String BEARER_SCHEME_WITH_SPACE = OidcConstants.BEARER_SCHEME + " "; - @ConfigProperty(name = "quarkus.oidc-client-reactive-filter.client-name") - Optional clientName; - - protected void initTokens() { - if (earlyTokenAcquisition) { - LOG.debug("Token acquisition will be delayed until this filter is executed to avoid blocking an IO thread"); - } - } - @Override public void filter(ResteasyReactiveClientRequestContext requestContext) { requestContext.suspend(); - super.getTokens().subscribe().with(new Consumer<>() { + getTokens(requestContext).subscribe().with(new Consumer<>() { @Override public void accept(Tokens tokens) { requestContext.getHeaders().putSingle(HttpHeaders.AUTHORIZATION, @@ -58,7 +47,4 @@ public void accept(Throwable t) { }); } - protected Optional clientId() { - return clientName; - } } diff --git a/extensions/oidc-client-reactive-filter/runtime/src/main/java/io/quarkus/oidc/client/reactive/filter/runtime/OidcClientReactiveFilterRecorder.java b/extensions/oidc-client-reactive-filter/runtime/src/main/java/io/quarkus/oidc/client/reactive/filter/runtime/OidcClientReactiveFilterRecorder.java new file mode 100644 index 0000000000000..1a8b8a6c6e7e4 --- /dev/null +++ b/extensions/oidc-client-reactive-filter/runtime/src/main/java/io/quarkus/oidc/client/reactive/filter/runtime/OidcClientReactiveFilterRecorder.java @@ -0,0 +1,59 @@ +package io.quarkus.oidc.client.reactive.filter.runtime; + +import java.util.Optional; +import java.util.function.Supplier; + +import io.quarkus.oidc.client.runtime.OidcClientsConfig; +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; +import io.quarkus.runtime.configuration.ConfigurationException; + +@Recorder +public class OidcClientReactiveFilterRecorder { + + private final RuntimeValue oidcClientsConfigRuntimeValue; + + public OidcClientReactiveFilterRecorder(RuntimeValue oidcClientsConfigRuntimeValue) { + this.oidcClientsConfigRuntimeValue = oidcClientsConfigRuntimeValue; + } + + public Supplier createTokensProducer(String clientName, String clientInvokerClass) { + final Optional clientId = Optional.ofNullable(clientName); + final boolean earlyTokensAcquisition; + if (clientName == null) { + earlyTokensAcquisition = oidcClientsConfigRuntimeValue.getValue().defaultClient.earlyTokensAcquisition; + } else { + if (oidcClientsConfigRuntimeValue.getValue().namedClients != null + && oidcClientsConfigRuntimeValue.getValue().namedClients.get(clientName) != null) { + earlyTokensAcquisition = oidcClientsConfigRuntimeValue.getValue().namedClients + .get(clientName).earlyTokensAcquisition; + } else { + throw new ConfigurationException( + String.format("Unknown OIDC client '%s' specified via @OidcClientFilter", clientName)); + } + } + + return new Supplier<>() { + @Override + public TokensProducer get() { + return new TokensProducer(earlyTokensAcquisition) { + + @Override + protected Optional clientId() { + return clientId; + } + + @Override + String clientInvokerClass() { + return clientInvokerClass; + } + }; + } + }; + } + + public void searchForNonDefaultProducers() { + TokensProducerRegistry.searchForNonDefaultProducers = true; + } + +} diff --git a/extensions/oidc-client-reactive-filter/runtime/src/main/java/io/quarkus/oidc/client/reactive/filter/runtime/TokensProducer.java b/extensions/oidc-client-reactive-filter/runtime/src/main/java/io/quarkus/oidc/client/reactive/filter/runtime/TokensProducer.java new file mode 100644 index 0000000000000..d5b9cbf76ad35 --- /dev/null +++ b/extensions/oidc-client-reactive-filter/runtime/src/main/java/io/quarkus/oidc/client/reactive/filter/runtime/TokensProducer.java @@ -0,0 +1,24 @@ +package io.quarkus.oidc.client.reactive.filter.runtime; + +import org.jboss.logging.Logger; + +import io.quarkus.oidc.client.runtime.AbstractTokensProducer; + +public abstract class TokensProducer extends AbstractTokensProducer { + + private static final Logger LOG = Logger.getLogger(TokensProducer.class); + + TokensProducer(boolean earlyTokensAcquisition) { + this.earlyTokenAcquisition = earlyTokensAcquisition; + init(); + } + + abstract String clientInvokerClass(); + + @Override + protected void initTokens() { + if (earlyTokenAcquisition) { + LOG.debug("Token acquisition will be delayed until this filter is executed to avoid blocking an IO thread"); + } + } +} diff --git a/extensions/oidc-client-reactive-filter/runtime/src/main/java/io/quarkus/oidc/client/reactive/filter/runtime/TokensProducerRegistry.java b/extensions/oidc-client-reactive-filter/runtime/src/main/java/io/quarkus/oidc/client/reactive/filter/runtime/TokensProducerRegistry.java new file mode 100644 index 0000000000000..92952c3a60aa5 --- /dev/null +++ b/extensions/oidc-client-reactive-filter/runtime/src/main/java/io/quarkus/oidc/client/reactive/filter/runtime/TokensProducerRegistry.java @@ -0,0 +1,54 @@ +package io.quarkus.oidc.client.reactive.filter.runtime; + +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Named; + +import org.jboss.resteasy.reactive.client.impl.ClientImpl; +import org.jboss.resteasy.reactive.client.spi.ResteasyReactiveClientRequestContext; + +import io.quarkus.arc.All; +import io.quarkus.oidc.client.Tokens; +import io.quarkus.oidc.client.runtime.AbstractTokensProducer; +import io.smallrye.mutiny.Uni; + +public abstract class TokensProducerRegistry { + + /** + * True if at least one @OidcClientFilter annotation instance specified non-default OidcClient name. + */ + static boolean searchForNonDefaultProducers = false; + + public static final String DEFAULT_TOKENS_PRODUCER = "io.quarkus.oidc.client.reactive.filter.runtime.defaultTokenProducer"; + + @All + @Inject + List tokensProducerList; + + @Named(DEFAULT_TOKENS_PRODUCER) + @Inject + TokensProducer defaultTokensProducer; + + protected Uni getTokens(ResteasyReactiveClientRequestContext requestContext) { + return findProducer(requestContext).getTokens(); + } + + private AbstractTokensProducer findProducer(ResteasyReactiveClientRequestContext requestContext) { + + if (searchForNonDefaultProducers) { + // find tokens producer that uses OidcClient with name specified via @OidcClientFilter(clientName = "someClientName") + final String clientInvokerClass = ((ClientImpl) requestContext.getClient()).getClientInvokerClass(); + if (clientInvokerClass != null) { + for (TokensProducer tokensProducer : tokensProducerList) { + if (clientInvokerClass.equals(tokensProducer.clientInvokerClass())) { + return tokensProducer; + } + } + } + } + + return defaultTokensProducer; + } + +} diff --git a/extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/OidcClientBuildStep.java b/extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/OidcClientBuildStep.java index 839ab11187618..2c433972f48d0 100644 --- a/extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/OidcClientBuildStep.java +++ b/extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/OidcClientBuildStep.java @@ -1,6 +1,8 @@ package io.quarkus.oidc.client.deployment; import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -10,6 +12,8 @@ import javax.enterprise.context.RequestScoped; import javax.inject.Singleton; +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.DotName; import io.quarkus.arc.BeanDestroyer; @@ -37,6 +41,7 @@ import io.quarkus.oidc.client.OidcClient; import io.quarkus.oidc.client.OidcClients; import io.quarkus.oidc.client.Tokens; +import io.quarkus.oidc.client.filter.OidcClientFilter; import io.quarkus.oidc.client.runtime.AbstractTokensProducer; import io.quarkus.oidc.client.runtime.OidcClientBuildTimeConfig; import io.quarkus.oidc.client.runtime.OidcClientRecorder; @@ -132,6 +137,25 @@ private SyntheticBeanBuildItem syntheticNamedOidcClientBeanFor(String clientName .done(); } + @BuildStep + public void collectOidcClientFilterClientNames(ApplicationArchivesBuildItem beanArchiveIndex, + BuildProducer clientNamesProducer) { + final DotName oidcClientFilterName = DotName.createSimple(OidcClientFilter.class); + Map clientInvokerClassToName = new HashMap<>(); + for (ApplicationArchive appArchive : beanArchiveIndex.getAllApplicationArchives()) { + for (AnnotationInstance annotationInstance : appArchive.getIndex().getAnnotations(oidcClientFilterName)) { + final AnnotationValue annotationValue = annotationInstance.value(); + if (annotationValue != null && !annotationValue.asString().isEmpty()) { + clientInvokerClassToName.put(annotationInstance.target().asClass().name().toString(), + annotationValue.asString()); + } + } + } + if (!clientInvokerClassToName.isEmpty()) { + clientNamesProducer.produce(new OidcClientFilterClientNamesMapBuildItem(clientInvokerClassToName)); + } + } + @BuildStep public void createNonDefaultTokensProducers( BuildProducer generatedBean, diff --git a/extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/OidcClientFilterClientNamesMapBuildItem.java b/extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/OidcClientFilterClientNamesMapBuildItem.java new file mode 100644 index 0000000000000..c4b4f6cf01030 --- /dev/null +++ b/extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/OidcClientFilterClientNamesMapBuildItem.java @@ -0,0 +1,23 @@ +package io.quarkus.oidc.client.deployment; + +import java.util.Map; + +import io.quarkus.builder.item.SimpleBuildItem; +import io.quarkus.oidc.client.filter.OidcClientFilter; + +/** + * Contains OIDC Client names specified via {@link OidcClientFilter#value()} linked to the target Rest client class. + */ +public final class OidcClientFilterClientNamesMapBuildItem extends SimpleBuildItem { + + private final Map clientInvokerClassToName; + + OidcClientFilterClientNamesMapBuildItem(Map clientInvokerClassToName) { + this.clientInvokerClassToName = Map.copyOf(clientInvokerClassToName); + } + + public Map getClientInvokerClassToName() { + return clientInvokerClassToName; + } + +} diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/filter/OidcClientFilter.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/filter/OidcClientFilter.java index 65574b10e6f00..c1290da28f7a1 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/filter/OidcClientFilter.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/filter/OidcClientFilter.java @@ -10,4 +10,10 @@ @Retention(RetentionPolicy.RUNTIME) @Documented public @interface OidcClientFilter { + + /** + * @return name of the OIDC client that should be used to acquire the tokens + */ + String value() default ""; + } diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java index a6f18ac1f16eb..7eeb1844dcd77 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java @@ -340,6 +340,7 @@ public T build(Class aClass) throws IllegalStateException, RestClientDefi configureProxy(globalProxy.host, globalProxy.port, restClientsConfig.proxyUser.orElse(null), restClientsConfig.proxyPassword.orElse(null), restClientsConfig.nonProxyHosts.orElse(null)); } + clientBuilder.clientInvokerClass(aClass.getName()); ClientImpl client = clientBuilder.build(); WebTargetImpl target = (WebTargetImpl) client.target(uri); target.setParamConverterProviders(paramConverterProviders); diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java index 9d323b012a410..2e26d70e72d44 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java @@ -68,6 +68,8 @@ public class ClientBuilderImpl extends ClientBuilder { private ClientLogger clientLogger = new DefaultClientLogger(); private String userAgent = "Resteasy Reactive Client"; + private String clientInvokerClass = null; + public ClientBuilderImpl() { configuration = new ConfigurationImpl(RuntimeType.CLIENT); } @@ -171,6 +173,11 @@ public ClientBuilder clientLogger(ClientLogger clientLogger) { return this; } + public ClientBuilder clientInvokerClass(String clientInvokerClass) { + this.clientInvokerClass = clientInvokerClass; + return this; + } + @Override public ClientImpl build() { Buffer keyStore = asBuffer(this.keyStore, keystorePassword); @@ -258,7 +265,8 @@ public ClientImpl build() { followRedirects, multiQueryParamMode, loggingScope, - clientLogger, userAgent); + clientLogger, userAgent, + clientInvokerClass); } diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientImpl.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientImpl.java index a7ea3759d8a86..372229c35d107 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientImpl.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientImpl.java @@ -92,13 +92,14 @@ public class ClientImpl implements Client { final Vertx vertx; private final MultiQueryParamMode multiQueryParamMode; private final String userAgent; + private final String clientInvokerClass; public ClientImpl(HttpClientOptions options, ConfigurationImpl configuration, ClientContext clientContext, HostnameVerifier hostnameVerifier, SSLContext sslContext, boolean followRedirects, MultiQueryParamMode multiQueryParamMode, LoggingScope loggingScope, - ClientLogger clientLogger, String userAgent) { + ClientLogger clientLogger, String userAgent, String clientInvokerClass) { this.userAgent = userAgent; configuration = configuration != null ? configuration : new ConfigurationImpl(RuntimeType.CLIENT); this.configuration = configuration; @@ -106,6 +107,7 @@ public ClientImpl(HttpClientOptions options, ConfigurationImpl configuration, Cl this.hostnameVerifier = hostnameVerifier; this.sslContext = sslContext; this.multiQueryParamMode = multiQueryParamMode; + this.clientInvokerClass = clientInvokerClass; Supplier vertx = clientContext.getVertx(); if (vertx != null) { this.vertx = vertx.get(); @@ -333,6 +335,10 @@ Vertx getVertx() { return vertx; } + public String getClientInvokerClass() { + return clientInvokerClass; + } + /** * The point of this class is to not obtain a Vertx reference unless it's absolutely necessary. * We do this in order to avoid needing a Vertx object unless an proper client request is made. diff --git a/integration-tests/oidc-client-reactive/src/main/java/io/quarkus/it/keycloak/FrontendResource.java b/integration-tests/oidc-client-reactive/src/main/java/io/quarkus/it/keycloak/FrontendResource.java index 2475b7cded8ec..835ad9c17691d 100644 --- a/integration-tests/oidc-client-reactive/src/main/java/io/quarkus/it/keycloak/FrontendResource.java +++ b/integration-tests/oidc-client-reactive/src/main/java/io/quarkus/it/keycloak/FrontendResource.java @@ -19,6 +19,10 @@ public class FrontendResource { @RestClient ProtectedResourceServiceReactiveFilter protectedResourceServiceReactiveFilter; + @Inject + @RestClient + ProtectedResourceServiceNamedFilter protectedResourceServiceNamedFilter; + @GET @Path("userNameCustomFilter") @Produces("text/plain") @@ -32,4 +36,11 @@ public Uni userName() { public Uni userNameReactive() { return protectedResourceServiceReactiveFilter.getUserName(); } + + @GET + @Path("userNameNamedFilter") + @Produces("text/plain") + public Uni userNameNamedFilter() { + return protectedResourceServiceNamedFilter.getUserName(); + } } diff --git a/integration-tests/oidc-client-reactive/src/main/java/io/quarkus/it/keycloak/ProtectedResourceServiceNamedFilter.java b/integration-tests/oidc-client-reactive/src/main/java/io/quarkus/it/keycloak/ProtectedResourceServiceNamedFilter.java new file mode 100644 index 0000000000000..04d703f5d5f39 --- /dev/null +++ b/integration-tests/oidc-client-reactive/src/main/java/io/quarkus/it/keycloak/ProtectedResourceServiceNamedFilter.java @@ -0,0 +1,21 @@ +package io.quarkus.it.keycloak; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import io.quarkus.oidc.client.filter.OidcClientFilter; +import io.smallrye.mutiny.Uni; + +@RegisterRestClient +@OidcClientFilter("named-client") +@Path("/") +public interface ProtectedResourceServiceNamedFilter { + + @GET + @Produces("text/plain") + @Path("userNameReactive") + Uni getUserName(); +} diff --git a/integration-tests/oidc-client-reactive/src/main/resources/application.properties b/integration-tests/oidc-client-reactive/src/main/resources/application.properties index 9a214ffeb8d97..26e2d60ffb0e4 100644 --- a/integration-tests/oidc-client-reactive/src/main/resources/application.properties +++ b/integration-tests/oidc-client-reactive/src/main/resources/application.properties @@ -10,8 +10,16 @@ quarkus.oidc-client.grant.type=password quarkus.oidc-client.grant-options.password.username=alice quarkus.oidc-client.grant-options.password.password=alice +quarkus.oidc-client.named-client.auth-server-url=${quarkus.oidc.auth-server-url} +quarkus.oidc-client.named-client.client-id=${quarkus.oidc.client-id} +quarkus.oidc-client.named-client.credentials.secret=${quarkus.oidc.credentials.secret} +quarkus.oidc-client.named-client.grant.type=password +quarkus.oidc-client.named-client.grant-options.password.username=jdoe +quarkus.oidc-client.named-client.grant-options.password.password=jdoe + io.quarkus.it.keycloak.ProtectedResourceServiceCustomFilter/mp-rest/url=http://localhost:8081/protected io.quarkus.it.keycloak.ProtectedResourceServiceReactiveFilter/mp-rest/url=http://localhost:8081/protected +io.quarkus.it.keycloak.ProtectedResourceServiceNamedFilter/mp-rest/url=http://localhost:8081/protected quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientImpl".min-level=TRACE quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientImpl".level=TRACE diff --git a/integration-tests/oidc-client-reactive/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java b/integration-tests/oidc-client-reactive/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java index f216770d3c413..ab999b6a55d01 100644 --- a/integration-tests/oidc-client-reactive/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java +++ b/integration-tests/oidc-client-reactive/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java @@ -35,6 +35,15 @@ public void testGetUserNameCustomFilter() { .body(equalTo("alice")); } + @Test + public void testGetUserNameNamedFilter() { + RestAssured.given().header("Accept", "text/plain") + .when().get("/frontend/userNameNamedFilter") + .then() + .statusCode(200) + .body(equalTo("jdoe")); + } + @Test public void testGetUserNameReactive() { RestAssured.given().header("Accept", "text/plain") diff --git a/integration-tests/oidc-client/src/main/java/io/quarkus/it/keycloak/FrontendResource.java b/integration-tests/oidc-client/src/main/java/io/quarkus/it/keycloak/FrontendResource.java index dd163cea232af..de5fe9f39ee15 100644 --- a/integration-tests/oidc-client/src/main/java/io/quarkus/it/keycloak/FrontendResource.java +++ b/integration-tests/oidc-client/src/main/java/io/quarkus/it/keycloak/FrontendResource.java @@ -25,6 +25,10 @@ public class FrontendResource { @RestClient ProtectedResourceServiceNoOidcClient protectedResourceServiceNoOidcClient; + @Inject + @RestClient + ProtectedResourceServiceNonDefaultOidcClient protectedResourceServiceNonDefaultOidcClient; + @Inject ManagedExecutor managedExecutor; @@ -37,6 +41,12 @@ public String userNameOidcClient() { return protectedResourceServiceOidcClient.getUserName(); } + @GET + @Path("userNonDefaultOidcClient") + public String userNameNonDefaultOidcClient() { + return protectedResourceServiceNonDefaultOidcClient.getUserName(); + } + @GET @Path("userOidcClientManagedExecutor") public String userNameOidcClientManagedExecutor() throws Exception { diff --git a/integration-tests/oidc-client/src/main/java/io/quarkus/it/keycloak/ProtectedResourceServiceNonDefaultOidcClient.java b/integration-tests/oidc-client/src/main/java/io/quarkus/it/keycloak/ProtectedResourceServiceNonDefaultOidcClient.java new file mode 100644 index 0000000000000..676632398d3a1 --- /dev/null +++ b/integration-tests/oidc-client/src/main/java/io/quarkus/it/keycloak/ProtectedResourceServiceNonDefaultOidcClient.java @@ -0,0 +1,18 @@ +package io.quarkus.it.keycloak; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import io.quarkus.oidc.client.filter.OidcClientFilter; + +@RegisterRestClient +@OidcClientFilter("non-default-client") +@Path("/") +public interface ProtectedResourceServiceNonDefaultOidcClient { + + @GET + String getUserName(); + +} diff --git a/integration-tests/oidc-client/src/main/resources/application.properties b/integration-tests/oidc-client/src/main/resources/application.properties index 6351861a8f3ab..d020d5631dc32 100644 --- a/integration-tests/oidc-client/src/main/resources/application.properties +++ b/integration-tests/oidc-client/src/main/resources/application.properties @@ -16,10 +16,18 @@ quarkus.oidc-client.named.grant.type=password quarkus.oidc-client.named.grant-options.password.username=alice quarkus.oidc-client.named.grant-options.password.password=alice +quarkus.oidc-client.non-default-client.auth-server-url=${quarkus.oidc.auth-server-url} +quarkus.oidc-client.non-default-client.client-id=${quarkus.oidc.client-id} +quarkus.oidc-client.non-default-client.credentials.secret=${quarkus.oidc.credentials.secret} +quarkus.oidc-client.non-default-client.grant.type=password +quarkus.oidc-client.non-default-client.grant-options.password.username=bob +quarkus.oidc-client.non-default-client.grant-options.password.password=bob + io.quarkus.it.keycloak.ProtectedResourceServiceRegisterProvider/mp-rest/url=http://localhost:8081/protected io.quarkus.it.keycloak.ProtectedResourceServiceOidcClient/mp-rest/url=http://localhost:8081/protected io.quarkus.it.keycloak.ProtectedResourceServiceNamedOidcClient/mp-rest/url=http://localhost:8081/protected io.quarkus.it.keycloak.ProtectedResourceServiceNoOidcClient/mp-rest/url=http://localhost:8081/protected +io.quarkus.it.keycloak.ProtectedResourceServiceNonDefaultOidcClient/mp-rest/url=http://localhost:8081/protected quarkus.tls.trust-all=true quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientImpl".min-level=TRACE diff --git a/integration-tests/oidc-client/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java b/integration-tests/oidc-client/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java index a760650e66bbd..a5f70b2f08f75 100644 --- a/integration-tests/oidc-client/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java +++ b/integration-tests/oidc-client/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java @@ -38,6 +38,7 @@ public Map start() { realm.getClients().add(createClient("quarkus-app")); realm.getUsers().add(createUser("alice", "user")); + realm.getUsers().add(createUser("bob", "user")); try { RestAssured diff --git a/integration-tests/oidc-client/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java b/integration-tests/oidc-client/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java index c64965dc44ce0..94c1747f1fc1f 100644 --- a/integration-tests/oidc-client/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java +++ b/integration-tests/oidc-client/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java @@ -36,6 +36,14 @@ public void testGetUserNameOidcClient() { .body(equalTo("alice")); } + @Test + public void testGetUserNameNonDefaultOidcClient() { + RestAssured.when().get("/frontend/userNonDefaultOidcClient") + .then() + .statusCode(200) + .body(equalTo("bob")); + } + @Test public void testGetUserNameOidcClientManagedExecutor() { RestAssured.when().get("/frontend/userOidcClientManagedExecutor")