Skip to content

Commit 2f562bc

Browse files
authored
Merge pull request #50723 from michalvavrik/feature/access-token-for-rest-client-methods
OIDC token propagation - allow users to select REST client methods for which token should be propagated
2 parents 2625fba + d266460 commit 2f562bc

File tree

10 files changed

+395
-15
lines changed

10 files changed

+395
-15
lines changed

docs/src/main/asciidoc/security-openid-connect-client-reference.adoc

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1391,6 +1391,29 @@ public interface ProtectedResourceService {
13911391

13921392
or
13931393

1394+
[source,java]
1395+
----
1396+
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
1397+
import io.quarkus.oidc.token.propagation.common.AccessToken;
1398+
import jakarta.ws.rs.GET;
1399+
import jakarta.ws.rs.Path;
1400+
1401+
@RegisterRestClient
1402+
@Path("/")
1403+
public interface InformationService {
1404+
1405+
@AccessToken
1406+
@GET
1407+
String getUserName();
1408+
1409+
@Path("/public")
1410+
@GET
1411+
String getPublicInformation();
1412+
}
1413+
----
1414+
1415+
or
1416+
13941417
[source,java]
13951418
----
13961419
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
@@ -1482,6 +1505,30 @@ public interface ProtectedResourceService {
14821505
String getUserName();
14831506
}
14841507
----
1508+
1509+
or
1510+
1511+
[source,java]
1512+
----
1513+
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
1514+
import io.quarkus.oidc.token.propagation.common.AccessToken;
1515+
import jakarta.ws.rs.GET;
1516+
import jakarta.ws.rs.Path;
1517+
1518+
@RegisterRestClient
1519+
@Path("/")
1520+
public interface InformationService {
1521+
1522+
@AccessToken
1523+
@GET
1524+
String getUserName();
1525+
1526+
@Path("/public")
1527+
@GET
1528+
String getPublicInformation();
1529+
}
1530+
----
1531+
14851532
or
14861533

14871534
[source,java]

extensions/oidc-token-propagation-common/deployment/src/main/java/io/quarkus/oidc/token/propagation/common/deployment/AccessTokenInstanceBuildItem.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.util.Objects;
44

55
import org.jboss.jandex.AnnotationTarget;
6+
import org.jboss.jandex.MethodInfo;
67

78
import io.quarkus.builder.item.MultiBuildItem;
89

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

18-
AccessTokenInstanceBuildItem(String clientName, Boolean tokenExchange, AnnotationTarget annotationTarget) {
20+
AccessTokenInstanceBuildItem(String clientName, Boolean tokenExchange, AnnotationTarget annotationTarget,
21+
MethodInfo targetMethodInfo) {
1922
this.clientName = Objects.requireNonNull(clientName);
2023
this.tokenExchange = tokenExchange;
2124
this.annotationTarget = Objects.requireNonNull(annotationTarget);
25+
this.targetMethodInfo = targetMethodInfo;
2226
}
2327

24-
public String getClientName() {
28+
String getClientName() {
2529
return clientName;
2630
}
2731

28-
public boolean exchangeTokenActivated() {
32+
boolean exchangeTokenActivated() {
2933
return tokenExchange;
3034
}
3135

@@ -34,6 +38,13 @@ public AnnotationTarget getAnnotationTarget() {
3438
}
3539

3640
public String targetClass() {
37-
return annotationTarget.asClass().name().toString();
41+
if (annotationTarget.kind() == AnnotationTarget.Kind.CLASS) {
42+
return annotationTarget.asClass().name().toString();
43+
}
44+
return annotationTarget.asMethod().declaringClass().name().toString();
45+
}
46+
47+
MethodInfo getTargetMethodInfo() {
48+
return targetMethodInfo;
3849
}
3950
}

extensions/oidc-token-propagation-common/deployment/src/main/java/io/quarkus/oidc/token/propagation/common/deployment/AccessTokenRequestFilterGenerator.java

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,30 @@
88
import jakarta.annotation.Priority;
99
import jakarta.inject.Singleton;
1010

11+
import org.jboss.jandex.MethodInfo;
12+
1113
import io.quarkus.arc.deployment.GeneratedBeanBuildItem;
1214
import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor;
1315
import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
1416
import io.quarkus.deployment.annotations.BuildProducer;
1517
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
1618
import io.quarkus.gizmo.ClassCreator;
19+
import io.quarkus.gizmo.MethodDescriptor;
20+
import io.quarkus.gizmo.ResultHandle;
21+
import io.quarkus.security.spi.runtime.MethodDescription;
1722

1823
public final class AccessTokenRequestFilterGenerator {
1924

2025
private static final int AUTHENTICATION = 1000;
2126

22-
private record ClientNameAndExchangeToken(String clientName, boolean exchangeTokenActivated) {
27+
private record RequestFilterKey(String clientName, boolean exchangeTokenActivated, MethodInfo targetMethodInfo) {
2328
}
2429

2530
private final BuildProducer<UnremovableBeanBuildItem> unremovableBeansProducer;
2631
private final BuildProducer<ReflectiveClassBuildItem> reflectiveClassProducer;
2732
private final BuildProducer<GeneratedBeanBuildItem> generatedBeanProducer;
2833
private final Class<?> requestFilterClass;
29-
private final Map<ClientNameAndExchangeToken, String> cache = new HashMap<>();
34+
private final Map<RequestFilterKey, String> cache = new HashMap<>();
3035

3136
public AccessTokenRequestFilterGenerator(BuildProducer<UnremovableBeanBuildItem> unremovableBeansProducer,
3237
BuildProducer<ReflectiveClassBuildItem> reflectiveClassProducer,
@@ -39,7 +44,9 @@ public AccessTokenRequestFilterGenerator(BuildProducer<UnremovableBeanBuildItem>
3944

4045
public String generateClass(AccessTokenInstanceBuildItem instance) {
4146
return cache.computeIfAbsent(
42-
new ClientNameAndExchangeToken(instance.getClientName(), instance.exchangeTokenActivated()), i -> {
47+
new RequestFilterKey(instance.getClientName(), instance.exchangeTokenActivated(),
48+
instance.getTargetMethodInfo()),
49+
i -> {
4350
var adaptor = new GeneratedBeanGizmoAdaptor(generatedBeanProducer);
4451
String className = createUniqueClassName(i);
4552
try (ClassCreator classCreator = ClassCreator.builder()
@@ -64,6 +71,37 @@ public String generateClass(AccessTokenInstanceBuildItem instance) {
6471
methodCreator.returnBoolean(true);
6572
}
6673
}
74+
75+
/*
76+
* protected MethodDescription getMethodDescription() {
77+
* return new MethodDescription(declaringClassName, methodName, parameterTypes);
78+
* }
79+
*/
80+
if (i.targetMethodInfo != null) {
81+
try (var methodCreator = classCreator.getMethodCreator("getMethodDescription",
82+
MethodDescription.class)) {
83+
methodCreator.addAnnotation(Override.class.getName(), RetentionPolicy.CLASS);
84+
methodCreator.setModifiers(Modifier.PROTECTED);
85+
86+
// String methodName
87+
var methodName = methodCreator.load(i.targetMethodInfo.name());
88+
// String declaringClassName
89+
var declaringClassName = methodCreator
90+
.load(i.targetMethodInfo.declaringClass().name().toString());
91+
// String[] paramTypes
92+
var paramTypes = methodCreator.marshalAsArray(String[].class,
93+
i.targetMethodInfo.parameterTypes().stream()
94+
.map(pt -> pt.name().toString()).map(methodCreator::load)
95+
.toArray(ResultHandle[]::new));
96+
// new MethodDescription(declaringClassName, methodName, parameterTypes)
97+
var methodDescriptionCtor = MethodDescriptor.ofConstructor(MethodDescription.class,
98+
String.class, String.class, String[].class);
99+
var newMethodDescription = methodCreator.newInstance(methodDescriptionCtor, declaringClassName,
100+
methodName, paramTypes);
101+
// return new MethodDescription(declaringClassName, methodName, parameterTypes);
102+
methodCreator.returnValue(newMethodDescription);
103+
}
104+
}
67105
}
68106
unremovableBeansProducer.produce(UnremovableBeanBuildItem.beanClassNames(className));
69107
reflectiveClassProducer
@@ -74,9 +112,13 @@ public String generateClass(AccessTokenInstanceBuildItem instance) {
74112
});
75113
}
76114

77-
private String createUniqueClassName(ClientNameAndExchangeToken i) {
78-
return "%s_%sClient_%sTokenExchange".formatted(requestFilterClass.getName(), clientName(i.clientName()),
79-
exchangeTokenName(i.exchangeTokenActivated()));
115+
private String createUniqueClassName(RequestFilterKey i) {
116+
String uniqueClassName = "%s_%sClient_%sTokenExchange".formatted(requestFilterClass.getName(),
117+
clientName(i.clientName()), exchangeTokenName(i.exchangeTokenActivated()));
118+
if (i.targetMethodInfo != null) {
119+
uniqueClassName = uniqueClassName + "_" + i.targetMethodInfo.name();
120+
}
121+
return uniqueClassName;
80122
}
81123

82124
private static String clientName(String clientName) {

extensions/oidc-token-propagation-common/deployment/src/main/java/io/quarkus/oidc/token/propagation/common/deployment/OidcTokenPropagationCommonProcessor.java

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
package io.quarkus.oidc.token.propagation.common.deployment;
22

3+
import static java.util.stream.Collectors.groupingBy;
4+
35
import java.util.List;
6+
import java.util.Objects;
47

58
import org.jboss.jandex.AnnotationInstance;
9+
import org.jboss.jandex.AnnotationTarget;
610
import org.jboss.jandex.DotName;
11+
import org.jboss.jandex.MethodInfo;
712

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

34+
private MethodInfo methodInfo() {
35+
if (instance.target().kind() == AnnotationTarget.Kind.METHOD) {
36+
return instance.target().asMethod();
37+
}
38+
return null;
39+
}
40+
41+
private String targetClassName() {
42+
if (instance.target().kind() == AnnotationTarget.Kind.METHOD) {
43+
return instance.target().asMethod().declaringClass().name().toString();
44+
}
45+
return instance.target().asClass().name().toString();
46+
}
47+
2948
private AccessTokenInstanceBuildItem build() {
30-
return new AccessTokenInstanceBuildItem(toClientName(), toExchangeToken(), instance.target());
49+
return new AccessTokenInstanceBuildItem(toClientName(), toExchangeToken(), instance.target(), methodInfo());
3150
}
3251
}
3352
var accessTokenAnnotations = index.getIndex().getAnnotations(ACCESS_TOKEN);
34-
return accessTokenAnnotations.stream().map(ItemBuilder::new).map(ItemBuilder::build).toList();
53+
var itemBuilders = accessTokenAnnotations.stream().map(ItemBuilder::new).toList();
54+
if (!itemBuilders.isEmpty()) {
55+
var targetClassToBuilders = itemBuilders.stream().collect(groupingBy(ItemBuilder::targetClassName));
56+
targetClassToBuilders.forEach((targetClassName, classBuilders) -> {
57+
if (classBuilders.size() > 1 && classBuilders.stream().map(ItemBuilder::methodInfo).anyMatch(Objects::isNull)) {
58+
throw new RuntimeException(
59+
ACCESS_TOKEN + " annotation can be applied either on class " + targetClassName + " or its methods");
60+
}
61+
});
62+
}
63+
return itemBuilders.stream().map(ItemBuilder::build).toList();
3564
}
3665

3766
}

extensions/oidc-token-propagation-common/runtime/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
<groupId>io.quarkus</groupId>
2323
<artifactId>quarkus-arc</artifactId>
2424
</dependency>
25+
<dependency>
26+
<groupId>io.quarkus</groupId>
27+
<artifactId>quarkus-security-runtime-spi</artifactId>
28+
</dependency>
2529
</dependencies>
2630

2731
<build>

extensions/oidc-token-propagation-common/runtime/src/main/java/io/quarkus/oidc/token/propagation/common/AccessToken.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@
1212
* The end result is that the request propagates the Bearer token present in the current active request or the token acquired
1313
* from the Authorization Code Flow,
1414
* as the HTTP {@code Authorization} header's {@code Bearer} scheme value.
15+
* <p>
16+
* This annotation may also be placed on individual methods of the REST Client interface.
17+
* When applied to a method, the {@link AccessTokenRequestFilter} will be registered only for that method.
1518
*/
16-
@Target({ ElementType.TYPE })
19+
@Target({ ElementType.TYPE, ElementType.METHOD })
1720
@Retention(RetentionPolicy.RUNTIME)
1821
@Documented
1922
public @interface AccessToken {

0 commit comments

Comments
 (0)