diff --git a/test-framework/security-jwt/pom.xml b/test-framework/security-jwt/pom.xml index 0aa072bb0146d..ee3df7bf14cd9 100644 --- a/test-framework/security-jwt/pom.xml +++ b/test-framework/security-jwt/pom.xml @@ -28,6 +28,12 @@ junit-jupiter compile + + io.quarkus + quarkus-jsonp + compile + true + org.eclipse.microprofile.jwt microprofile-jwt-auth-api diff --git a/test-framework/security-jwt/src/main/java/io/quarkus/test/security/jwt/Claim.java b/test-framework/security-jwt/src/main/java/io/quarkus/test/security/jwt/Claim.java index 22c2c4c3ea32b..4cbd4e90a0721 100644 --- a/test-framework/security-jwt/src/main/java/io/quarkus/test/security/jwt/Claim.java +++ b/test-framework/security-jwt/src/main/java/io/quarkus/test/security/jwt/Claim.java @@ -7,7 +7,21 @@ @Retention(RetentionPolicy.RUNTIME) @Target({}) public @interface Claim { + /** + * Claim name + */ String key(); + /** + * Claim value + */ String value(); + + /** + * Claim value type. + * If this type is set to {@link ClaimType#DEFAULT} then the value will be converted to String unless the claim + * is a standard claim such as `exp` (expiry), `iat` (issued at), `nbf` (not before), `auth_time` (authentication time) + * whose value will be converted to Long or `email_verified` whose value will be converted to Boolean. + */ + ClaimType type() default ClaimType.DEFAULT; } diff --git a/test-framework/security-jwt/src/main/java/io/quarkus/test/security/jwt/ClaimType.java b/test-framework/security-jwt/src/main/java/io/quarkus/test/security/jwt/ClaimType.java new file mode 100644 index 0000000000000..7f87ce0a26c08 --- /dev/null +++ b/test-framework/security-jwt/src/main/java/io/quarkus/test/security/jwt/ClaimType.java @@ -0,0 +1,59 @@ +package io.quarkus.test.security.jwt; + +import java.io.StringReader; + +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.json.JsonReader; + +public enum ClaimType { + LONG { + @Override + public Long convert(String value) { + return Long.parseLong(value); + } + }, + INTEGER { + @Override + public Integer convert(String value) { + return Integer.parseInt(value); + } + }, + BOOLEAN { + @Override + public Boolean convert(String value) { + return Boolean.parseBoolean(value); + } + }, + STRING { + @Override + public String convert(String value) { + return value; + } + }, + JSON_ARRAY { + @Override + public JsonArray convert(String value) { + try (JsonReader jsonReader = Json.createReader(new StringReader(value))) { + return jsonReader.readArray(); + } + } + }, + JSON_OBJECT { + @Override + public JsonObject convert(String value) { + try (JsonReader jsonReader = Json.createReader(new StringReader(value))) { + return jsonReader.readObject(); + } + } + }, + DEFAULT { + @Override + public String convert(String value) { + return value; + } + }; + + abstract Object convert(String value); +} \ No newline at end of file diff --git a/test-framework/security-jwt/src/main/java/io/quarkus/test/security/jwt/JwtTestSecurityIdentityAugmentor.java b/test-framework/security-jwt/src/main/java/io/quarkus/test/security/jwt/JwtTestSecurityIdentityAugmentor.java new file mode 100644 index 0000000000000..76ffc96107f76 --- /dev/null +++ b/test-framework/security-jwt/src/main/java/io/quarkus/test/security/jwt/JwtTestSecurityIdentityAugmentor.java @@ -0,0 +1,110 @@ +package io.quarkus.test.security.jwt; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import jakarta.json.Json; +import jakarta.json.JsonValue; + +import org.eclipse.microprofile.jwt.Claims; +import org.eclipse.microprofile.jwt.JsonWebToken; + +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.quarkus.test.security.TestSecurityIdentityAugmentor; + +public class JwtTestSecurityIdentityAugmentor implements TestSecurityIdentityAugmentor { + private static Map standardClaimTypes = Map.of( + Claims.exp.name(), ClaimType.LONG, + Claims.iat.name(), ClaimType.LONG, + Claims.nbf.name(), ClaimType.LONG, + Claims.auth_time.name(), ClaimType.LONG, + Claims.email_verified.name(), ClaimType.BOOLEAN); + + @Override + public SecurityIdentity augment(final SecurityIdentity identity, final Annotation[] annotations) { + QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity); + + final JwtSecurity jwtSecurity = findJwtSecurity(annotations); + builder.setPrincipal(new JsonWebToken() { + + @Override + public String getName() { + return identity.getPrincipal().getName(); + } + + @SuppressWarnings("unchecked") + @Override + public T getClaim(String claimName) { + if (Claims.groups.name().equals(claimName)) { + return (T) identity.getRoles(); + } + if (jwtSecurity != null && jwtSecurity.claims() != null) { + for (Claim claim : jwtSecurity.claims()) { + if (claim.key().equals(claimName)) { + return (T) wrapValue(claim, convertClaimValue(claim)); + } + } + } + return null; + } + + @Override + public Set getClaimNames() { + if (jwtSecurity != null && jwtSecurity.claims() != null) { + return Arrays.stream(jwtSecurity.claims()).map(Claim::key).collect(Collectors.toSet()); + } + return Collections.emptySet(); + } + + }); + + return builder.build(); + } + + private static JwtSecurity findJwtSecurity(Annotation[] annotations) { + for (Annotation ann : annotations) { + if (ann instanceof JwtSecurity) { + return (JwtSecurity) ann; + } + } + return null; + } + + private Object wrapValue(Claim claim, Object convertedClaimValue) { + Claims claimType = getClaimType(claim.key()); + if (Claims.UNKNOWN == claimType) { + if (convertedClaimValue instanceof Long) { + return Json.createValue((Long) convertedClaimValue); + } else if (convertedClaimValue instanceof Integer) { + return Json.createValue((Integer) convertedClaimValue); + } else if (convertedClaimValue instanceof Boolean) { + return (Boolean) convertedClaimValue ? JsonValue.TRUE : JsonValue.FALSE; + } + } + return convertedClaimValue; + } + + protected Claims getClaimType(String claimName) { + Claims claimType; + try { + claimType = Claims.valueOf(claimName); + } catch (IllegalArgumentException e) { + claimType = Claims.UNKNOWN; + } + return claimType; + } + + private Object convertClaimValue(Claim claim) { + ClaimType type = claim.type(); + if (type == ClaimType.DEFAULT && standardClaimTypes.containsKey(claim.key())) { + type = standardClaimTypes.get(claim.key()); + } + return type.convert(claim.value()); + } + +} diff --git a/test-framework/security-jwt/src/main/java/io/quarkus/test/security/jwt/JwtTestSecurityIdentityAugmentorProducer.java b/test-framework/security-jwt/src/main/java/io/quarkus/test/security/jwt/JwtTestSecurityIdentityAugmentorProducer.java index 8b4c84f120304..8e894b814459a 100644 --- a/test-framework/security-jwt/src/main/java/io/quarkus/test/security/jwt/JwtTestSecurityIdentityAugmentorProducer.java +++ b/test-framework/security-jwt/src/main/java/io/quarkus/test/security/jwt/JwtTestSecurityIdentityAugmentorProducer.java @@ -1,20 +1,9 @@ package io.quarkus.test.security.jwt; -import java.lang.annotation.Annotation; -import java.util.Arrays; -import java.util.Collections; -import java.util.Set; -import java.util.stream.Collectors; - import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; -import org.eclipse.microprofile.jwt.Claims; -import org.eclipse.microprofile.jwt.JsonWebToken; - import io.quarkus.arc.Unremovable; -import io.quarkus.security.identity.SecurityIdentity; -import io.quarkus.security.runtime.QuarkusSecurityIdentity; import io.quarkus.test.security.TestSecurityIdentityAugmentor; @ApplicationScoped @@ -25,57 +14,4 @@ public class JwtTestSecurityIdentityAugmentorProducer { public TestSecurityIdentityAugmentor produce() { return new JwtTestSecurityIdentityAugmentor(); } - - private static class JwtTestSecurityIdentityAugmentor implements TestSecurityIdentityAugmentor { - - @Override - public SecurityIdentity augment(final SecurityIdentity identity, final Annotation[] annotations) { - QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity); - - final JwtSecurity jwtSecurity = findJwtSecurity(annotations); - builder.setPrincipal(new JsonWebToken() { - - @Override - public String getName() { - return identity.getPrincipal().getName(); - } - - @SuppressWarnings("unchecked") - @Override - public T getClaim(String claimName) { - if (Claims.groups.name().equals(claimName)) { - return (T) identity.getRoles(); - } - if (jwtSecurity != null && jwtSecurity.claims() != null) { - for (Claim claim : jwtSecurity.claims()) { - if (claim.key().equals(claimName)) { - return (T) claim.value(); - } - } - } - return null; - } - - @Override - public Set getClaimNames() { - if (jwtSecurity != null && jwtSecurity.claims() != null) { - return Arrays.stream(jwtSecurity.claims()).map(Claim::key).collect(Collectors.toSet()); - } - return Collections.emptySet(); - } - - }); - - return builder.build(); - } - - private JwtSecurity findJwtSecurity(Annotation[] annotations) { - for (Annotation ann : annotations) { - if (ann instanceof JwtSecurity) { - return (JwtSecurity) ann; - } - } - return null; - } - } } diff --git a/test-framework/security-jwt/src/test/java/io/quarkus/test/security/jwt/JwtTestSecurityIdentityAugmentorTest.java b/test-framework/security-jwt/src/test/java/io/quarkus/test/security/jwt/JwtTestSecurityIdentityAugmentorTest.java new file mode 100644 index 0000000000000..17f0787939be7 --- /dev/null +++ b/test-framework/security-jwt/src/test/java/io/quarkus/test/security/jwt/JwtTestSecurityIdentityAugmentorTest.java @@ -0,0 +1,75 @@ +package io.quarkus.test.security.jwt; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.annotation.Annotation; +import java.security.Principal; +import java.util.Set; + +import jakarta.json.JsonArray; +import jakarta.json.JsonNumber; +import jakarta.json.JsonObject; +import jakarta.json.JsonValue; + +import org.eclipse.microprofile.jwt.Claims; +import org.eclipse.microprofile.jwt.JsonWebToken; +import org.junit.jupiter.api.Test; + +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; + +public class JwtTestSecurityIdentityAugmentorTest { + + @Test + @JwtSecurity(claims = { + @Claim(key = "exp", value = "123456789"), + @Claim(key = "iat", value = "123456788"), + @Claim(key = "nbf", value = "123456787"), + @Claim(key = "auth_time", value = "123456786"), + @Claim(key = "customlong", value = "123456785", type = ClaimType.LONG), + @Claim(key = "email", value = "user@gmail.com"), + @Claim(key = "email_verified", value = "true"), + @Claim(key = "email_checked", value = "false", type = ClaimType.BOOLEAN), + @Claim(key = "jsonarray_claim", value = "[\"1\", \"2\"]", type = ClaimType.JSON_ARRAY), + @Claim(key = "jsonobject_claim", value = "{\"a\":\"1\", \"b\":\"2\"}", type = ClaimType.JSON_OBJECT) + }) + public void testClaimValues() throws Exception { + SecurityIdentity identity = QuarkusSecurityIdentity.builder() + .setPrincipal(new Principal() { + @Override + public String getName() { + return "alice"; + } + + }) + .addRole("user") + .build(); + + JwtTestSecurityIdentityAugmentor augmentor = new JwtTestSecurityIdentityAugmentor(); + + Annotation[] annotations = JwtTestSecurityIdentityAugmentorTest.class.getMethod("testClaimValues").getAnnotations(); + JsonWebToken jwt = (JsonWebToken) augmentor.augment(identity, annotations).getPrincipal(); + + assertEquals("alice", jwt.getName()); + assertEquals(Set.of("user"), jwt.getGroups()); + + assertEquals(123456789, jwt.getExpirationTime()); + assertEquals(123456788, jwt.getIssuedAtTime()); + assertEquals(123456787, (Long) jwt.getClaim(Claims.nbf.name())); + assertEquals(123456786, (Long) jwt.getClaim(Claims.auth_time.name())); + assertEquals(123456785, ((JsonNumber) jwt.getClaim("customlong")).longValue()); + assertEquals("user@gmail.com", jwt.getClaim(Claims.email)); + assertTrue((Boolean) jwt.getClaim(Claims.email_verified.name())); + assertEquals(JsonValue.FALSE, jwt.getClaim("email_checked")); + + JsonArray array = jwt.getClaim("jsonarray_claim"); + assertEquals("1", array.getString(0)); + assertEquals("2", array.getString(1)); + + JsonObject map = jwt.getClaim("jsonobject_claim"); + assertEquals("1", map.getString("a")); + assertEquals("2", map.getString("b")); + } + +} diff --git a/test-framework/security-oidc/src/main/java/io/quarkus/test/security/oidc/Claim.java b/test-framework/security-oidc/src/main/java/io/quarkus/test/security/oidc/Claim.java index a0a326a72e525..578a26b36278c 100644 --- a/test-framework/security-oidc/src/main/java/io/quarkus/test/security/oidc/Claim.java +++ b/test-framework/security-oidc/src/main/java/io/quarkus/test/security/oidc/Claim.java @@ -7,7 +7,21 @@ @Retention(RetentionPolicy.RUNTIME) @Target({}) public @interface Claim { + /** + * Claim name + */ String key(); + /** + * Claim value + */ String value(); + + /** + * Claim value type. + * If this type is set to {@link ClaimType#DEFAULT} then the value will be converted to String unless the claim + * is a standard claim such as `exp` (expiry), `iat` (issued at), `nbf` (not before), `auth_time` (authentication time) + * whose value will be converted to Long or `email_verified` whose value will be converted to Boolean. + */ + ClaimType type() default ClaimType.DEFAULT; } diff --git a/test-framework/security-oidc/src/main/java/io/quarkus/test/security/oidc/ClaimType.java b/test-framework/security-oidc/src/main/java/io/quarkus/test/security/oidc/ClaimType.java new file mode 100644 index 0000000000000..f5c7722a2ff9d --- /dev/null +++ b/test-framework/security-oidc/src/main/java/io/quarkus/test/security/oidc/ClaimType.java @@ -0,0 +1,59 @@ +package io.quarkus.test.security.oidc; + +import java.io.StringReader; + +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.json.JsonReader; + +public enum ClaimType { + LONG { + @Override + public Long convert(String value) { + return Long.parseLong(value); + } + }, + INTEGER { + @Override + public Integer convert(String value) { + return Integer.parseInt(value); + } + }, + BOOLEAN { + @Override + public Boolean convert(String value) { + return Boolean.parseBoolean(value); + } + }, + STRING { + @Override + public String convert(String value) { + return value; + } + }, + JSON_ARRAY { + @Override + public JsonArray convert(String value) { + try (JsonReader jsonReader = Json.createReader(new StringReader(value))) { + return jsonReader.readArray(); + } + } + }, + JSON_OBJECT { + @Override + public JsonObject convert(String value) { + try (JsonReader jsonReader = Json.createReader(new StringReader(value))) { + return jsonReader.readObject(); + } + } + }, + DEFAULT { + @Override + public String convert(String value) { + return value; + } + }; + + abstract Object convert(String value); +} \ No newline at end of file diff --git a/test-framework/security-oidc/src/main/java/io/quarkus/test/security/oidc/OidcTestSecurityIdentityAugmentor.java b/test-framework/security-oidc/src/main/java/io/quarkus/test/security/oidc/OidcTestSecurityIdentityAugmentor.java new file mode 100644 index 0000000000000..ad3d6e6aaf2eb --- /dev/null +++ b/test-framework/security-oidc/src/main/java/io/quarkus/test/security/oidc/OidcTestSecurityIdentityAugmentor.java @@ -0,0 +1,164 @@ +package io.quarkus.test.security.oidc; + +import java.lang.annotation.Annotation; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonObjectBuilder; + +import org.eclipse.microprofile.jwt.Claims; +import org.eclipse.microprofile.jwt.JsonWebToken; +import org.jose4j.jwt.JwtClaims; + +import io.quarkus.oidc.AccessTokenCredential; +import io.quarkus.oidc.IdTokenCredential; +import io.quarkus.oidc.OidcConfigurationMetadata; +import io.quarkus.oidc.common.runtime.OidcConstants; +import io.quarkus.oidc.runtime.OidcJwtCallerPrincipal; +import io.quarkus.oidc.runtime.OidcUtils; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.quarkus.test.security.TestSecurityIdentityAugmentor; +import io.smallrye.jwt.build.Jwt; +import io.smallrye.jwt.util.KeyUtils; +import io.vertx.core.json.JsonObject; + +public class OidcTestSecurityIdentityAugmentor implements TestSecurityIdentityAugmentor { + + private static Map standardClaimTypes = Map.of( + Claims.exp.name(), ClaimType.LONG, + Claims.iat.name(), ClaimType.LONG, + Claims.nbf.name(), ClaimType.LONG, + Claims.auth_time.name(), ClaimType.LONG, + Claims.email_verified.name(), ClaimType.BOOLEAN); + + private Optional issuer; + private PrivateKey privateKey; + + public OidcTestSecurityIdentityAugmentor(Optional issuer) { + this.issuer = issuer; + try { + privateKey = KeyUtils.generateKeyPair(2048).getPrivate(); + } catch (NoSuchAlgorithmException ex) { + throw new RuntimeException(ex); + } + } + + @Override + public SecurityIdentity augment(final SecurityIdentity identity, final Annotation[] annotations) { + QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity); + + final OidcSecurity oidcSecurity = findOidcSecurity(annotations); + + final boolean introspectionRequired = oidcSecurity != null && oidcSecurity.introspectionRequired(); + + if (!introspectionRequired) { + // JsonWebToken + JsonObjectBuilder claims = Json.createObjectBuilder(); + claims.add(Claims.preferred_username.name(), identity.getPrincipal().getName()); + claims.add(Claims.groups.name(), + Json.createArrayBuilder(identity.getRoles().stream().collect(Collectors.toList())).build()); + if (oidcSecurity != null && oidcSecurity.claims() != null) { + for (Claim claim : oidcSecurity.claims()) { + Object claimValue = convertClaimValue(claim); + if (claimValue instanceof String) { + claims.add(claim.key(), (String) claimValue); + } else if (claimValue instanceof Long) { + claims.add(claim.key(), (Long) claimValue); + } else if (claimValue instanceof Integer) { + claims.add(claim.key(), (Integer) claimValue); + } else if (claimValue instanceof Boolean) { + claims.add(claim.key(), (Boolean) claimValue); + } else if (claimValue instanceof JsonArray) { + claims.add(claim.key(), (JsonArray) claimValue); + } else if (claimValue instanceof jakarta.json.JsonObject) { + claims.add(claim.key(), (jakarta.json.JsonObject) claimValue); + } + } + } + jakarta.json.JsonObject claimsJson = claims.build(); + String jwt = generateToken(claimsJson); + IdTokenCredential idToken = new IdTokenCredential(jwt); + AccessTokenCredential accessToken = new AccessTokenCredential(jwt); + + try { + JsonWebToken principal = new OidcJwtCallerPrincipal(JwtClaims.parse(claimsJson.toString()), idToken); + builder.setPrincipal(principal); + } catch (Exception ex) { + throw new RuntimeException(); + } + builder.addCredential(idToken); + builder.addCredential(accessToken); + } else { + JsonObjectBuilder introspectionBuilder = Json.createObjectBuilder(); + introspectionBuilder.add(OidcConstants.INTROSPECTION_TOKEN_ACTIVE, true); + introspectionBuilder.add(OidcConstants.INTROSPECTION_TOKEN_USERNAME, identity.getPrincipal().getName()); + introspectionBuilder.add(OidcConstants.TOKEN_SCOPE, + identity.getRoles().stream().collect(Collectors.joining(" "))); + + if (oidcSecurity != null && oidcSecurity.introspection() != null) { + for (TokenIntrospection introspection : oidcSecurity.introspection()) { + introspectionBuilder.add(introspection.key(), introspection.value()); + } + } + + builder.addAttribute(OidcUtils.INTROSPECTION_ATTRIBUTE, + new io.quarkus.oidc.TokenIntrospection(introspectionBuilder.build())); + builder.addCredential(new AccessTokenCredential(UUID.randomUUID().toString(), null)); + } + + // UserInfo + if (oidcSecurity != null && oidcSecurity.userinfo() != null) { + JsonObjectBuilder userInfoBuilder = Json.createObjectBuilder(); + for (UserInfo userinfo : oidcSecurity.userinfo()) { + userInfoBuilder.add(userinfo.key(), userinfo.value()); + } + builder.addAttribute(OidcUtils.USER_INFO_ATTRIBUTE, new io.quarkus.oidc.UserInfo(userInfoBuilder.build())); + } + + // OidcConfigurationMetadata + JsonObject configMetadataBuilder = new JsonObject(); + if (issuer.isPresent()) { + configMetadataBuilder.put("issuer", issuer.get()); + } + if (oidcSecurity != null && oidcSecurity.config() != null) { + for (ConfigMetadata config : oidcSecurity.config()) { + configMetadataBuilder.put(config.key(), config.value()); + } + } + builder.addAttribute(OidcUtils.CONFIG_METADATA_ATTRIBUTE, new OidcConfigurationMetadata(configMetadataBuilder)); + + return builder.build(); + } + + private String generateToken(jakarta.json.JsonObject claims) { + try { + return Jwt.claims(claims).sign(privateKey); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private static OidcSecurity findOidcSecurity(Annotation[] annotations) { + for (Annotation ann : annotations) { + if (ann instanceof OidcSecurity) { + return (OidcSecurity) ann; + } + } + return null; + } + + private Object convertClaimValue(Claim claim) { + ClaimType type = claim.type(); + if (type == ClaimType.DEFAULT && standardClaimTypes.containsKey(claim.key())) { + type = standardClaimTypes.get(claim.key()); + } + return type.convert(claim.value()); + } +} diff --git a/test-framework/security-oidc/src/main/java/io/quarkus/test/security/oidc/OidcTestSecurityIdentityAugmentorProducer.java b/test-framework/security-oidc/src/main/java/io/quarkus/test/security/oidc/OidcTestSecurityIdentityAugmentorProducer.java index accf1ce2042f3..8e49aaaa0c2c6 100644 --- a/test-framework/security-oidc/src/main/java/io/quarkus/test/security/oidc/OidcTestSecurityIdentityAugmentorProducer.java +++ b/test-framework/security-oidc/src/main/java/io/quarkus/test/security/oidc/OidcTestSecurityIdentityAugmentorProducer.java @@ -1,37 +1,15 @@ package io.quarkus.test.security.oidc; -import java.lang.annotation.Annotation; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; import java.util.Optional; -import java.util.UUID; -import java.util.stream.Collectors; -import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; import jakarta.inject.Inject; -import jakarta.json.Json; -import jakarta.json.JsonObjectBuilder; import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.eclipse.microprofile.jwt.Claims; -import org.eclipse.microprofile.jwt.JsonWebToken; -import org.jose4j.jwt.JwtClaims; import io.quarkus.arc.Unremovable; -import io.quarkus.oidc.AccessTokenCredential; -import io.quarkus.oidc.IdTokenCredential; -import io.quarkus.oidc.OidcConfigurationMetadata; -import io.quarkus.oidc.common.runtime.OidcConstants; -import io.quarkus.oidc.runtime.OidcJwtCallerPrincipal; -import io.quarkus.oidc.runtime.OidcUtils; -import io.quarkus.security.identity.SecurityIdentity; -import io.quarkus.security.runtime.QuarkusSecurityIdentity; import io.quarkus.test.security.TestSecurityIdentityAugmentor; -import io.smallrye.jwt.build.Jwt; -import io.smallrye.jwt.util.KeyUtils; -import io.vertx.core.json.JsonObject; @ApplicationScoped public class OidcTestSecurityIdentityAugmentorProducer { @@ -40,109 +18,9 @@ public class OidcTestSecurityIdentityAugmentorProducer { @ConfigProperty(name = "quarkus.oidc.token.issuer") Optional issuer; - PrivateKey privateKey; - - @PostConstruct - public void init() { - try { - privateKey = KeyUtils.generateKeyPair(2048).getPrivate(); - } catch (NoSuchAlgorithmException ex) { - throw new RuntimeException(ex); - } - } - @Produces @Unremovable public TestSecurityIdentityAugmentor produce() { - return new OidcTestSecurityIdentityAugmentor(); - } - - private class OidcTestSecurityIdentityAugmentor implements TestSecurityIdentityAugmentor { - - @Override - public SecurityIdentity augment(final SecurityIdentity identity, final Annotation[] annotations) { - QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity); - - final OidcSecurity oidcSecurity = findOidcSecurity(annotations); - - final boolean introspectionRequired = oidcSecurity != null && oidcSecurity.introspectionRequired(); - - if (!introspectionRequired) { - // JsonWebToken - JwtClaims claims = new JwtClaims(); - claims.setClaim(Claims.preferred_username.name(), identity.getPrincipal().getName()); - claims.setClaim(Claims.groups.name(), identity.getRoles().stream().collect(Collectors.toList())); - if (oidcSecurity != null && oidcSecurity.claims() != null) { - for (Claim claim : oidcSecurity.claims()) { - claims.setClaim(claim.key(), claim.value()); - } - } - String jwt = generateToken(claims); - IdTokenCredential idToken = new IdTokenCredential(jwt); - AccessTokenCredential accessToken = new AccessTokenCredential(jwt); - - JsonWebToken principal = new OidcJwtCallerPrincipal(claims, idToken); - builder.setPrincipal(principal); - builder.addCredential(idToken); - builder.addCredential(accessToken); - } else { - JsonObjectBuilder introspectionBuilder = Json.createObjectBuilder(); - introspectionBuilder.add(OidcConstants.INTROSPECTION_TOKEN_ACTIVE, true); - introspectionBuilder.add(OidcConstants.INTROSPECTION_TOKEN_USERNAME, identity.getPrincipal().getName()); - introspectionBuilder.add(OidcConstants.TOKEN_SCOPE, - identity.getRoles().stream().collect(Collectors.joining(" "))); - - if (oidcSecurity != null && oidcSecurity.introspection() != null) { - for (TokenIntrospection introspection : oidcSecurity.introspection()) { - introspectionBuilder.add(introspection.key(), introspection.value()); - } - } - - builder.addAttribute(OidcUtils.INTROSPECTION_ATTRIBUTE, - new io.quarkus.oidc.TokenIntrospection(introspectionBuilder.build())); - builder.addCredential(new AccessTokenCredential(UUID.randomUUID().toString(), null)); - } - - // UserInfo - if (oidcSecurity != null && oidcSecurity.userinfo() != null) { - JsonObjectBuilder userInfoBuilder = Json.createObjectBuilder(); - for (UserInfo userinfo : oidcSecurity.userinfo()) { - userInfoBuilder.add(userinfo.key(), userinfo.value()); - } - builder.addAttribute(OidcUtils.USER_INFO_ATTRIBUTE, new io.quarkus.oidc.UserInfo(userInfoBuilder.build())); - } - - // OidcConfigurationMetadata - JsonObject configMetadataBuilder = new JsonObject(); - if (issuer.isPresent()) { - configMetadataBuilder.put("issuer", issuer.get()); - } - if (oidcSecurity != null && oidcSecurity.config() != null) { - for (ConfigMetadata config : oidcSecurity.config()) { - configMetadataBuilder.put(config.key(), config.value()); - } - } - builder.addAttribute(OidcUtils.CONFIG_METADATA_ATTRIBUTE, new OidcConfigurationMetadata(configMetadataBuilder)); - - return builder.build(); - } - - private String generateToken(JwtClaims claims) { - try { - return Jwt.claims(claims.getClaimsMap()).sign(privateKey); - } catch (Exception ex) { - throw new RuntimeException(ex); - } - } - - private OidcSecurity findOidcSecurity(Annotation[] annotations) { - for (Annotation ann : annotations) { - if (ann instanceof OidcSecurity) { - return (OidcSecurity) ann; - } - } - return null; - } + return new OidcTestSecurityIdentityAugmentor(issuer); } - } diff --git a/test-framework/security-oidc/src/test/java/io/quarkus/test/security/oidc/OidcTestSecurityIdentityAugmentorTest.java b/test-framework/security-oidc/src/test/java/io/quarkus/test/security/oidc/OidcTestSecurityIdentityAugmentorTest.java new file mode 100644 index 0000000000000..6b7fc8cb1519d --- /dev/null +++ b/test-framework/security-oidc/src/test/java/io/quarkus/test/security/oidc/OidcTestSecurityIdentityAugmentorTest.java @@ -0,0 +1,76 @@ +package io.quarkus.test.security.oidc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.annotation.Annotation; +import java.security.Principal; +import java.util.Optional; +import java.util.Set; + +import jakarta.json.JsonArray; +import jakarta.json.JsonNumber; +import jakarta.json.JsonObject; +import jakarta.json.JsonValue; + +import org.eclipse.microprofile.jwt.Claims; +import org.eclipse.microprofile.jwt.JsonWebToken; +import org.junit.jupiter.api.Test; + +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; + +public class OidcTestSecurityIdentityAugmentorTest { + + @Test + @OidcSecurity(claims = { + @Claim(key = "exp", value = "123456789"), + @Claim(key = "iat", value = "123456788"), + @Claim(key = "nbf", value = "123456787"), + @Claim(key = "auth_time", value = "123456786"), + @Claim(key = "customlong", value = "123456785", type = ClaimType.LONG), + @Claim(key = "email", value = "user@gmail.com"), + @Claim(key = "email_verified", value = "true"), + @Claim(key = "email_checked", value = "false", type = ClaimType.BOOLEAN), + @Claim(key = "jsonarray_claim", value = "[\"1\", \"2\"]", type = ClaimType.JSON_ARRAY), + @Claim(key = "jsonobject_claim", value = "{\"a\":\"1\", \"b\":\"2\"}", type = ClaimType.JSON_OBJECT) + }) + public void testClaimValues() throws Exception { + SecurityIdentity identity = QuarkusSecurityIdentity.builder() + .setPrincipal(new Principal() { + @Override + public String getName() { + return "alice"; + } + + }) + .addRole("user") + .build(); + + OidcTestSecurityIdentityAugmentor augmentor = new OidcTestSecurityIdentityAugmentor(Optional.of("https://issuer.org")); + + Annotation[] annotations = OidcTestSecurityIdentityAugmentorTest.class.getMethod("testClaimValues").getAnnotations(); + JsonWebToken jwt = (JsonWebToken) augmentor.augment(identity, annotations).getPrincipal(); + + assertEquals("alice", jwt.getName()); + assertEquals(Set.of("user"), jwt.getGroups()); + + assertEquals(123456789, jwt.getExpirationTime()); + assertEquals(123456788, jwt.getIssuedAtTime()); + assertEquals(123456787, (Long) jwt.getClaim(Claims.nbf.name())); + assertEquals(123456786, (Long) jwt.getClaim(Claims.auth_time.name())); + assertEquals(123456785, ((JsonNumber) jwt.getClaim("customlong")).longValue()); + assertEquals("user@gmail.com", jwt.getClaim(Claims.email)); + assertTrue((Boolean) jwt.getClaim(Claims.email_verified.name())); + assertEquals(JsonValue.FALSE, jwt.getClaim("email_checked")); + + JsonArray array = jwt.getClaim("jsonarray_claim"); + assertEquals("1", array.getString(0)); + assertEquals("2", array.getString(1)); + + JsonObject map = jwt.getClaim("jsonobject_claim"); + assertEquals("1", map.getString("a")); + assertEquals("2", map.getString("b")); + } + +}