diff --git a/bom/application/pom.xml b/bom/application/pom.xml index affd938a32c2c..63d6e2d36e469 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -2419,6 +2419,16 @@ quarkus-test-security ${project.version} + + io.quarkus + quarkus-test-security-jwt + ${project.version} + + + io.quarkus + quarkus-test-security-oidc + ${project.version} + io.quarkus quarkus-junit5-internal diff --git a/docs/src/main/asciidoc/security-jwt.adoc b/docs/src/main/asciidoc/security-jwt.adoc index e8b430dd1f0f5..ce08b2eab71ea 100644 --- a/docs/src/main/asciidoc/security-jwt.adoc +++ b/docs/src/main/asciidoc/security-jwt.adoc @@ -813,6 +813,7 @@ Please see link:security-openid-connect-client#token-propagation[Token Propagati [[integration-testing]] == Testing +[[integration-testing-wiremock]] === Wiremock If you configure `mp.jwt.verify.publickey.location` to point to HTTPS or HTTP based JsonWebKey (JWK) set then you can use the same approach as described in the link:security-openid-connect#integration-testing[OpenId Connect Bearer Token Integration testing] `Wiremock` section but only change the `application.properties` to use MP JWT configuration properties instead: @@ -827,6 +828,7 @@ mp.jwt.verify.issuer=${keycloak.url}/realms/quarkus smallrye.jwt.sign.key.location=privateKey.jwk ---- +[[integration-testing-keycloak]] === Keycloak If you work with Keycloak and configure `mp.jwt.verify.publickey.location` to point to HTTPS or HTTP based JsonWebKey (JWK) set then you can use the same approach as described in the link:security-openid-connect#integration-testing-keycloak[OpenId Connect Bearer Token Integration testing] `Keycloak` section but only change the `application.properties` to use MP JWT configuration properties instead: @@ -838,6 +840,7 @@ mp.jwt.verify.publickey.location=${keycloak.url}/realms/quarkus/protocol/openid- mp.jwt.verify.issuer=${keycloak.url}/realms/quarkus ---- +[[integration-testing-public-key]] === Local Public Key You can use the same approach as described in the link:security-openid-connect#integration-testing[OpenId Connect Bearer Token Integration testing] `Local Public Key` section but only change the `application.properties` to use MP JWT configuration properties instead: @@ -852,9 +855,82 @@ mp.jwt.verify.issuer=${keycloak.url}/realms/quarkus smallrye.jwt.sign.key.location=privateKey.pem ---- +[[integration-testing-security-annotation]] === TestSecurity annotation -Please see link:security-testing#testing-security[TestingSecurity Annotation] section how to do simple tests with the `TestSecurity` annotation. +Add the following dependency: +[source,xml] +---- + + io.quarkus + quarkus-test-security-jwt + test + +---- + +and write a test code like this one: + +[source, java] +---- +package io.quarkus.it.keycloak; + +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.SecurityAttribute; +import io.quarkus.test.security.TestSecurity; +import io.restassured.RestAssured; + +@QuarkusTest +@TestHTTPEndpoint(ProtectedResource.class) +public class TestSecurityAuthTest { + + @Test + @TestSecurity(user = "userJwt", roles = "viewer", attributes = { + @SecurityAttribute(key = "claim.email", value = "user@gmail.com") + }) + public void testJwtWithDummyUser() { + RestAssured.when().get("test-security-jwt").then() + .body(is("userJwt:viewer:user@gmail.com")); + } + +} +---- + +where `ProtectedResource` class may look like this: + +[source, java] +---- +package io.quarkus.it.keycloak; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.jwt.JsonWebToken; +import io.quarkus.security.Authenticated; +import io.quarkus.security.identity.SecurityIdentity; + +@Path("/web-app") +@Authenticated +public class ProtectedResource { + + @Inject + JsonWebToken accessToken; + + @GET + @Path("test-security-jwt") + public String testSecurityJwt() { + return accessToken.getName() + ":" + accessToken.getGroups().iterator().next() + + ":" + accessToken.getClaim("email"); + } +} +---- + +Note that `TestSecurity` `user` property is returned as `JsonWebToken.getName()` and `roles` property - as `JsonWebToken.getGroups()`. Additionally, any `SecurityAttribute` with the key starting from `claim.` will be returned as a token claim value, for example, `claim.email` will support `JsonWebToken.getClaim("email")`, etc. [[generate-jwt-tokens]] == Generate JWT tokens with SmallRye JWT diff --git a/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc b/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc index 1e4f21bc4f37a..1250c14360430 100644 --- a/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc @@ -681,6 +681,7 @@ Additionally, `OidcWiremockTestResource` set token issuer and audience to `https `OidcWiremockTestResource` can be used to emulate all OpenId Connect providers. +[[integration-testing-keycloak]] === Keycloak If you work with Keycloak then you can test against a live Keycloak instance by adding the following dependency: @@ -733,9 +734,20 @@ public class CodeFlowAuthorizationTest { By default, `KeycloakTestResourceLifecycleManager` uses HTTPS to initialize a Keycloak instance which can be disabled with `keycloak.use.https=false`. Default realm name is `quarkus` and client id - `quarkus-web-app` - set `keycloak.realm` and `keycloak.web-app.client` system properties to customize the values if needed. +[[integration-testing-security-annotation]] === TestSecurity annotation -Please see link:security-testing#testing-security[TestingSecurity Annotation] section how to do simple tests with the `TestSecurity` annotation. +Please see link:security-jwt#integration-testing-security-annotation[Use TestingSecurity with injected JsonWebToken] section for more information about using `@TestSecurity` for testing the `web-app` application endpoint code which depends on the injected ID and access `JsonWebToken`. + +The only difference is that the following dependency should be used when testing the `web-app` applications: +[source,xml] +---- + + io.quarkus + quarkus-test-security-oidc + test + +---- == Configuration Reference diff --git a/docs/src/main/asciidoc/security-openid-connect.adoc b/docs/src/main/asciidoc/security-openid-connect.adoc index ef975f4cf8744..50a4f7f2a0047 100644 --- a/docs/src/main/asciidoc/security-openid-connect.adoc +++ b/docs/src/main/asciidoc/security-openid-connect.adoc @@ -658,6 +658,7 @@ public class BearerTokenAuthorizationTest { By default, `KeycloakTestResourceLifecycleManager` uses HTTPS to initialize a Keycloak instance which can be disabled with `keycloak.use.https=false`. Default realm name is `quarkus` and client id - `quarkus-service-app` - set `keycloak.realm` and `keycloak.service.client` system properties to customize the values if needed. +[[integration-testing-public-key]] === Local Public Key You can also use a local inlined public key for testing your `quarkus-oidc` `service` applications: @@ -674,9 +675,20 @@ copy `privateKey.pem` from the `integration-tests/oidc-tenancy` in the `main` Qu This approach provides a more limited coverage compared to the Wiremock approach - for example, the remote communication code is not covered. +[[integration-testing-security-annotation]] === TestSecurity annotation -Please see link:security-testing#testing-security[TestingSecurity Annotation] section how to do simple tests with the `TestSecurity` annotation. +Please see link:security-jwt#integration-testing-security-annotation[Use TestingSecurity with injected JsonWebToken] section for more information about using `@TestSecurity` for testing the `service` application endpoint code which depends on the injected `JsonWebToken`. + +The only difference is that the following dependency should be used when testing the `service` applications: +[source,xml] +---- + + io.quarkus + quarkus-test-security-oidc + test + +---- == References diff --git a/docs/src/main/asciidoc/security-testing.adoc b/docs/src/main/asciidoc/security-testing.adoc index 6a8d79e6c4e30..c4f516cc69c40 100644 --- a/docs/src/main/asciidoc/security-testing.adoc +++ b/docs/src/main/asciidoc/security-testing.adoc @@ -82,6 +82,8 @@ This will run the test with an identity with the given username and roles. Note disable authorization while also providing an identity to run the test under, which can be useful if the endpoint expects an identity to be present. +See link:security-openid-connect#integration-testing-security-annotation[OpenId Connect Bearer Token Integration testing], link:security-openid-connect-web-authentication#integration-testing-security-annotation[OpenId Connect Authorization Code Flow Integration testing] and link:security-jwt#integration-testing-security-annotation[SmallRye JWT Integration testing] for more details about testing the endpoint code which depends on the injected `JsonWebToken`. + [WARNING] ==== The feature is only available for `@QuarkusTest` and will **not** work on a `@NativeImageTest`. @@ -96,7 +98,7 @@ for example by setting `quarkus.http.auth.basic=true` or `%test.quarkus.http.aut == Use Wiremock for Integration Testing You can also use Wiremock to mock the authorization OAuth2 and OIDC services: -See link:security-oauth2#integration-testing[OAuth2 Integration testing], link:security-openid-connect#integration-testing[OpenId Connect Bearer Token Integration testing], link:security-openid-connect-web-authentication#integration-testing[OpenId Connect Authorization Code Flow Integration testing] and link:security-jwt#integration-testing[SmallRye JWT Integration testing] for more details. +See link:security-oauth2#integration-testing[OAuth2 Integration testing], link:security-openid-connect#integration-testing-wiremock[OpenId Connect Bearer Token Integration testing], link:security-openid-connect-web-authentication#integration-testing-wiremock[OpenId Connect Authorization Code Flow Integration testing] and link:security-jwt#integration-testing-wiremock[SmallRye JWT Integration testing] for more details. == References diff --git a/integration-tests/oidc-code-flow/pom.xml b/integration-tests/oidc-code-flow/pom.xml index 7ae446386df45..6b42fa3b90725 100644 --- a/integration-tests/oidc-code-flow/pom.xml +++ b/integration-tests/oidc-code-flow/pom.xml @@ -37,7 +37,7 @@ io.quarkus - quarkus-test-security + quarkus-test-security-oidc test diff --git a/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java b/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java index d32c550baba41..c6d39d48ff723 100644 --- a/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java +++ b/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java @@ -52,11 +52,18 @@ public class ProtectedResource { SecurityContext securityContext; @GET - @Path("sec") - public String hello() { + @Path("test-security") + public String testSecurity() { return securityContext.getUserPrincipal().getName(); } + @GET + @Path("test-security-oidc") + public String testSecurityJwt() { + return idToken.getName() + ":" + idToken.getGroups().iterator().next() + + ":" + idToken.getClaim("email"); + } + @GET @Path("configMetadataIssuer") public String configMetadataIssuer() { diff --git a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java index aaf566303e5da..53b0880a9cd6b 100644 --- a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java +++ b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java @@ -6,6 +6,7 @@ import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.SecurityAttribute; import io.quarkus.test.security.TestSecurity; import io.restassured.RestAssured; @@ -16,8 +17,17 @@ public class TestSecurityLazyAuthTest { @Test @TestSecurity(user = "user1", roles = "viewer") public void testWithDummyUser() { - RestAssured.when().get("sec").then() + RestAssured.when().get("test-security").then() .body(is("user1")); } + @Test + @TestSecurity(user = "userOidc", roles = "viewer", attributes = { + @SecurityAttribute(key = "claim.email", value = "user@gmail.com") + }) + public void testJwtWithDummyUser() { + RestAssured.when().get("test-security-oidc").then() + .body(is("userOidc:viewer:user@gmail.com")); + } + } diff --git a/integration-tests/oidc/pom.xml b/integration-tests/oidc/pom.xml index 469a31eba448a..c10218ddb9c03 100644 --- a/integration-tests/oidc/pom.xml +++ b/integration-tests/oidc/pom.xml @@ -28,6 +28,11 @@ quarkus-test-keycloak-server test + + io.quarkus + quarkus-test-security-oidc + test + io.quarkus quarkus-junit5 diff --git a/integration-tests/oidc/src/main/java/io/quarkus/it/keycloak/ProtectedJwtResource.java b/integration-tests/oidc/src/main/java/io/quarkus/it/keycloak/ProtectedJwtResource.java new file mode 100644 index 0000000000000..5104bc88e8b28 --- /dev/null +++ b/integration-tests/oidc/src/main/java/io/quarkus/it/keycloak/ProtectedJwtResource.java @@ -0,0 +1,39 @@ +package io.quarkus.it.keycloak; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.SecurityContext; + +import org.eclipse.microprofile.jwt.JsonWebToken; + +import io.quarkus.security.Authenticated; +import io.quarkus.security.identity.SecurityIdentity; + +@Path("/web-app") +@Authenticated +public class ProtectedJwtResource { + + @Inject + SecurityIdentity identity; + + @Inject + JsonWebToken accessToken; + + @Context + SecurityContext securityContext; + + @GET + @Path("test-security") + public String testSecurity() { + return securityContext.getUserPrincipal().getName(); + } + + @GET + @Path("test-security-jwt") + public String testSecurityJwt() { + return accessToken.getName() + ":" + accessToken.getGroups().iterator().next() + + ":" + accessToken.getClaim("email"); + } +} diff --git a/integration-tests/oidc/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java b/integration-tests/oidc/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java new file mode 100644 index 0000000000000..204b41c5d175d --- /dev/null +++ b/integration-tests/oidc/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java @@ -0,0 +1,33 @@ +package io.quarkus.it.keycloak; + +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.SecurityAttribute; +import io.quarkus.test.security.TestSecurity; +import io.restassured.RestAssured; + +@QuarkusTest +@TestHTTPEndpoint(ProtectedJwtResource.class) +public class TestSecurityLazyAuthTest { + + @Test + @TestSecurity(user = "user1", roles = "viewer") + public void testWithDummyUser() { + RestAssured.when().get("test-security").then() + .body(is("user1")); + } + + @Test + @TestSecurity(user = "userJwt", roles = "viewer", attributes = { + @SecurityAttribute(key = "claim.email", value = "user@gmail.com") + }) + public void testJwtWithDummyUser() { + RestAssured.when().get("test-security-jwt").then() + .body(is("userJwt:viewer:user@gmail.com")); + } + +} diff --git a/integration-tests/smallrye-jwt-token-propagation/pom.xml b/integration-tests/smallrye-jwt-token-propagation/pom.xml index c0151077f9f99..435a3005ba800 100644 --- a/integration-tests/smallrye-jwt-token-propagation/pom.xml +++ b/integration-tests/smallrye-jwt-token-propagation/pom.xml @@ -28,6 +28,11 @@ keycloak-core + + io.quarkus + quarkus-test-security-jwt + test + io.quarkus quarkus-junit5 diff --git a/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/ProtectedJwtResource.java b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/ProtectedJwtResource.java new file mode 100644 index 0000000000000..5104bc88e8b28 --- /dev/null +++ b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/ProtectedJwtResource.java @@ -0,0 +1,39 @@ +package io.quarkus.it.keycloak; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.SecurityContext; + +import org.eclipse.microprofile.jwt.JsonWebToken; + +import io.quarkus.security.Authenticated; +import io.quarkus.security.identity.SecurityIdentity; + +@Path("/web-app") +@Authenticated +public class ProtectedJwtResource { + + @Inject + SecurityIdentity identity; + + @Inject + JsonWebToken accessToken; + + @Context + SecurityContext securityContext; + + @GET + @Path("test-security") + public String testSecurity() { + return securityContext.getUserPrincipal().getName(); + } + + @GET + @Path("test-security-jwt") + public String testSecurityJwt() { + return accessToken.getName() + ":" + accessToken.getGroups().iterator().next() + + ":" + accessToken.getClaim("email"); + } +} diff --git a/integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java b/integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java new file mode 100644 index 0000000000000..204b41c5d175d --- /dev/null +++ b/integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java @@ -0,0 +1,33 @@ +package io.quarkus.it.keycloak; + +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.SecurityAttribute; +import io.quarkus.test.security.TestSecurity; +import io.restassured.RestAssured; + +@QuarkusTest +@TestHTTPEndpoint(ProtectedJwtResource.class) +public class TestSecurityLazyAuthTest { + + @Test + @TestSecurity(user = "user1", roles = "viewer") + public void testWithDummyUser() { + RestAssured.when().get("test-security").then() + .body(is("user1")); + } + + @Test + @TestSecurity(user = "userJwt", roles = "viewer", attributes = { + @SecurityAttribute(key = "claim.email", value = "user@gmail.com") + }) + public void testJwtWithDummyUser() { + RestAssured.when().get("test-security-jwt").then() + .body(is("userJwt:viewer:user@gmail.com")); + } + +} diff --git a/test-framework/pom.xml b/test-framework/pom.xml index db2f816d0a165..ee0da3c9e880d 100644 --- a/test-framework/pom.xml +++ b/test-framework/pom.xml @@ -36,6 +36,8 @@ vault ldap security + security-jwt + security-oidc oidc-server keycloak-server jacoco diff --git a/test-framework/security-jwt/pom.xml b/test-framework/security-jwt/pom.xml new file mode 100644 index 0000000000000..8bd1a0123be40 --- /dev/null +++ b/test-framework/security-jwt/pom.xml @@ -0,0 +1,55 @@ + + + + io.quarkus + quarkus-test-framework + 999-SNAPSHOT + ../pom.xml + + + 4.0.0 + quarkus-test-security-jwt + Quarkus - Test Framework - Security JWT + Module that contains utilities to test Quarkus security with JWT token + + + + io.quarkus + quarkus-junit5 + + + io.quarkus + quarkus-test-security + + + org.junit.jupiter + junit-jupiter + compile + + + org.eclipse.microprofile.jwt + microprofile-jwt-auth-api + + + + + + + + org.jboss.jandex + jandex-maven-plugin + + + make-index + + jandex + + + + + + + + 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 new file mode 100644 index 0000000000000..11e90403337cc --- /dev/null +++ b/test-framework/security-jwt/src/main/java/io/quarkus/test/security/jwt/JwtTestSecurityIdentityAugmentorProducer.java @@ -0,0 +1,63 @@ +package io.quarkus.test.security.jwt; + +import java.util.Map; +import java.util.Set; + +import javax.enterprise.context.ApplicationScoped; +import javax.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 +public class JwtTestSecurityIdentityAugmentorProducer { + + @Produces + @Unremovable + public TestSecurityIdentityAugmentor produce() { + return new JwtTestSecurityIdentityAugmentor(); + } + + private static class JwtTestSecurityIdentityAugmentor implements TestSecurityIdentityAugmentor { + + @Override + public SecurityIdentity augment(final SecurityIdentity identity) { + QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity); + + 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(); + } + for (Map.Entry entry : identity.getAttributes().entrySet()) { + if (entry.getKey().startsWith("claim." + claimName)) { + return (T) entry.getValue(); + } + } + return null; + } + + @Override + public Set getClaimNames() { + return null; + } + + }); + + return builder.build(); + } + } +} diff --git a/test-framework/security-oidc/pom.xml b/test-framework/security-oidc/pom.xml new file mode 100644 index 0000000000000..095270779e35a --- /dev/null +++ b/test-framework/security-oidc/pom.xml @@ -0,0 +1,55 @@ + + + + io.quarkus + quarkus-test-framework + 999-SNAPSHOT + ../pom.xml + + + 4.0.0 + quarkus-test-security-oidc + Quarkus - Test Framework - Security OIDC + Module that contains utilities to test Quarkus OpenIdConnect security + + + + io.quarkus + quarkus-junit5 + + + io.quarkus + quarkus-test-security + + + io.quarkus + quarkus-oidc + + + org.junit.jupiter + junit-jupiter + compile + + + + + + + + org.jboss.jandex + jandex-maven-plugin + + + make-index + + jandex + + + + + + + + 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 new file mode 100644 index 0000000000000..080239157a7c7 --- /dev/null +++ b/test-framework/security-oidc/src/main/java/io/quarkus/test/security/oidc/OidcTestSecurityIdentityAugmentorProducer.java @@ -0,0 +1,67 @@ +package io.quarkus.test.security.oidc; + +import java.util.Map; +import java.util.stream.Collectors; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Produces; + +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.runtime.OidcJwtCallerPrincipal; +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; + +@ApplicationScoped +public class OidcTestSecurityIdentityAugmentorProducer { + + @Produces + @Unremovable + public TestSecurityIdentityAugmentor produce() { + return new OidcTestSecurityIdentityAugmentor(); + } + + private static class OidcTestSecurityIdentityAugmentor implements TestSecurityIdentityAugmentor { + + @Override + public SecurityIdentity augment(final SecurityIdentity identity) { + QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity); + + JwtClaims claims = new JwtClaims(); + claims.setClaim(Claims.preferred_username.name(), identity.getPrincipal().getName()); + claims.setClaim(Claims.groups.name(), identity.getRoles().stream().collect(Collectors.toList())); + for (Map.Entry entry : identity.getAttributes().entrySet()) { + if (entry.getKey().startsWith("claim.")) { + claims.setClaim(entry.getKey().substring("claim.".length()), entry.getValue()); + } + } + String jwt = generateToken(claims); + IdTokenCredential idToken = new IdTokenCredential(jwt, null); + AccessTokenCredential accessToken = new AccessTokenCredential(jwt, null); + + JsonWebToken principal = new OidcJwtCallerPrincipal(claims, idToken); + builder.setPrincipal(principal); + builder.addCredential(idToken); + builder.addCredential(accessToken); + + return builder.build(); + } + + private String generateToken(JwtClaims claims) { + try { + return Jwt.claims(claims.getClaimsMap()).sign(KeyUtils.generateKeyPair(2048).getPrivate()); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + } + +} diff --git a/test-framework/security/src/main/java/io/quarkus/test/security/QuarkusSecurityTestExtension.java b/test-framework/security/src/main/java/io/quarkus/test/security/QuarkusSecurityTestExtension.java index 21e9be14cc0fb..127af0ff7eda7 100644 --- a/test-framework/security/src/main/java/io/quarkus/test/security/QuarkusSecurityTestExtension.java +++ b/test-framework/security/src/main/java/io/quarkus/test/security/QuarkusSecurityTestExtension.java @@ -5,8 +5,10 @@ import java.util.HashSet; import java.util.stream.Collectors; +import javax.enterprise.inject.Instance; import javax.enterprise.inject.spi.CDI; +import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.runtime.QuarkusPrincipal; import io.quarkus.security.runtime.QuarkusSecurityIdentity; import io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback; @@ -64,11 +66,20 @@ public void beforeEach(QuarkusTestMethodContext context) { .collect(Collectors.toMap(s -> s.key(), s -> s.value()))); } - CDI.current().select(TestIdentityAssociation.class).get().setTestIdentity(user.build()); + SecurityIdentity userIdentity = augment(user.build()); + CDI.current().select(TestIdentityAssociation.class).get().setTestIdentity(userIdentity); } } catch (Exception e) { throw new RuntimeException("Unable to setup @TestSecurity", e); } } + + private SecurityIdentity augment(SecurityIdentity identity) { + Instance producer = CDI.current().select(TestSecurityIdentityAugmentor.class); + if (producer.isResolvable()) { + return producer.get().augment(identity); + } + return identity; + } } diff --git a/test-framework/security/src/main/java/io/quarkus/test/security/TestSecurityIdentityAugmentor.java b/test-framework/security/src/main/java/io/quarkus/test/security/TestSecurityIdentityAugmentor.java new file mode 100644 index 0000000000000..71af6f2c76acd --- /dev/null +++ b/test-framework/security/src/main/java/io/quarkus/test/security/TestSecurityIdentityAugmentor.java @@ -0,0 +1,7 @@ +package io.quarkus.test.security; + +import io.quarkus.security.identity.SecurityIdentity; + +public interface TestSecurityIdentityAugmentor { + SecurityIdentity augment(SecurityIdentity identity); +}