Skip to content

Commit e31bb9a

Browse files
committed
Support custom claim types in quarkus-test-security-oidc
1 parent 18a01a9 commit e31bb9a

File tree

4 files changed

+328
-123
lines changed

4 files changed

+328
-123
lines changed

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,19 @@
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, the value will be converted to String if this type is set to Object.class.
22+
* Supported types: String, Integer, int, Long, long, Boolean, boolean, jakarta.json.JsonArray, jakarta.json.JsonObject.
23+
*/
24+
Class<?> type() default Object.class;
1325
}
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
package io.quarkus.test.security.oidc;
2+
3+
import java.io.StringReader;
4+
import java.lang.annotation.Annotation;
5+
import java.security.NoSuchAlgorithmException;
6+
import java.security.PrivateKey;
7+
import java.util.Map;
8+
import java.util.Optional;
9+
import java.util.UUID;
10+
import java.util.stream.Collectors;
11+
12+
import jakarta.json.Json;
13+
import jakarta.json.JsonArray;
14+
import jakarta.json.JsonObjectBuilder;
15+
import jakarta.json.JsonReader;
16+
17+
import org.eclipse.microprofile.jwt.Claims;
18+
import org.eclipse.microprofile.jwt.JsonWebToken;
19+
import org.jose4j.jwt.JwtClaims;
20+
21+
import io.quarkus.oidc.AccessTokenCredential;
22+
import io.quarkus.oidc.IdTokenCredential;
23+
import io.quarkus.oidc.OidcConfigurationMetadata;
24+
import io.quarkus.oidc.common.runtime.OidcConstants;
25+
import io.quarkus.oidc.runtime.OidcJwtCallerPrincipal;
26+
import io.quarkus.oidc.runtime.OidcUtils;
27+
import io.quarkus.security.identity.SecurityIdentity;
28+
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
29+
import io.quarkus.test.security.TestSecurityIdentityAugmentor;
30+
import io.smallrye.jwt.build.Jwt;
31+
import io.smallrye.jwt.util.KeyUtils;
32+
import io.vertx.core.json.JsonObject;
33+
34+
public class OidcTestSecurityIdentityAugmentor implements TestSecurityIdentityAugmentor {
35+
36+
private static Converter<Long> longConverter = new LongConverter();
37+
private static Converter<Integer> intConverter = new IntegerConverter();
38+
private static Converter<Boolean> booleanConverter = new BooleanConverter();
39+
private static Map<String, Converter<?>> standardClaimConverteres = Map.of(
40+
Claims.exp.name(), longConverter,
41+
Claims.iat.name(), longConverter,
42+
Claims.nbf.name(), longConverter,
43+
Claims.auth_time.name(), longConverter,
44+
Claims.email_verified.name(), booleanConverter);
45+
46+
private static Map<Class<?>, Converter<?>> converters = Map.of(
47+
String.class, new StringConverter(),
48+
Integer.class, intConverter,
49+
int.class, intConverter,
50+
Long.class, longConverter,
51+
long.class, longConverter,
52+
Boolean.class, booleanConverter,
53+
boolean.class, booleanConverter,
54+
JsonArray.class, new JsonArrayConverter(),
55+
jakarta.json.JsonObject.class, new JsonObjectConverter());
56+
57+
private Optional<String> issuer;
58+
private PrivateKey privateKey;
59+
60+
public OidcTestSecurityIdentityAugmentor(Optional<String> issuer) {
61+
this.issuer = issuer;
62+
try {
63+
privateKey = KeyUtils.generateKeyPair(2048).getPrivate();
64+
} catch (NoSuchAlgorithmException ex) {
65+
throw new RuntimeException(ex);
66+
}
67+
}
68+
69+
@Override
70+
public SecurityIdentity augment(final SecurityIdentity identity, final Annotation[] annotations) {
71+
QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity);
72+
73+
final OidcSecurity oidcSecurity = findOidcSecurity(annotations);
74+
75+
final boolean introspectionRequired = oidcSecurity != null && oidcSecurity.introspectionRequired();
76+
77+
if (!introspectionRequired) {
78+
// JsonWebToken
79+
JsonObjectBuilder claims = Json.createObjectBuilder();
80+
claims.add(Claims.preferred_username.name(), identity.getPrincipal().getName());
81+
claims.add(Claims.groups.name(),
82+
Json.createArrayBuilder(identity.getRoles().stream().collect(Collectors.toList())).build());
83+
if (oidcSecurity != null && oidcSecurity.claims() != null) {
84+
for (Claim claim : oidcSecurity.claims()) {
85+
Object claimValue = convertClaimValue(claim);
86+
if (claimValue instanceof String) {
87+
claims.add(claim.key(), (String) claimValue);
88+
} else if (claimValue instanceof Long) {
89+
claims.add(claim.key(), (Long) claimValue);
90+
} else if (claimValue instanceof Integer) {
91+
claims.add(claim.key(), (Integer) claimValue);
92+
} else if (claimValue instanceof Boolean) {
93+
claims.add(claim.key(), (Boolean) claimValue);
94+
} else if (claimValue instanceof JsonArray) {
95+
claims.add(claim.key(), (JsonArray) claimValue);
96+
} else if (claimValue instanceof jakarta.json.JsonObject) {
97+
claims.add(claim.key(), (jakarta.json.JsonObject) claimValue);
98+
}
99+
}
100+
}
101+
jakarta.json.JsonObject claimsJson = claims.build();
102+
String jwt = generateToken(claimsJson);
103+
IdTokenCredential idToken = new IdTokenCredential(jwt);
104+
AccessTokenCredential accessToken = new AccessTokenCredential(jwt);
105+
106+
try {
107+
JsonWebToken principal = new OidcJwtCallerPrincipal(JwtClaims.parse(claimsJson.toString()), idToken);
108+
builder.setPrincipal(principal);
109+
} catch (Exception ex) {
110+
throw new RuntimeException();
111+
}
112+
builder.addCredential(idToken);
113+
builder.addCredential(accessToken);
114+
} else {
115+
JsonObjectBuilder introspectionBuilder = Json.createObjectBuilder();
116+
introspectionBuilder.add(OidcConstants.INTROSPECTION_TOKEN_ACTIVE, true);
117+
introspectionBuilder.add(OidcConstants.INTROSPECTION_TOKEN_USERNAME, identity.getPrincipal().getName());
118+
introspectionBuilder.add(OidcConstants.TOKEN_SCOPE,
119+
identity.getRoles().stream().collect(Collectors.joining(" ")));
120+
121+
if (oidcSecurity != null && oidcSecurity.introspection() != null) {
122+
for (TokenIntrospection introspection : oidcSecurity.introspection()) {
123+
introspectionBuilder.add(introspection.key(), introspection.value());
124+
}
125+
}
126+
127+
builder.addAttribute(OidcUtils.INTROSPECTION_ATTRIBUTE,
128+
new io.quarkus.oidc.TokenIntrospection(introspectionBuilder.build()));
129+
builder.addCredential(new AccessTokenCredential(UUID.randomUUID().toString(), null));
130+
}
131+
132+
// UserInfo
133+
if (oidcSecurity != null && oidcSecurity.userinfo() != null) {
134+
JsonObjectBuilder userInfoBuilder = Json.createObjectBuilder();
135+
for (UserInfo userinfo : oidcSecurity.userinfo()) {
136+
userInfoBuilder.add(userinfo.key(), userinfo.value());
137+
}
138+
builder.addAttribute(OidcUtils.USER_INFO_ATTRIBUTE, new io.quarkus.oidc.UserInfo(userInfoBuilder.build()));
139+
}
140+
141+
// OidcConfigurationMetadata
142+
JsonObject configMetadataBuilder = new JsonObject();
143+
if (issuer.isPresent()) {
144+
configMetadataBuilder.put("issuer", issuer.get());
145+
}
146+
if (oidcSecurity != null && oidcSecurity.config() != null) {
147+
for (ConfigMetadata config : oidcSecurity.config()) {
148+
configMetadataBuilder.put(config.key(), config.value());
149+
}
150+
}
151+
builder.addAttribute(OidcUtils.CONFIG_METADATA_ATTRIBUTE, new OidcConfigurationMetadata(configMetadataBuilder));
152+
153+
return builder.build();
154+
}
155+
156+
private String generateToken(jakarta.json.JsonObject claims) {
157+
try {
158+
return Jwt.claims(claims).sign(privateKey);
159+
} catch (Exception ex) {
160+
throw new RuntimeException(ex);
161+
}
162+
}
163+
164+
private static OidcSecurity findOidcSecurity(Annotation[] annotations) {
165+
for (Annotation ann : annotations) {
166+
if (ann instanceof OidcSecurity) {
167+
return (OidcSecurity) ann;
168+
}
169+
}
170+
return null;
171+
}
172+
173+
@SuppressWarnings("unchecked")
174+
private <T> T convertClaimValue(Claim claim) {
175+
if (claim.type() != Object.class) {
176+
Converter<?> converter = converters.get(claim.type());
177+
if (converter != null) {
178+
return (T) converter.convert(claim.value());
179+
} else {
180+
throw new RuntimeException("Unsupported claim type: " + claim.type().getName());
181+
}
182+
} else if (standardClaimConverteres.containsKey(claim.key())) {
183+
Converter<?> converter = standardClaimConverteres.get(claim.key());
184+
return (T) converter.convert(claim.value());
185+
} else {
186+
return (T) claim.value();
187+
}
188+
}
189+
190+
private static interface Converter<T> {
191+
T convert(String value);
192+
}
193+
194+
private static class StringConverter implements Converter<String> {
195+
@Override
196+
public String convert(String value) {
197+
return value;
198+
}
199+
}
200+
201+
private static class IntegerConverter implements Converter<Integer> {
202+
@Override
203+
public Integer convert(String value) {
204+
return Integer.valueOf(value);
205+
}
206+
}
207+
208+
private static class LongConverter implements Converter<Long> {
209+
@Override
210+
public Long convert(String value) {
211+
return Long.valueOf(value);
212+
}
213+
}
214+
215+
private static class BooleanConverter implements Converter<Boolean> {
216+
@Override
217+
public Boolean convert(String value) {
218+
return Boolean.valueOf(value);
219+
}
220+
}
221+
222+
private static class JsonObjectConverter implements Converter<jakarta.json.JsonObject> {
223+
@Override
224+
public jakarta.json.JsonObject convert(String value) {
225+
try (JsonReader jsonReader = Json.createReader(new StringReader(value))) {
226+
return jsonReader.readObject();
227+
}
228+
}
229+
}
230+
231+
private static class JsonArrayConverter implements Converter<JsonArray> {
232+
@Override
233+
public JsonArray convert(String value) {
234+
try (JsonReader jsonReader = Json.createReader(new StringReader(value))) {
235+
return jsonReader.readArray();
236+
}
237+
}
238+
}
239+
}

0 commit comments

Comments
 (0)