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 11, 2022
1 parent d4e5ec7 commit 9a84925
Show file tree
Hide file tree
Showing 39 changed files with 918 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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]]
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -35,4 +47,57 @@ void registerProvider(BuildProducer<AdditionalBeanBuildItem> additionalBeans,
OidcClientRequestFilter.class));
}
}

@Record(ExecutionTime.STATIC_INIT)
@BuildStep
void setClientInvokerToName(OidcClientFilterRecorder recorder,
Optional<OidcClientFilterClientNamesMapBuildItem> 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<String, String> invokerClassToClientName = new HashMap<>();
for (Map.Entry<String, String> 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<OidcClientFilterClientNamesMapBuildItem> scanningResult,
BuildProducer<SyntheticBeanBuildItem> 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());
}
}
}
}
}
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
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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;
Expand All @@ -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());
Expand All @@ -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()
.<AbstractTokensProducer> instance(toTokensProducerBeanName(clientId))
.get();
}
}
}
return this;
}

protected Optional<String> clientId() {
Expand Down
Loading

0 comments on commit 9a84925

Please sign in to comment.