Skip to content

Commit

Permalink
Select OIDC client per REST client with @accesstoken
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed Mar 4, 2024
1 parent 9b17ee9 commit c8ae838
Show file tree
Hide file tree
Showing 11 changed files with 585 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1031,8 +1031,9 @@ quarkus.oidc-client.credentials.secret=secret
quarkus.oidc-client.grant.type=exchange
quarkus.oidc-client.grant-options.exchange.audience=quarkus-app-exchange
quarkus.oidc-token-propagation.exchange-token=true
quarkus.oidc-token-propagation.exchange-token=true <1>
----
<1> Please note that the `exchange-token` configuration property is ignored when the OidcClient name is set with the `io.quarkus.oidc.token.propagation.AccessToken#exchangeTokenClient` annotation attribute.

Check warning on line 1036 in docs/src/main/asciidoc/security-openid-connect-client-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Fluff] Depending on the context, consider using 'Be concise: rewrite the sentence to not use' rather than 'note that'. Raw Output: {"message": "[Quarkus.Fluff] Depending on the context, consider using 'Be concise: rewrite the sentence to not use' rather than 'note that'.", "location": {"path": "docs/src/main/asciidoc/security-openid-connect-client-reference.adoc", "range": {"start": {"line": 1036, "column": 12}}}, "severity": "INFO"}

Note `AccessTokenRequestReactiveFilter` will use `OidcClient` to exchange the current token, and you can use `quarkus.oidc-client.grant-options.exchange` to set the additional exchange properties expected by your OpenID Connect Provider.

Expand All @@ -1051,7 +1052,7 @@ quarkus.oidc-client.scopes=https://graph.microsoft.com/user.read,offline_access
quarkus.oidc-token-propagation-reactive.exchange-token=true
----

`AccessTokenRequestReactiveFilter` uses a default `OidcClient` by default. A named `OidcClient` can be selected with a `quarkus.oidc-token-propagation-reactive.client-name` configuration property.
`AccessTokenRequestReactiveFilter` uses a default `OidcClient` by default. A named `OidcClient` can be selected with a `quarkus.oidc-token-propagation-reactive.client-name` configuration property or with the `io.quarkus.oidc.token.propagation.AccessToken#exchangeTokenClient` annotation attribute.

[[token-propagation]]
== Token Propagation

Check warning on line 1058 in docs/src/main/asciidoc/security-openid-connect-client-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Headings] Use sentence-style capitalization in 'Token Propagation'. Raw Output: {"message": "[Quarkus.Headings] Use sentence-style capitalization in 'Token Propagation'.", "location": {"path": "docs/src/main/asciidoc/security-openid-connect-client-reference.adoc", "range": {"start": {"line": 1058, "column": 4}}}, "severity": "INFO"}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.quarkus.oidc.client.deployment;

import java.util.Objects;

import org.jboss.jandex.AnnotationTarget;

import io.quarkus.builder.item.MultiBuildItem;

