Skip to content
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 @@ -1391,6 +1391,29 @@ public interface ProtectedResourceService {

or

[source,java]
----
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.common.AccessToken;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@RegisterRestClient
@Path("/")
public interface InformationService {

@AccessToken
@GET
String getUserName();

@Path("/public")
@GET
String getPublicInformation();
}
----

or

[source,java]
----
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
Expand Down Expand Up @@ -1482,6 +1505,30 @@ public interface ProtectedResourceService {
String getUserName();
}
----

or

[source,java]
----
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.common.AccessToken;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@RegisterRestClient
@Path("/")
public interface InformationService {

@AccessToken
@GET
String getUserName();

@Path("/public")
@GET
String getPublicInformation();
}
----

or

[source,java]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.Objects;

import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.MethodInfo;

import io.quarkus.builder.item.MultiBuildItem;

Expand All @@ -14,18 +15,21 @@ public final class AccessTokenInstanceBuildItem extends MultiBuildItem {
private final String clientName;
private final boolean tokenExchange;
private final AnnotationTarget annotationTarget;
private final MethodInfo targetMethodInfo;

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

public String getClientName() {
String getClientName() {
return clientName;
}

public boolean exchangeTokenActivated() {
boolean exchangeTokenActivated() {
return tokenExchange;
}

Expand All @@ -34,6 +38,13 @@ public AnnotationTarget getAnnotationTarget() {
}

public String targetClass() {
return annotationTarget.asClass().name().toString();
if (annotationTarget.kind() == AnnotationTarget.Kind.CLASS) {
return annotationTarget.asClass().name().toString();
}
return annotationTarget.asMethod().declaringClass().name().toString();
}

MethodInfo getTargetMethodInfo() {
return targetMethodInfo;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,30 @@
import jakarta.annotation.Priority;
import jakarta.inject.Singleton;

import org.jboss.jandex.MethodInfo;

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;
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.gizmo.ResultHandle;
import io.quarkus.security.spi.runtime.MethodDescription;

public final class AccessTokenRequestFilterGenerator {

private static final int AUTHENTICATION = 1000;

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

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<>();
private final Map<RequestFilterKey, String> cache = new HashMap<>();

public AccessTokenRequestFilterGenerator(BuildProducer<UnremovableBeanBuildItem> unremovableBeansProducer,
BuildProducer<ReflectiveClassBuildItem> reflectiveClassProducer,
Expand All @@ -39,7 +44,9 @@ public AccessTokenRequestFilterGenerator(BuildProducer<UnremovableBeanBuildItem>

public String generateClass(AccessTokenInstanceBuildItem instance) {
return cache.computeIfAbsent(
new ClientNameAndExchangeToken(instance.getClientName(), instance.exchangeTokenActivated()), i -> {
new RequestFilterKey(instance.getClientName(), instance.exchangeTokenActivated(),
instance.getTargetMethodInfo()),
i -> {
var adaptor = new GeneratedBeanGizmoAdaptor(generatedBeanProducer);
String className = createUniqueClassName(i);
try (ClassCreator classCreator = ClassCreator.builder()
Expand All @@ -64,6 +71,37 @@ public String generateClass(AccessTokenInstanceBuildItem instance) {
methodCreator.returnBoolean(true);
}
}

/*
* protected MethodDescription getMethodDescription() {
* return new MethodDescription(declaringClassName, methodName, parameterTypes);
* }
*/
if (i.targetMethodInfo != null) {
try (var methodCreator = classCreator.getMethodCreator("getMethodDescription",
MethodDescription.class)) {
methodCreator.addAnnotation(Override.class.getName(), RetentionPolicy.CLASS);
methodCreator.setModifiers(Modifier.PROTECTED);

// String methodName
var methodName = methodCreator.load(i.targetMethodInfo.name());
// String declaringClassName
var declaringClassName = methodCreator
.load(i.targetMethodInfo.declaringClass().name().toString());
// String[] paramTypes
var paramTypes = methodCreator.marshalAsArray(String[].class,
i.targetMethodInfo.parameterTypes().stream()
.map(pt -> pt.name().toString()).map(methodCreator::load)
.toArray(ResultHandle[]::new));
// new MethodDescription(declaringClassName, methodName, parameterTypes)
var methodDescriptionCtor = MethodDescriptor.ofConstructor(MethodDescription.class,
String.class, String.class, String[].class);
var newMethodDescription = methodCreator.newInstance(methodDescriptionCtor, declaringClassName,
methodName, paramTypes);
// return new MethodDescription(declaringClassName, methodName, parameterTypes);
methodCreator.returnValue(newMethodDescription);
}
}
}
unremovableBeansProducer.produce(UnremovableBeanBuildItem.beanClassNames(className));
reflectiveClassProducer
Expand All @@ -74,9 +112,13 @@ public String generateClass(AccessTokenInstanceBuildItem instance) {
});
}

private String createUniqueClassName(ClientNameAndExchangeToken i) {
return "%s_%sClient_%sTokenExchange".formatted(requestFilterClass.getName(), clientName(i.clientName()),
exchangeTokenName(i.exchangeTokenActivated()));
private String createUniqueClassName(RequestFilterKey i) {
String uniqueClassName = "%s_%sClient_%sTokenExchange".formatted(requestFilterClass.getName(),
clientName(i.clientName()), exchangeTokenName(i.exchangeTokenActivated()));
if (i.targetMethodInfo != null) {
uniqueClassName = uniqueClassName + "_" + i.targetMethodInfo.name();
}
return uniqueClassName;
}

private static String clientName(String clientName) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package io.quarkus.oidc.token.propagation.common.deployment;

import static java.util.stream.Collectors.groupingBy;

import java.util.List;
import java.util.Objects;

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

import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
Expand All @@ -26,12 +31,36 @@ private boolean toExchangeToken() {
return instance.value("exchangeTokenClient") != null;
}

private MethodInfo methodInfo() {
if (instance.target().kind() == AnnotationTarget.Kind.METHOD) {
return instance.target().asMethod();
}
return null;
}

private String targetClassName() {
if (instance.target().kind() == AnnotationTarget.Kind.METHOD) {
return instance.target().asMethod().declaringClass().name().toString();
}
return instance.target().asClass().name().toString();
}

private AccessTokenInstanceBuildItem build() {
return new AccessTokenInstanceBuildItem(toClientName(), toExchangeToken(), instance.target());
return new AccessTokenInstanceBuildItem(toClientName(), toExchangeToken(), instance.target(), methodInfo());
}
}
var accessTokenAnnotations = index.getIndex().getAnnotations(ACCESS_TOKEN);
return accessTokenAnnotations.stream().map(ItemBuilder::new).map(ItemBuilder::build).toList();
var itemBuilders = accessTokenAnnotations.stream().map(ItemBuilder::new).toList();
if (!itemBuilders.isEmpty()) {
var targetClassToBuilders = itemBuilders.stream().collect(groupingBy(ItemBuilder::targetClassName));
targetClassToBuilders.forEach((targetClassName, classBuilders) -> {
if (classBuilders.size() > 1 && classBuilders.stream().map(ItemBuilder::methodInfo).anyMatch(Objects::isNull)) {
throw new RuntimeException(
ACCESS_TOKEN + " annotation can be applied either on class " + targetClassName + " or its methods");
}
});
}
return itemBuilders.stream().map(ItemBuilder::build).toList();
}

}
4 changes: 4 additions & 0 deletions extensions/oidc-token-propagation-common/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security-runtime-spi</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@
* The end result is that the request propagates the Bearer token present in the current active request or the token acquired
* from the Authorization Code Flow,
* as the HTTP {@code Authorization} header's {@code Bearer} scheme value.
* <p>
* This annotation may also be placed on individual methods of the REST Client interface.
* When applied to a method, the {@link AccessTokenRequestFilter} will be registered only for that method.
*/
@Target({ ElementType.TYPE })
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AccessToken {
Expand Down
Loading
Loading