Skip to content

Commit

Permalink
Enhance OidcClientFilter to select named OIDC clients
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed Dec 14, 2022
1 parent 7c6bb9b commit 561a063
Show file tree
Hide file tree
Showing 41 changed files with 942 additions and 127 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ public class OidcClientResource {
}
----

[[use-oidc-clients]]
=== Use OidcClients

`io.quarkus.oidc.client.OidcClients` is a container of ``OidcClient``s - it includes a default `OidcClient` and named clients which can be configured like this:
Expand Down Expand Up @@ -419,7 +420,25 @@ 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 through annotation has higher priority than the `quarkus.oidc-client-reactive-filter.client-name` configuration property.
For example, given <<use-oidc-clients, this>> `jwt-secret` named OIDC client declaration, you can refer to this client like this:

[source,java]
----
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientFilter;
import io.smallrye.mutiny.Uni;
@RegisterRestClient
@OidcClientFilter("jwt-secret")
@Path("/")
public interface ProtectedResourceService {
@GET
Uni<String> getUserName();
}
----

[[oidc-client-filter]]
=== Use OidcClient in RestClient ClientFilter
Expand Down Expand Up @@ -478,6 +497,23 @@ 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 through annotation has higher priority than the `quarkus.oidc-client-filter.client-name` configuration property.
For example, given <<use-oidc-clients, this>> `jwt-secret` named OIDC client declaration, you can refer to this client like this:

[source,java]
----
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientFilter;
@RegisterRestClient
@OidcClientFilter("jwt-secret")
@Path("/")
public interface ProtectedResourceService {
@GET
String getUserName();
}
----

=== Use Custom RestClient ClientFilter

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.quarkus.oidc.client.filter.deployment;

import java.util.Set;

import io.quarkus.builder.item.SimpleBuildItem;

/**
* Contains a list of all Rest clients annotated with @OidcClientFilter("someClientName").
*/
public final class NamedOidcClientFilterBuildItem extends SimpleBuildItem {

final Set<String> namedFilterClientClasses;

NamedOidcClientFilterBuildItem(Set<String> namedFilterClientClasses) {
this.namedFilterClientClasses = Set.copyOf(namedFilterClientClasses);
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
package io.quarkus.oidc.client.filter.deployment;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Predicate;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanBuildItem;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.BuildSteps;
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.OidcClientFilterDeploymentHelper;
import io.quarkus.oidc.client.filter.OidcClientFilter;
import io.quarkus.oidc.client.filter.OidcClientRequestFilter;
import io.quarkus.oidc.client.filter.runtime.AbstractOidcClientRequestFilter;
import io.quarkus.oidc.client.filter.runtime.OidcClientFilterConfig;
import io.quarkus.restclient.deployment.RestClientAnnotationProviderBuildItem;
import io.quarkus.restclient.deployment.RestClientPredicateProviderBuildItem;
import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem;

@BuildSteps(onlyIf = IsEnabled.class)
Expand All @@ -24,15 +36,92 @@ public class OidcClientFilterBuildStep {
@BuildStep
void registerProvider(BuildProducer<AdditionalBeanBuildItem> additionalBeans,
BuildProducer<ReflectiveClassBuildItem> reflectiveClass,
NamedOidcClientFilterBuildItem namedOidcClientFilterBuildItem,
BuildProducer<ResteasyJaxrsProviderBuildItem> jaxrsProviders,
BuildProducer<RestClientPredicateProviderBuildItem> restPredicateProvider,
BuildProducer<RestClientAnnotationProviderBuildItem> restAnnotationProvider) {

additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(OidcClientRequestFilter.class));
reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, OidcClientRequestFilter.class));
final Set<String> namedFilterClientClasses = namedOidcClientFilterBuildItem.namedFilterClientClasses;

// register default request filter provider against the rest of the clients (client != namedFilterClientClasses)
if (config.registerFilter) {
jaxrsProviders.produce(new ResteasyJaxrsProviderBuildItem(OidcClientRequestFilter.class.getName()));
if (namedFilterClientClasses.isEmpty()) {
// register default request filter as global rest client provider
jaxrsProviders.produce(new ResteasyJaxrsProviderBuildItem(OidcClientRequestFilter.class.getName()));
} else {
// register all clients without @OidcClientFilter("clientName")
restPredicateProvider
.produce(new RestClientPredicateProviderBuildItem(OidcClientRequestFilter.class.getName(),
new Predicate<ClassInfo>() {
// test whether the provider should be added restClientClassInfo
@Override
public boolean test(ClassInfo restClientClassInfo) {
// do not register default request filter as provider against Rest client with named filter
return !namedFilterClientClasses.contains(restClientClassInfo.name().toString());
}
}));
}
} else {
restAnnotationProvider.produce(new RestClientAnnotationProviderBuildItem(OIDC_CLIENT_FILTER,
OidcClientRequestFilter.class));
if (namedFilterClientClasses.isEmpty()) {
// register default request filter against all the Rest clients annotated with @OidcClientFilter
restAnnotationProvider.produce(new RestClientAnnotationProviderBuildItem(OIDC_CLIENT_FILTER,
OidcClientRequestFilter.class));
} else {
// register default request filter against Rest client annotated with @OidcClientFilter without named ones
restPredicateProvider
.produce(new RestClientPredicateProviderBuildItem(OidcClientRequestFilter.class.getName(),
new Predicate<ClassInfo>() {
// test whether the provider should be added restClientClassInfo
@Override
public boolean test(ClassInfo restClientClassInfo) {
// do not register default request filter as provider against Rest client with named filter
return restClientClassInfo.hasAnnotation(OIDC_CLIENT_FILTER)
&& !namedFilterClientClasses.contains(restClientClassInfo.name().toString());
}
}));
}
}
}

