Skip to content

Commit

Permalink
Merge pull request #39132 from michalvavrik/feature/access-token-name…
Browse files Browse the repository at this point in the history
…d-client

OIDC token propagation: add option to select named OIDC client and token exchange per REST client with the @accesstoken annotation
  • Loading branch information
sberyozkin authored Mar 4, 2024
2 parents e1d7c83 + 5ba2a00 commit 58b0395
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.

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
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.
* Please note that the default OidcClient's name is `Default`. You do not have to enable this attribute
* if you use the default OidcClient and already have either 'quarkus.oidc-token-propagation.exchange-token'
* or 'quarkus.oidc-token-propagation-reactive.exchange-token' property set to 'true'
*/
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 58b0395

Please sign in to comment.