Skip to content

Commit ebe4832

Browse files
authored
Merge pull request quarkusio#36183 from sberyozkin/jwt_jsonstr_singleton
Fail smallrye-jwt build if @claim types are injected without ClaimValue in ApplicationScoped beans
2 parents 089bd28 + 839712d commit ebe4832

File tree

9 files changed

+217
-22
lines changed

9 files changed

+217
-22
lines changed

extensions/smallrye-jwt/deployment/src/main/java/io/quarkus/smallrye/jwt/deployment/SmallRyeJwtProcessor.java

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
import java.util.Set;
66
import java.util.function.BooleanSupplier;
77

8+
import jakarta.enterprise.context.RequestScoped;
9+
810
import org.eclipse.microprofile.config.Config;
911
import org.eclipse.microprofile.config.ConfigProvider;
1012
import org.eclipse.microprofile.jwt.Claim;
13+
import org.eclipse.microprofile.jwt.ClaimValue;
1114
import org.eclipse.microprofile.jwt.Claims;
1215
import org.jboss.jandex.AnnotationInstance;
1316
import org.jboss.jandex.AnnotationValue;
@@ -19,6 +22,7 @@
1922
import io.quarkus.arc.deployment.BeanRegistrationPhaseBuildItem;
2023
import io.quarkus.arc.deployment.BeanRegistrationPhaseBuildItem.BeanConfiguratorBuildItem;
2124
import io.quarkus.arc.processor.BeanConfigurator;
25+
import io.quarkus.arc.processor.BeanInfo;
2226
import io.quarkus.arc.processor.BuildExtension;
2327
import io.quarkus.arc.processor.BuiltinScope;
2428
import io.quarkus.arc.processor.DotNames;
@@ -61,6 +65,12 @@ class SmallRyeJwtProcessor {
6165
private static final DotName CLAIM_NAME = DotName.createSimple(Claim.class.getName());
6266
private static final DotName CLAIMS_NAME = DotName.createSimple(Claims.class.getName());
6367

68+
private static final DotName CLAIM_VALUE_NAME = DotName.createSimple(ClaimValue.class);
69+
private static final DotName REQUEST_SCOPED_NAME = DotName.createSimple(RequestScoped.class);
70+
71+
private static final Set<DotName> ALL_PROVIDER_NAMES = Set.of(DotNames.PROVIDER, DotNames.INSTANCE,
72+
DotNames.INJECTABLE_INSTANCE);
73+
6474
SmallRyeJwtBuildTimeConfig config;
6575

6676
@BuildStep(onlyIf = IsEnabled.class)
@@ -161,14 +171,29 @@ void registerOptionalClaimProducer(BeanRegistrationPhaseBuildItem beanRegistrati
161171
continue;
162172
}
163173
AnnotationInstance claimQualifier = injectionPoint.getRequiredQualifier(CLAIM_NAME);
164-
if (claimQualifier != null && injectionPoint.getType().name().equals(DotNames.PROVIDER)) {
165-
// Classes from jakarta.json are handled specially
174+
if (claimQualifier != null) {
166175
Type actualType = injectionPoint.getRequiredType();
167-
if (actualType.name().equals(DotNames.OPTIONAL) && !actualType.name().toString()
168-
.startsWith("jakarta.json")) {
176+
177+
Optional<BeanInfo> bean = injectionPoint.getTargetBean();
178+
if (bean.isPresent()) {
179+
DotName scope = bean.get().getScope().getDotName();
180+
if (!REQUEST_SCOPED_NAME.equals(scope)
181+
&& (!ALL_PROVIDER_NAMES.contains(injectionPoint.getType().name())
182+
&& !CLAIM_VALUE_NAME.equals(actualType.name()))) {
183+
String error = String.format(
184+
"%s type can not be used to represent JWT claims in @Singleton or @ApplicationScoped beans"
185+
+ ", make the bean @RequestScoped or wrap this type with org.eclipse.microprofile.jwt.ClaimValue"
186+
+ " or jakarta.inject.Provider or jakarta.enterprise.inject.Instance",
187+
actualType.name());
188+
throw new IllegalStateException(error);
189+
}
190+
}
191+
192+
if (injectionPoint.getType().name().equals(DotNames.PROVIDER) && actualType.name().equals(DotNames.OPTIONAL)) {
169193
additionalTypes.add(actualType);
170194
}
171195
}
196+
172197
}
173198

174199
// Register a custom bean
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package io.quarkus.jwt.test;
2+
3+
import java.util.Set;
4+
import java.util.stream.Collectors;
5+
6+
import jakarta.enterprise.inject.Instance;
7+
import jakarta.inject.Inject;
8+
import jakarta.inject.Provider;
9+
import jakarta.json.JsonString;
10+
import jakarta.ws.rs.GET;
11+
import jakarta.ws.rs.Path;
12+
import jakarta.ws.rs.Produces;
13+
import jakarta.ws.rs.core.MediaType;
14+
15+
import org.eclipse.microprofile.jwt.Claim;
16+
import org.eclipse.microprofile.jwt.ClaimValue;
17+
import org.eclipse.microprofile.jwt.Claims;
18+
19+
import io.quarkus.security.Authenticated;
20+
21+
@Path("/endp")
22+
@Authenticated
23+
public class ClaimsSingletonEndpoint {
24+
@Inject
25+
@Claim(standard = Claims.upn)
26+
ClaimValue<JsonString> upn;
27+
@Inject
28+
@Claim(standard = Claims.raw_token)
29+
Instance<String> rawToken;
30+
@Inject
31+
@Claim(standard = Claims.groups)
32+
Provider<Set<String>> groups;
33+
34+
@GET
35+
@Path("claims")
36+
@Produces(MediaType.TEXT_PLAIN)
37+
public String verifyGroups() {
38+
return upn.getValue().getString() + ":" + groups.get().stream().collect(Collectors.joining(",")) + ":" + rawToken.get();
39+
}
40+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package io.quarkus.jwt.test;
2+
3+
import static org.hamcrest.Matchers.equalTo;
4+
5+
import org.jboss.shrinkwrap.api.ShrinkWrap;
6+
import org.jboss.shrinkwrap.api.spec.JavaArchive;
7+
import org.junit.jupiter.api.Test;
8+
import org.junit.jupiter.api.extension.RegisterExtension;
9+
10+
import io.quarkus.test.QuarkusUnitTest;
11+
import io.restassured.RestAssured;
12+
import io.smallrye.jwt.build.Jwt;
13+
14+
public class ClaimsSingletonTest {
15+
16+
@RegisterExtension
17+
static final QuarkusUnitTest config = new QuarkusUnitTest()
18+
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
19+
.addClass(ClaimsSingletonEndpoint.class)
20+
.addAsResource("jwtPublicKey.pem")
21+
.addAsResource("jwtPrivateKey.pem")
22+
.addAsResource("applicationJwtSign.properties", "application.properties"));
23+
24+
@Test
25+
public void verify() throws Exception {
26+
String token1 = generateToken("alice", "user");
27+
RestAssured.given().auth()
28+
.oauth2(token1)
29+
.when().get("/endp/claims")
30+
.then()
31+
.statusCode(200).body(equalTo("alice:user:" + token1));
32+
String token2 = generateToken("bob", "admin");
33+
RestAssured.given().auth()
34+
.oauth2(token2)
35+
.when().get("/endp/claims")
36+
.then()
37+
.statusCode(200).body(equalTo("bob:admin:" + token2));
38+
}
39+
40+
private String generateToken(String upn, String role) {
41+
return Jwt.upn(upn).issuer("immo-jwt").groups(role).sign();
42+
}
43+
}

extensions/smallrye-jwt/deployment/src/test/java/io/quarkus/jwt/test/DefaultScopedEndpoint.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package io.quarkus.jwt.test;
22

3-
import java.util.Optional;
4-
53
import jakarta.annotation.security.RolesAllowed;
64
import jakarta.inject.Inject;
75
import jakarta.json.Json;
@@ -16,6 +14,7 @@
1614
import jakarta.ws.rs.core.SecurityContext;
1715

1816
import org.eclipse.microprofile.jwt.Claim;
17+
import org.eclipse.microprofile.jwt.ClaimValue;
1918
import org.eclipse.microprofile.jwt.Claims;
2019

2120
/**
@@ -25,7 +24,7 @@
2524
public class DefaultScopedEndpoint {
2625
@Inject
2726
@Claim(standard = Claims.preferred_username)
28-
Optional<JsonString> currentUsername;
27+
ClaimValue<JsonString> currentUsername;
2928
@Context
3029
private SecurityContext context;
3130

@@ -42,13 +41,13 @@ public class DefaultScopedEndpoint {
4241
public JsonObject validateUsername(@QueryParam("username") String username) {
4342
boolean pass = false;
4443
String msg;
45-
if (!currentUsername.isPresent()) {
44+
if (currentUsername.getValue() == null) {
4645
msg = "Injected preferred_username value is null, FAIL";
47-
} else if (currentUsername.get().getString().equals(username)) {
46+
} else if (currentUsername.getValue().getString().equals(username)) {
4847
msg = "\nInjected Principal#getName matches, PASS";
4948
pass = true;
5049
} else {
51-
msg = String.format("Injected preferred_username %s != %s, FAIL", currentUsername.get().getString(), username);
50+
msg = String.format("Injected preferred_username %s != %s, FAIL", currentUsername.getValue().getString(), username);
5251
}
5352

5453
JsonObject result = Json.createObjectBuilder()
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package io.quarkus.jwt.test;
2+
3+
import static org.junit.jupiter.api.Assertions.assertTrue;
4+
5+
import org.junit.jupiter.api.Assertions;
6+
import org.junit.jupiter.api.Test;
7+
import org.junit.jupiter.api.extension.RegisterExtension;
8+
9+
import io.quarkus.test.QuarkusUnitTest;
10+
11+
public class OptionalTypeApplicationScopedBeanTest {
12+
13+
@RegisterExtension
14+
static final QuarkusUnitTest test = new QuarkusUnitTest()
15+
.withApplicationRoot((jar) -> jar
16+
.addClass(OptionalTypeApplicationScopedEndpoint.class))
17+
.assertException(t -> {
18+
assertTrue(t.getMessage().startsWith(
19+
"java.util.Optional type can not be used to represent JWT claims in @Singleton or @ApplicationScoped beans, make the bean @RequestScoped"
20+
+ " or wrap this type with"));
21+
});
22+
23+
@Test
24+
public void test() {
25+
Assertions.fail();
26+
}
27+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package io.quarkus.jwt.test;
2+
3+
import java.util.Optional;
4+
5+
import jakarta.enterprise.context.ApplicationScoped;
6+
import jakarta.inject.Inject;
7+
import jakarta.json.JsonString;
8+
import jakarta.ws.rs.GET;
9+
10+
import org.eclipse.microprofile.jwt.Claim;
11+
import org.eclipse.microprofile.jwt.Claims;
12+
13+
@ApplicationScoped
14+
public class OptionalTypeApplicationScopedEndpoint {
15+
@Inject
16+
@Claim(standard = Claims.upn)
17+
Optional<JsonString> upn;
18+
19+
@GET
20+
public String get() {
21+
return "hello";
22+
}
23+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package io.quarkus.jwt.test;
2+
3+
import static org.junit.jupiter.api.Assertions.assertTrue;
4+
5+
import org.junit.jupiter.api.Assertions;
6+
import org.junit.jupiter.api.Test;
7+
import org.junit.jupiter.api.extension.RegisterExtension;
8+
9+
import io.quarkus.test.QuarkusUnitTest;
10+
11+
public class OptionalTypeSingletonBeanTest {
12+
13+
@RegisterExtension
14+
static final QuarkusUnitTest test = new QuarkusUnitTest()
15+
.withApplicationRoot((jar) -> jar
16+
.addClass(OptionalTypeSingletonEndpoint.class))
17+
.assertException(t -> {
18+
assertTrue(t.getMessage().startsWith(
19+
"java.util.Optional type can not be used to represent JWT claims in @Singleton or @ApplicationScoped beans, make the bean @RequestScoped"
20+
+ " or wrap this type with"));
21+
});
22+
23+
@Test
24+
public void test() {
25+
Assertions.fail();
26+
}
27+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package io.quarkus.jwt.test;
2+
3+
import java.util.Optional;
4+
5+
import jakarta.inject.Inject;
6+
import jakarta.json.JsonString;
7+
import jakarta.ws.rs.GET;
8+
import jakarta.ws.rs.Path;
9+
10+
import org.eclipse.microprofile.jwt.Claim;
11+
import org.eclipse.microprofile.jwt.Claims;
12+
13+
@Path("/endpoint")
14+
public class OptionalTypeSingletonEndpoint {
15+
@Inject
16+
@Claim(standard = Claims.upn)
17+
Optional<JsonString> upn;
18+
19+
@GET
20+
public String get() {
21+
return "hello";
22+
}
23+
}

extensions/smallrye-jwt/deployment/src/test/java/io/quarkus/jwt/test/ScopingUnitTest.java

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,18 +48,6 @@ public void verifyUsernameClaim() throws Exception {
4848
Assertions.assertTrue(reply.getBoolean("pass"), reply.getString("msg"));
4949

5050
String token2 = TokenUtils.generateTokenString("/Token2.json");
51-
Response response2 = RestAssured.given().auth()
52-
.oauth2(token2)
53-
.when()
54-
// We expect the injected preferred_username claim to still be jdoe due to default scope = @ApplicationScoped
55-
.queryParam("username", "jdoe")
56-
.get("/endp-defaultscoped/validateUsername").andReturn();
57-
58-
Assertions.assertEquals(HttpURLConnection.HTTP_OK, response2.getStatusCode());
59-
String replyString2 = response2.body().asString();
60-
JsonReader jsonReader2 = Json.createReader(new StringReader(replyString2));
61-
JsonObject reply2 = jsonReader2.readObject();
62-
Assertions.assertTrue(reply2.getBoolean("pass"), reply2.getString("msg"));
6351

6452
Response response3 = RestAssured.given().auth()
6553
.oauth2(token)

0 commit comments

Comments
 (0)