/**
* Represents one {@link io.quarkus.oidc.token.propagation.AccessToken} annotation instance.
*/
public final class AccessTokenInstanceBuildItem extends MultiBuildItem {

private final String clientName;
private final boolean tokenExchange;
private final AnnotationTarget annotationTarget;

AccessTokenInstanceBuildItem(String clientName, Boolean tokenExchange, AnnotationTarget annotationTarget) {
this.clientName = Objects.requireNonNull(clientName);
this.tokenExchange = tokenExchange;
this.annotationTarget = Objects.requireNonNull(annotationTarget);
}

public String getClientName() {
return clientName;
}

public boolean exchangeTokenActivated() {
return tokenExchange;
}

public AnnotationTarget getAnnotationTarget() {
return annotationTarget;
}

public String targetClass() {
return annotationTarget.asClass().name().toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package io.quarkus.oidc.client.deployment;

import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;

import jakarta.annotation.Priority;
import jakarta.inject.Singleton;

import io.quarkus.arc.deployment.GeneratedBeanBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor;
import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.gizmo.ClassCreator;

public final class AccessTokenRequestFilterGenerator {

private static final int AUTHENTICATION = 1000;

private record ClientNameAndExchangeToken(String clientName, boolean exchangeTokenActivated) {
}

private final BuildProducer<UnremovableBeanBuildItem> unremovableBeansProducer;
private final BuildProducer<ReflectiveClassBuildItem> reflectiveClassProducer;
private final BuildProducer<GeneratedBeanBuildItem> generatedBeanProducer;
private final Class<?> requestFilterClass;
private final Map<ClientNameAndExchangeToken, String> cache = new HashMap<>();

public AccessTokenRequestFilterGenerator(BuildProducer<UnremovableBeanBuildItem> unremovableBeansProducer,
BuildProducer<ReflectiveClassBuildItem> reflectiveClassProducer,
BuildProducer<GeneratedBeanBuildItem> generatedBeanProducer, Class<?> requestFilterClass) {
this.unremovableBeansProducer = unremovableBeansProducer;
this.reflectiveClassProducer = reflectiveClassProducer;
this.generatedBeanProducer = generatedBeanProducer;
this.requestFilterClass = requestFilterClass;
}

public String generateClass(AccessTokenInstanceBuildItem instance) {
return cache.computeIfAbsent(
new ClientNameAndExchangeToken(instance.getClientName(), instance.exchangeTokenActivated()), i -> {
var adaptor = new GeneratedBeanGizmoAdaptor(generatedBeanProducer);
String className = createUniqueClassName(i);
try (ClassCreator classCreator = ClassCreator.builder()
.className(className)
.superClass(requestFilterClass)
.classOutput(adaptor)
.build()) {
classCreator.addAnnotation(Priority.class).add("value", AUTHENTICATION);
classCreator.addAnnotation(Singleton.class);

if (!i.clientName().isEmpty()) {
try (var methodCreator = classCreator.getMethodCreator("getClientName", String.class)) {
methodCreator.addAnnotation(Override.class.getName(), RetentionPolicy.CLASS);
methodCreator.setModifiers(Modifier.PROTECTED);
methodCreator.returnValue(methodCreator.load(i.clientName()));
}
}
if (i.exchangeTokenActivated()) {
try (var methodCreator = classCreator.getMethodCreator("isExchangeToken", boolean.class)) {
methodCreator.addAnnotation(Override.class.getName(), RetentionPolicy.CLASS);
methodCreator.setModifiers(Modifier.PROTECTED);
methodCreator.returnBoolean(true);
}
}
}
unremovableBeansProducer.produce(UnremovableBeanBuildItem.beanClassNames(className));
reflectiveClassProducer
.produce(ReflectiveClassBuildItem.builder(className).methods().fields().constructors().build());
return className;
});
}

private String createUniqueClassName(ClientNameAndExchangeToken i) {
return "%s_%sClient_%sTokenExchange".formatted(requestFilterClass.getName(), clientName(i.clientName()),
exchangeTokenName(i.exchangeTokenActivated()));
}

private static String clientName(String clientName) {
if (clientName.isEmpty()) {
return "Default";
} else {
return clientName;
}
}

private static String exchangeTokenName(boolean enabled) {
if (enabled) {
return "Enabled";
} else {
return "Default";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static io.quarkus.oidc.client.deployment.OidcClientFilterDeploymentHelper.sanitize;

import java.lang.reflect.Modifier;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
Expand All @@ -12,6 +13,7 @@
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Singleton;

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

import io.quarkus.arc.BeanDestroyer;
Expand All @@ -28,6 +30,7 @@
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.ApplicationArchivesBuildItem;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem;
import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem;
import io.quarkus.gizmo.ClassCreator;
Expand All @@ -45,12 +48,15 @@
import io.quarkus.oidc.client.runtime.OidcClientsConfig;
import io.quarkus.oidc.client.runtime.TokensHelper;
import io.quarkus.oidc.client.runtime.TokensProducer;
import io.quarkus.oidc.token.propagation.AccessToken;
import io.quarkus.runtime.TlsConfig;
import io.quarkus.vertx.core.deployment.CoreVertxBuildItem;

@BuildSteps(onlyIf = OidcClientBuildStep.IsEnabled.class)
public class OidcClientBuildStep {

private static final DotName ACCESS_TOKEN = DotName.createSimple(AccessToken.class.getName());

@BuildStep
ExtensionSslNativeSupportBuildItem enableSslInNative() {
return new ExtensionSslNativeSupportBuildItem(Feature.OIDC_CLIENT);
Expand Down Expand Up @@ -149,6 +155,26 @@ public void createNonDefaultTokensProducers(
}
}

@BuildStep
public List<AccessTokenInstanceBuildItem> collectAccessTokenInstances(CombinedIndexBuildItem index) {
record ItemBuilder(AnnotationInstance instance) {

private String toClientName() {
var value = instance.value("exchangeTokenClient");
return value == null || value.asString().equals("Default") ? "" : value.asString();
}

private boolean toExchangeToken() {
return instance.value("exchangeTokenClient") != null;
}

private AccessTokenInstanceBuildItem build() {
return new AccessTokenInstanceBuildItem(toClientName(), toExchangeToken(), instance.target());
}
}
return index.getIndex().getAnnotations(ACCESS_TOKEN).stream().map(ItemBuilder::new).map(ItemBuilder::build).toList();
}

/**
* Creates a Tokens producer class like follows:
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,13 @@
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AccessToken {

/**
* Selects name of the configured OidcClient and activates token exchange for the annotated REST client.
* Default OidcClient is used when no value is specified, in which case the token exchange will only be active when
* the '...exchange-token' configuration property is set to 'true'.
* Please note that value 'Default' selects the default OidcClient and activates token exchange.
*/
String exchangeTokenClient() default "";

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,55 @@
import static io.quarkus.oidc.token.propagation.TokenPropagationConstants.JWT_PROPAGATE_TOKEN_CREDENTIAL;
import static io.quarkus.oidc.token.propagation.TokenPropagationConstants.OIDC_PROPAGATE_TOKEN_CREDENTIAL;

import java.util.Collection;
import java.util.List;
import java.util.function.BooleanSupplier;

import jakarta.ws.rs.Priorities;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.DotName;
import org.jboss.jandex.Type;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanBuildItem;
import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
import io.quarkus.deployment.Capabilities;
import io.quarkus.deployment.Capability;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.BuildSteps;
import io.quarkus.deployment.builditem.AdditionalIndexedClassesBuildItem;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.SystemPropertyBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.oidc.token.propagation.AccessToken;
import io.quarkus.oidc.client.deployment.AccessTokenInstanceBuildItem;
import io.quarkus.oidc.client.deployment.AccessTokenRequestFilterGenerator;
import io.quarkus.rest.client.reactive.deployment.DotNames;
import io.quarkus.rest.client.reactive.deployment.RegisterProviderAnnotationInstanceBuildItem;
import io.quarkus.runtime.configuration.ConfigurationException;

@BuildSteps(onlyIf = OidcTokenPropagationReactiveBuildStep.IsEnabled.class)
public class OidcTokenPropagationReactiveBuildStep {

private static final DotName ACCESS_TOKEN = DotName.createSimple(AccessToken.class.getName());
private static final DotName ACCESS_TOKEN_REQUEST_REACTIVE_FILTER = DotName
.createSimple(AccessTokenRequestReactiveFilter.class.getName());

@BuildStep
void oidcClientFilterSupport(CombinedIndexBuildItem indexBuildItem,
BuildProducer<RegisterProviderAnnotationInstanceBuildItem> producer) {
Collection<AnnotationInstance> instances = indexBuildItem.getIndex().getAnnotations(ACCESS_TOKEN);
for (AnnotationInstance instance : instances) {
String targetClass = instance.target().asClass().name().toString();
producer.produce(new RegisterProviderAnnotationInstanceBuildItem(targetClass, AnnotationInstance.create(
DotNames.REGISTER_PROVIDER, instance.target(), List.of(AnnotationValue.createClassValue("value",
Type.create(ACCESS_TOKEN_REQUEST_REACTIVE_FILTER, org.jboss.jandex.Type.Kind.CLASS))))));
void oidcClientFilterSupport(List<AccessTokenInstanceBuildItem> accessTokenInstances,
BuildProducer<UnremovableBeanBuildItem> unremovableBeans,
BuildProducer<ReflectiveClassBuildItem> reflectiveClass,
BuildProducer<GeneratedBeanBuildItem> generatedBean,
BuildProducer<RegisterProviderAnnotationInstanceBuildItem> providerProducer) {
if (!accessTokenInstances.isEmpty()) {
var filterGenerator = new AccessTokenRequestFilterGenerator(unremovableBeans, reflectiveClass, generatedBean,
AccessTokenRequestReactiveFilter.class);
for (AccessTokenInstanceBuildItem instance : accessTokenInstances) {
String providerClass = filterGenerator.generateClass(instance);
providerProducer
.produce(new RegisterProviderAnnotationInstanceBuildItem(instance.targetClass(),
AnnotationInstance.create(DotNames.REGISTER_PROVIDER, instance.getAnnotationTarget(), List.of(
AnnotationValue.createClassValue("value",
Type.create(DotName.createSimple(providerClass),
org.jboss.jandex.Type.Kind.CLASS)),
AnnotationValue.createIntegerValue("priority", Priorities.AUTHENTICATION)))));
}
}
}

Expand All @@ -55,7 +64,6 @@ void registerProvider(BuildProducer<AdditionalBeanBuildItem> additionalBeans,
ReflectiveClassBuildItem.builder(AccessTokenRequestReactiveFilter.class).methods().fields().build());
additionalIndexedClassesBuildItem
.produce(new AdditionalIndexedClassesBuildItem(AccessTokenRequestReactiveFilter.class.getName()));

}

@BuildStep(onlyIf = IsEnabledDuringAuth.class)
Expand Down
Loading

0 comments on commit c8ae838

Please sign in to comment.