Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OIDC token propagation: add option to select named OIDC client and token exchange per REST client with the @AccessToken annotation #39132

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1031,8 +1031,9 @@
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,10 +1052,10 @@
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"}

The `quarkus-oidc-token-propagation` extension provides two Jakarta REST `jakarta.ws.rs.client.ClientRequestFilter` class implementations that simplify the propagation of authentication information.
`io.quarkus.oidc.token.propagation.AccessTokenRequestFilter` propagates the xref:security-oidc-bearer-token-authentication.adoc[Bearer token] present in the current active request or the token acquired from the xref:security-oidc-code-flow-authentication.adoc[Authorization code flow mechanism], as the HTTP `Authorization` header's `Bearer` scheme value.
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
Loading