Skip to content

Commit 8e3bb8b

Browse files
authored
Merge pull request #34227 from sberyozkin/test_jwt_custom_claim
Support custom claim types in quarkus-test-security-jwt and quarkus-test-security-oidc
2 parents ae7a5cf + df3ddc0 commit 8e3bb8b

File tree

11 files changed

+578
-187
lines changed

11 files changed

+578
-187
lines changed

test-framework/security-jwt/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@
2828
<artifactId>junit-jupiter</artifactId>
2929
<scope>compile</scope>
3030
</dependency>
31+
<dependency>
32+
<groupId>io.quarkus</groupId>
33+
<artifactId>quarkus-jsonp</artifactId>
34+
<scope>compile</scope>
35+
<optional>true</optional>
36+
</dependency>
3137
<dependency>
3238
<groupId>org.eclipse.microprofile.jwt</groupId>
3339
<artifactId>microprofile-jwt-auth-api</artifactId>

test-framework/security-jwt/src/main/java/io/quarkus/test/security/jwt/Claim.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,21 @@
77
@Retention(RetentionPolicy.RUNTIME)
88
@Target({})
99
public @interface Claim {
10+
/**
11+
* Claim name
12+
*/
1013
String key();
1114

15+
/**
16+
* Claim value
17+
*/
1218
String value();
19+
20+
/**
21+
* Claim value type.
22+
* If this type is set to {@link ClaimType#DEFAULT} then the value will be converted to String unless the claim
23+
* is a standard claim such as `exp` (expiry), `iat` (issued at), `nbf` (not before), `auth_time` (authentication time)
24+
* whose value will be converted to Long or `email_verified` whose value will be converted to Boolean.
25+
*/
26+
ClaimType type() default ClaimType.DEFAULT;
1327
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package io.quarkus.test.security.jwt;
2+
3+
import java.io.StringReader;
4+
5+
import jakarta.json.Json;
6+
import jakarta.json.JsonArray;
7+
import jakarta.json.JsonObject;
8+
import jakarta.json.JsonReader;
9+
10+
public enum ClaimType {
11+
LONG {
12+
@Override
13+
public Long convert(String value) {
14+
return Long.parseLong(value);
15+
}
16+
},
17+
INTEGER {
18+
@Override
19+
public Integer convert(String value) {
20+
return Integer.parseInt(value);
21+
}
22+
},
23+
BOOLEAN {
24+
@Override
25+
public Boolean convert(String value) {
26+
return Boolean.parseBoolean(value);
27+
}
28+
},
29+
STRING {
30+
@Override
31+
public String convert(String value) {
32+
return value;
33+
}
34+
},
35+
JSON_ARRAY {
36+
@Override
37+
public JsonArray convert(String value) {
38+
try (JsonReader jsonReader = Json.createReader(new StringReader(value))) {
39+
return jsonReader.readArray();
40+
}
41+
}
42+
},
43+
JSON_OBJECT {
44+
@Override
45+
public JsonObject convert(String value) {
46+
try (JsonReader jsonReader = Json.createReader(new StringReader(value))) {
47+
return jsonReader.readObject();
48+
}
49+
}
50+
},
51+
DEFAULT {
52+
@Override
53+
public String convert(String value) {
54+
return value;
55+
}
56+
};
57+
58+
abstract Object convert(String value);
59+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package io.quarkus.test.security.jwt;
2+
3+
import java.lang.annotation.Annotation;
4+
import java.util.Arrays;
5+
import java.util.Collections;
6+
import java.util.Map;
7+
import java.util.Set;
8+
import java.util.stream.Collectors;
9+
10+
import jakarta.json.Json;
11+
import jakarta.json.JsonValue;
12+
13+
import org.eclipse.microprofile.jwt.Claims;
14+
import org.eclipse.microprofile.jwt.JsonWebToken;
15+
16+
import io.quarkus.security.identity.SecurityIdentity;
17+
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
18+
import io.quarkus.test.security.TestSecurityIdentityAugmentor;
19+
20+
public class JwtTestSecurityIdentityAugmentor implements TestSecurityIdentityAugmentor {
21+
private static Map<String, ClaimType> standardClaimTypes = Map.of(
22+
Claims.exp.name(), ClaimType.LONG,
23+
Claims.iat.name(), ClaimType.LONG,
24+
Claims.nbf.name(), ClaimType.LONG,
25+
Claims.auth_time.name(), ClaimType.LONG,
26+
Claims.email_verified.name(), ClaimType.BOOLEAN);
27+
28+
@Override
29+
public SecurityIdentity augment(final SecurityIdentity identity, final Annotation[] annotations) {
30+
QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity);
31+
32+
final JwtSecurity jwtSecurity = findJwtSecurity(annotations);
33+
builder.setPrincipal(new JsonWebToken() {
34+
35+
@Override
36+
public String getName() {
37+
return identity.getPrincipal().getName();
38+
}
39+
40+
@SuppressWarnings("unchecked")
41+
@Override
42+
public <T> T getClaim(String claimName) {
43+
if (Claims.groups.name().equals(claimName)) {
44+
return (T) identity.getRoles();
45+
}
46+
if (jwtSecurity != null && jwtSecurity.claims() != null) {
47+
for (Claim claim : jwtSecurity.claims()) {
48+
if (claim.key().equals(claimName)) {
49+
return (T) wrapValue(claim, convertClaimValue(claim));
50+
}
51+
}
52+
}
53+
return null;
54+
}
55+
56+
@Override
57+
public Set<String> getClaimNames() {
58+
if (jwtSecurity != null && jwtSecurity.claims() != null) {
59+
return Arrays.stream(jwtSecurity.claims()).map(Claim::key).collect(Collectors.toSet());
60+
}
61+
return Collections.emptySet();
62+
}
63+
64+
});
65+
66+
return builder.build();
67+
}
68+
69+
private static JwtSecurity findJwtSecurity(Annotation[] annotations) {
70+
for (Annotation ann : annotations) {
71+
if (ann instanceof JwtSecurity) {
72+
return (JwtSecurity) ann;
73+
}
74+
}
75+
return null;
76+
}
77+
78+
private Object wrapValue(Claim claim, Object convertedClaimValue) {
79+
Claims claimType = getClaimType(claim.key());
80+
if (Claims.UNKNOWN == claimType) {
81+
if (convertedClaimValue instanceof Long) {
82+
return Json.createValue((Long) convertedClaimValue);
83+
} else if (convertedClaimValue instanceof Integer) {
84+
return Json.createValue((Integer) convertedClaimValue);
85+
} else if (convertedClaimValue instanceof Boolean) {
86+
return (Boolean) convertedClaimValue ? JsonValue.TRUE : JsonValue.FALSE;
87+
}
88+
}
89+
return convertedClaimValue;
90+
}
91+
92+
protected Claims getClaimType(String claimName) {
93+
Claims claimType;
94+
try {
95+
claimType = Claims.valueOf(claimName);
96+
} catch (IllegalArgumentException e) {
97+
claimType = Claims.UNKNOWN;
98+
}
99+
return claimType;
100+
}
101+
102+
private Object convertClaimValue(Claim claim) {
103+
ClaimType type = claim.type();
104+
if (type == ClaimType.DEFAULT && standardClaimTypes.containsKey(claim.key())) {
105+
type = standardClaimTypes.get(claim.key());
106+
}
107+
return type.convert(claim.value());
108+
}
109+
110+
}
Lines changed: 0 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,9 @@
11
package io.quarkus.test.security.jwt;
22

3-
import java.lang.annotation.Annotation;
4-
import java.util.Arrays;
5-
import java.util.Collections;
6-
import java.util.Set;
7-
import java.util.stream.Collectors;
8-
93
import jakarta.enterprise.context.ApplicationScoped;
104
import jakarta.enterprise.inject.Produces;
115

12-
import org.eclipse.microprofile.jwt.Claims;
13-
import org.eclipse.microprofile.jwt.JsonWebToken;
14-
156
import io.quarkus.arc.Unremovable;
16-
import io.quarkus.security.identity.SecurityIdentity;
17-
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
187
import io.quarkus.test.security.TestSecurityIdentityAugmentor;
198

209
@ApplicationScoped
@@ -25,57 +14,4 @@ public class JwtTestSecurityIdentityAugmentorProducer {
2514
public TestSecurityIdentityAugmentor produce() {
2615
return new JwtTestSecurityIdentityAugmentor();
2716
}
28-
29-
private static class JwtTestSecurityIdentityAugmentor implements TestSecurityIdentityAugmentor {
30-
31-
@Override
32-
public SecurityIdentity augment(final SecurityIdentity identity, final Annotation[] annotations) {
33-
QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity);
34-
35-
final JwtSecurity jwtSecurity = findJwtSecurity(annotations);
36-
builder.setPrincipal(new JsonWebToken() {
37-
38-
@Override
39-
public String getName() {
40-
return identity.getPrincipal().getName();
41-
}
42-
43-
@SuppressWarnings("unchecked")
44-
@Override
45-
public <T> T getClaim(String claimName) {
46-
if (Claims.groups.name().equals(claimName)) {
47-
return (T) identity.getRoles();
48-
}
49-
if (jwtSecurity != null && jwtSecurity.claims() != null) {
50-
for (Claim claim : jwtSecurity.claims()) {
51-
if (claim.key().equals(claimName)) {
52-
return (T) claim.value();
53-
}
54-
}
55-
}
56-
return null;
57-
}
58-
59-
@Override
60-
public Set<String> getClaimNames() {
61-
if (jwtSecurity != null && jwtSecurity.claims() != null) {
62-
return Arrays.stream(jwtSecurity.claims()).map(Claim::key).collect(Collectors.toSet());
63-
}
64-
return Collections.emptySet();
65-
}
66-
67-
});
68-
69-
return builder.build();
70-
}
71-
72-
private JwtSecurity findJwtSecurity(Annotation[] annotations) {
73-
for (Annotation ann : annotations) {
74-
if (ann instanceof JwtSecurity) {
75-
return (JwtSecurity) ann;
76-
}
77-
}
78-
return null;
79-
}
80-
}
8117
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package io.quarkus.test.security.jwt;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertTrue;
5+
6+
import java.lang.annotation.Annotation;
7+
import java.security.Principal;
8+
import java.util.Set;
9+
10+
import jakarta.json.JsonArray;
11+
import jakarta.json.JsonNumber;
12+
import jakarta.json.JsonObject;
13+
import jakarta.json.JsonValue;
14+
15+
import org.eclipse.microprofile.jwt.Claims;
16+
import org.eclipse.microprofile.jwt.JsonWebToken;
17+
import org.junit.jupiter.api.Test;
18+
19+
import io.quarkus.security.identity.SecurityIdentity;
20+
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
21+
22+
public class JwtTestSecurityIdentityAugmentorTest {
23+
24+
@Test
25+
@JwtSecurity(claims = {
26+
@Claim(key = "exp", value = "123456789"),
27+
@Claim(key = "iat", value = "123456788"),
28+
@Claim(key = "nbf", value = "123456787"),
29+
@Claim(key = "auth_time", value = "123456786"),
30+
@Claim(key = "customlong", value = "123456785", type = ClaimType.LONG),
31+
@Claim(key = "email", value = "[email protected]"),
32+
@Claim(key = "email_verified", value = "true"),
33+
@Claim(key = "email_checked", value = "false", type = ClaimType.BOOLEAN),
34+
@Claim(key = "jsonarray_claim", value = "[\"1\", \"2\"]", type = ClaimType.JSON_ARRAY),
35+
@Claim(key = "jsonobject_claim", value = "{\"a\":\"1\", \"b\":\"2\"}", type = ClaimType.JSON_OBJECT)
36+
})
37+
public void testClaimValues() throws Exception {
38+
SecurityIdentity identity = QuarkusSecurityIdentity.builder()
39+
.setPrincipal(new Principal() {
40+
@Override
41+
public String getName() {
42+
return "alice";
43+
}
44+
45+
})
46+
.addRole("user")
47+
.build();
48+
49+
JwtTestSecurityIdentityAugmentor augmentor = new JwtTestSecurityIdentityAugmentor();
50+
51+
Annotation[] annotations = JwtTestSecurityIdentityAugmentorTest.class.getMethod("testClaimValues").getAnnotations();
52+
JsonWebToken jwt = (JsonWebToken) augmentor.augment(identity, annotations).getPrincipal();
53+
54+
assertEquals("alice", jwt.getName());
55+
assertEquals(Set.of("user"), jwt.getGroups());
56+
57+
assertEquals(123456789, jwt.getExpirationTime());
58+
assertEquals(123456788, jwt.getIssuedAtTime());
59+
assertEquals(123456787, (Long) jwt.getClaim(Claims.nbf.name()));
60+
assertEquals(123456786, (Long) jwt.getClaim(Claims.auth_time.name()));
61+
assertEquals(123456785, ((JsonNumber) jwt.getClaim("customlong")).longValue());
62+
assertEquals("[email protected]", jwt.getClaim(Claims.email));
63+
assertTrue((Boolean) jwt.getClaim(Claims.email_verified.name()));
64+
assertEquals(JsonValue.FALSE, jwt.getClaim("email_checked"));
65+
66+
JsonArray array = jwt.getClaim("jsonarray_claim");
67+
assertEquals("1", array.getString(0));
68+
assertEquals("2", array.getString(1));
69+
70+
JsonObject map = jwt.getClaim("jsonobject_claim");
71+
assertEquals("1", map.getString("a"));
72+
assertEquals("2", map.getString("b"));
73+
}
74+
75+
}

test-framework/security-oidc/src/main/java/io/quarkus/test/security/oidc/Claim.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,21 @@
77
@Retention(RetentionPolicy.RUNTIME)
88
@Target({})
99
public @interface Claim {
10+
/**
11+
* Claim name
12+
*/
1013
String key();
1114

15+
/**
16+
* Claim value
17+
*/
1218
String value();
19+
20+
/**
21+
* Claim value type.
22+
* If this type is set to {@link ClaimType#DEFAULT} then the value will be converted to String unless the claim
23+
* is a standard claim such as `exp` (expiry), `iat` (issued at), `nbf` (not before), `auth_time` (authentication time)
24+
* whose value will be converted to Long or `email_verified` whose value will be converted to Boolean.
25+
*/
26+
ClaimType type() default ClaimType.DEFAULT;
1327
}

0 commit comments

Comments
 (0)