@BuildStep
NamedOidcClientFilterBuildItem registerNamedProviders(BuildProducer<ReflectiveClassBuildItem> reflectiveClass,
CombinedIndexBuildItem indexBuildItem,
BuildProducer<RestClientPredicateProviderBuildItem> restPredicateProvider,
BuildProducer<GeneratedBeanBuildItem> generatedBean) {

// create and register named request filter for each @OidcClientFilter("clientName")
final var helper = new OidcClientFilterDeploymentHelper<>(AbstractOidcClientRequestFilter.class, generatedBean);
Collection<AnnotationInstance> instances = indexBuildItem.getIndex().getAnnotations(OIDC_CLIENT_FILTER);
final Set<String> namedFilterClientClasses = new HashSet<>();
for (AnnotationInstance instance : instances) {
// get client name from annotation @OidcClientFilter("clientName")
final String clientName = OidcClientFilterDeploymentHelper.getClientName(instance);
// do not create & register named filter for the OidcClient registered through configuration property
// as default request filter got it covered
if (clientName != null && !clientName.equals(config.clientName.orElse(null))) {

// create named filter class for named OidcClient
// we generate exactly one custom filter for each named client specified through annotation
final var generatedProvider = helper.getOrCreateNamedTokensProducerFor(clientName);
final var targetRestClient = instance.target().asClass().name().toString();
namedFilterClientClasses.add(targetRestClient);

// register for reflection
reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, true, true, generatedProvider));

// register named request filter provider against Rest client
restPredicateProvider.produce(new RestClientPredicateProviderBuildItem(generatedProvider,
new Predicate<ClassInfo>() {
// test whether the provider should be added restClientClassInfo
@Override
public boolean test(ClassInfo restClientClassInfo) {
return targetRestClient.equals(restClientClassInfo.name().toString());
}
}));
}
}
return new NamedOidcClientFilterBuildItem(namedFilterClientClasses);
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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"));
}

}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 561a063

Please sign in to comment.