diff --git a/README.md b/README.md
index 998b10406..53a6a6142 100644
--- a/README.md
+++ b/README.md
@@ -316,7 +316,7 @@ Restrictions are defined using common annotations (`@RolesAllowed` etc.).
A simple Keycloak realm with 1 client (protected application), 2 users and 2 roles is provided in `test-realm.json`.
-### `security/keycloak-authz`
+### `security/keycloak-authz-classic`
Verifies token-based authn and URL-based authz.
Authentication is OIDC, and Keycloak is used for issuing and verifying tokens.
@@ -324,6 +324,14 @@ Authorization is based on URL patterns, and Keycloak is used for defining and en
A simple Keycloak realm with 1 client (protected application), 2 users, 2 roles and 2 protected resources is provided in `test-realm.json`.
+### `security/keycloak-authz-reactive`
+QUARKUS-1257 - Verifies authenticated endpoints with a generic body in parent class
+Verifies token-based authn and URL-based authz.
+Authentication is OIDC, and Keycloak is used for issuing and verifying tokens.
+Authorization is based on URL patterns, and Keycloak is used for defining and enforcing restrictions.
+
+A simple Keycloak realm with 1 client (protected application), 2 users, 2 roles and 3 protected resources is provided in `test-realm.json`.
+
### `security/keycloak-webapp`
Verifies authorization code flow and role-based authentication to protect web applications.
diff --git a/pom.xml b/pom.xml
index 04687d7a7..6083d5997 100644
--- a/pom.xml
+++ b/pom.xml
@@ -397,7 +397,8 @@
security/https
security/jwt
security/keycloak
- security/keycloak-authz
+ security/keycloak-authz-classic
+ security/keycloak-authz-reactive
security/keycloak-jwt
security/keycloak-webapp
security/keycloak-oauth2
diff --git a/security/keycloak-authz/pom.xml b/security/keycloak-authz-classic/pom.xml
similarity index 96%
rename from security/keycloak-authz/pom.xml
rename to security/keycloak-authz-classic/pom.xml
index 44725ed0e..a057ce829 100644
--- a/security/keycloak-authz/pom.xml
+++ b/security/keycloak-authz-classic/pom.xml
@@ -9,7 +9,7 @@
security-keycloak-authz
jar
- Quarkus QE TS: Security: Keycloak + Authorization
+ Quarkus QE TS: Security: Keycloak + Authorization + Classic
io.quarkus
diff --git a/security/keycloak-authz/src/main/java/io/quarkus/ts/security/keycloak/authz/AdminResource.java b/security/keycloak-authz-classic/src/main/java/io/quarkus/ts/security/keycloak/authz/AdminResource.java
similarity index 100%
rename from security/keycloak-authz/src/main/java/io/quarkus/ts/security/keycloak/authz/AdminResource.java
rename to security/keycloak-authz-classic/src/main/java/io/quarkus/ts/security/keycloak/authz/AdminResource.java
diff --git a/security/keycloak-authz/src/main/java/io/quarkus/ts/security/keycloak/authz/UserResource.java b/security/keycloak-authz-classic/src/main/java/io/quarkus/ts/security/keycloak/authz/UserResource.java
similarity index 100%
rename from security/keycloak-authz/src/main/java/io/quarkus/ts/security/keycloak/authz/UserResource.java
rename to security/keycloak-authz-classic/src/main/java/io/quarkus/ts/security/keycloak/authz/UserResource.java
diff --git a/security/keycloak-authz/src/main/resources/application.properties b/security/keycloak-authz-classic/src/main/resources/application.properties
similarity index 100%
rename from security/keycloak-authz/src/main/resources/application.properties
rename to security/keycloak-authz-classic/src/main/resources/application.properties
diff --git a/security/keycloak-authz/src/test/java/io/quarkus/ts/security/keycloak/authz/BaseAuthzSecurityIT.java b/security/keycloak-authz-classic/src/test/java/io/quarkus/ts/security/keycloak/authz/BaseAuthzSecurityIT.java
similarity index 100%
rename from security/keycloak-authz/src/test/java/io/quarkus/ts/security/keycloak/authz/BaseAuthzSecurityIT.java
rename to security/keycloak-authz-classic/src/test/java/io/quarkus/ts/security/keycloak/authz/BaseAuthzSecurityIT.java
diff --git a/security/keycloak-authz/src/test/java/io/quarkus/ts/security/keycloak/authz/KeycloakAuthzSecurityIT.java b/security/keycloak-authz-classic/src/test/java/io/quarkus/ts/security/keycloak/authz/KeycloakAuthzSecurityIT.java
similarity index 100%
rename from security/keycloak-authz/src/test/java/io/quarkus/ts/security/keycloak/authz/KeycloakAuthzSecurityIT.java
rename to security/keycloak-authz-classic/src/test/java/io/quarkus/ts/security/keycloak/authz/KeycloakAuthzSecurityIT.java
diff --git a/security/keycloak-authz/src/test/java/io/quarkus/ts/security/keycloak/authz/OpenShiftRhSso73AuthzSecurityIT.java b/security/keycloak-authz-classic/src/test/java/io/quarkus/ts/security/keycloak/authz/OpenShiftRhSso73AuthzSecurityIT.java
similarity index 100%
rename from security/keycloak-authz/src/test/java/io/quarkus/ts/security/keycloak/authz/OpenShiftRhSso73AuthzSecurityIT.java
rename to security/keycloak-authz-classic/src/test/java/io/quarkus/ts/security/keycloak/authz/OpenShiftRhSso73AuthzSecurityIT.java
diff --git a/security/keycloak-authz/src/test/java/io/quarkus/ts/security/keycloak/authz/OpenShiftRhSso74AuthzSecurityIT.java b/security/keycloak-authz-classic/src/test/java/io/quarkus/ts/security/keycloak/authz/OpenShiftRhSso74AuthzSecurityIT.java
similarity index 100%
rename from security/keycloak-authz/src/test/java/io/quarkus/ts/security/keycloak/authz/OpenShiftRhSso74AuthzSecurityIT.java
rename to security/keycloak-authz-classic/src/test/java/io/quarkus/ts/security/keycloak/authz/OpenShiftRhSso74AuthzSecurityIT.java
diff --git a/security/keycloak-authz/src/test/resources/keycloak-realm.json b/security/keycloak-authz-classic/src/test/resources/keycloak-realm.json
similarity index 100%
rename from security/keycloak-authz/src/test/resources/keycloak-realm.json
rename to security/keycloak-authz-classic/src/test/resources/keycloak-realm.json
diff --git a/security/keycloak-authz/src/test/resources/test.properties b/security/keycloak-authz-classic/src/test/resources/test.properties
similarity index 100%
rename from security/keycloak-authz/src/test/resources/test.properties
rename to security/keycloak-authz-classic/src/test/resources/test.properties
diff --git a/security/keycloak-authz-reactive/pom.xml b/security/keycloak-authz-reactive/pom.xml
new file mode 100644
index 000000000..4c2254083
--- /dev/null
+++ b/security/keycloak-authz-reactive/pom.xml
@@ -0,0 +1,60 @@
+
+
+ 4.0.0
+
+ io.quarkus.ts.qe
+ parent
+ 1.0.0-SNAPSHOT
+ ../..
+
+ security-keycloak-authz-reactive
+ jar
+ Quarkus QE TS: Security: Keycloak + Authorization + Reactive
+
+
+ io.quarkus
+ quarkus-resteasy-reactive
+
+
+ io.quarkus
+ quarkus-keycloak-authorization
+
+
+ org.apache.httpcomponents
+ httpclient
+ test
+
+
+ io.quarkus.qe
+ quarkus-test-service-keycloak
+ test
+
+
+
+
+
+ skip-tests-on-windows
+
+
+ windows
+
+
+
+
+
+ maven-surefire-plugin
+
+ true
+
+
+
+ maven-failsafe-plugin
+
+ true
+
+
+
+
+
+
+
diff --git a/security/keycloak-authz-reactive/src/main/java/io/quarkus/ts/security/keycloak/authz/AdminResource.java b/security/keycloak-authz-reactive/src/main/java/io/quarkus/ts/security/keycloak/authz/AdminResource.java
new file mode 100644
index 000000000..01e99f8e8
--- /dev/null
+++ b/security/keycloak-authz-reactive/src/main/java/io/quarkus/ts/security/keycloak/authz/AdminResource.java
@@ -0,0 +1,34 @@
+package io.quarkus.ts.security.keycloak.authz;
+
+import javax.inject.Inject;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+import org.eclipse.microprofile.jwt.JsonWebToken;
+
+import io.quarkus.security.identity.SecurityIdentity;
+import io.smallrye.mutiny.Uni;
+
+@Path("/admin")
+public class AdminResource {
+ @Inject
+ SecurityIdentity identity;
+
+ @Inject
+ JsonWebToken jwt;
+
+ @GET
+ @Produces(MediaType.TEXT_PLAIN)
+ public Uni get() {
+ return Uni.createFrom().item("Hello, admin " + identity.getPrincipal().getName());
+ }
+
+ @GET
+ @Path("/issuer")
+ @Produces(MediaType.TEXT_PLAIN)
+ public Uni issuer() {
+ return Uni.createFrom().item("admin token issued by " + jwt.getIssuer());
+ }
+}
diff --git a/security/keycloak-authz-reactive/src/main/java/io/quarkus/ts/security/keycloak/authz/UserAdvancedResource.java b/security/keycloak-authz-reactive/src/main/java/io/quarkus/ts/security/keycloak/authz/UserAdvancedResource.java
new file mode 100644
index 000000000..8a11bea57
--- /dev/null
+++ b/security/keycloak-authz-reactive/src/main/java/io/quarkus/ts/security/keycloak/authz/UserAdvancedResource.java
@@ -0,0 +1,16 @@
+package io.quarkus.ts.security.keycloak.authz;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+
+import io.quarkus.security.Authenticated;
+import io.smallrye.mutiny.Uni;
+
+@Path("/user-details")
+@Authenticated
+public class UserAdvancedResource extends UserDetailsResource {
+ @GET
+ public Uni info() {
+ return Uni.createFrom().item(identity.getPrincipal().getName());
+ }
+}
diff --git a/security/keycloak-authz-reactive/src/main/java/io/quarkus/ts/security/keycloak/authz/UserDetailsResource.java b/security/keycloak-authz-reactive/src/main/java/io/quarkus/ts/security/keycloak/authz/UserDetailsResource.java
new file mode 100644
index 000000000..106ead273
--- /dev/null
+++ b/security/keycloak-authz-reactive/src/main/java/io/quarkus/ts/security/keycloak/authz/UserDetailsResource.java
@@ -0,0 +1,33 @@
+package io.quarkus.ts.security.keycloak.authz;
+
+import javax.inject.Inject;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+import io.quarkus.security.identity.SecurityIdentity;
+import io.smallrye.mutiny.Uni;
+
+public class UserDetailsResource {
+
+ @Inject
+ SecurityIdentity identity;
+
+ @Path("/advanced")
+ @POST
+ @Produces(MediaType.TEXT_PLAIN)
+ @Consumes(MediaType.TEXT_PLAIN)
+ public Uni getUserName(T body) {
+ return Uni.createFrom().item(identity.getPrincipal().getName());
+ }
+
+ @Path("/advanced-specific")
+ @POST
+ @Produces(MediaType.TEXT_PLAIN)
+ @Consumes(MediaType.TEXT_PLAIN)
+ public Uni WebAuthnSpecific(String body) {
+ return Uni.createFrom().item(identity.getPrincipal().getName());
+ }
+}
diff --git a/security/keycloak-authz-reactive/src/main/java/io/quarkus/ts/security/keycloak/authz/UserResource.java b/security/keycloak-authz-reactive/src/main/java/io/quarkus/ts/security/keycloak/authz/UserResource.java
new file mode 100644
index 000000000..4270e15c3
--- /dev/null
+++ b/security/keycloak-authz-reactive/src/main/java/io/quarkus/ts/security/keycloak/authz/UserResource.java
@@ -0,0 +1,34 @@
+package io.quarkus.ts.security.keycloak.authz;
+
+import javax.inject.Inject;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+import org.eclipse.microprofile.jwt.JsonWebToken;
+
+import io.quarkus.security.identity.SecurityIdentity;
+import io.smallrye.mutiny.Uni;
+
+@Path("/user")
+public class UserResource {
+ @Inject
+ SecurityIdentity identity;
+
+ @Inject
+ JsonWebToken jwt;
+
+ @GET
+ @Produces(MediaType.TEXT_PLAIN)
+ public Uni get() {
+ return Uni.createFrom().item("Hello, user " + identity.getPrincipal().getName());
+ }
+
+ @GET
+ @Path("/issuer")
+ @Produces(MediaType.TEXT_PLAIN)
+ public Uni issuer() {
+ return Uni.createFrom().item("user token issued by " + jwt.getIssuer());
+ }
+}
diff --git a/security/keycloak-authz-reactive/src/main/resources/application.properties b/security/keycloak-authz-reactive/src/main/resources/application.properties
new file mode 100644
index 000000000..dcad830d4
--- /dev/null
+++ b/security/keycloak-authz-reactive/src/main/resources/application.properties
@@ -0,0 +1,8 @@
+quarkus.oidc.auth-server-url=${KEYCLOAK_HTTP_URL:http://localhost:8180}/auth/realms/test-realm
+quarkus.oidc.client-id=test-application-client
+quarkus.oidc.credentials.secret=test-application-client-secret
+# tolerate 1 minute of clock skew between the Keycloak server and the application
+quarkus.oidc.token.lifespan-grace=60
+quarkus.keycloak.policy-enforcer.enable=true
+quarkus.keycloak.policy-enforcer.paths.health.path=/q/*
+quarkus.keycloak.policy-enforcer.paths.health.enforcement-mode=DISABLED
diff --git a/security/keycloak-authz-reactive/src/test/java/io/quarkus/ts/security/keycloak/authz/BaseAuthzSecurityIT.java b/security/keycloak-authz-reactive/src/test/java/io/quarkus/ts/security/keycloak/authz/BaseAuthzSecurityIT.java
new file mode 100644
index 000000000..9f8f9756f
--- /dev/null
+++ b/security/keycloak-authz-reactive/src/test/java/io/quarkus/ts/security/keycloak/authz/BaseAuthzSecurityIT.java
@@ -0,0 +1,148 @@
+package io.quarkus.ts.security.keycloak.authz;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.startsWith;
+
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.platform.commons.util.StringUtils;
+import org.keycloak.authorization.client.AuthzClient;
+
+import io.quarkus.test.bootstrap.KeycloakService;
+import io.quarkus.test.bootstrap.RestService;
+import io.smallrye.mutiny.helpers.test.UniAssertSubscriber;
+import io.vertx.core.http.HttpMethod;
+import io.vertx.mutiny.core.buffer.Buffer;
+import io.vertx.mutiny.ext.web.client.HttpRequest;
+import io.vertx.mutiny.ext.web.client.HttpResponse;
+
+public abstract class BaseAuthzSecurityIT {
+
+ static final String NORMAL_USER = "test-normal-user";
+ static final String ADMIN_USER = "test-admin-user";
+ static final String REALM_DEFAULT = "test-realm";
+ static final String CLIENT_ID_DEFAULT = "test-application-client";
+ static final String CLIENT_SECRET_DEFAULT = "test-application-client-secret";
+
+ private AuthzClient authzClient;
+ private UniAssertSubscriber> response;
+
+ @BeforeEach
+ public void setup() {
+ authzClient = getKeycloak().createAuthzClient(CLIENT_ID_DEFAULT, CLIENT_SECRET_DEFAULT);
+ }
+
+ @Tag("QUARKUS-1257")
+ @Test
+ public void genericAndExtendedSecuredEndpointShouldResponseOk() {
+ String bearerToken = getToken(NORMAL_USER, NORMAL_USER);
+
+ whenMakeRequestTo(HttpMethod.GET, "/user-details", bearerToken);
+ thenStatusCodeIs(HttpStatus.SC_OK);
+ thenBodyIs(NORMAL_USER);
+
+ whenMakeRequestTo(HttpMethod.POST, "/user-details/advanced-specific", bearerToken);
+ thenStatusCodeIs(HttpStatus.SC_OK);
+ thenBodyIs(NORMAL_USER);
+
+ whenMakeRequestTo(HttpMethod.POST, "/user-details/advanced", bearerToken);
+ thenStatusCodeIs(HttpStatus.SC_OK);
+ thenBodyIs(NORMAL_USER);
+ }
+
+ @Test
+ public void normalUserUserResource() {
+ whenMakeRequestTo(HttpMethod.GET, "/user", getToken(NORMAL_USER, NORMAL_USER));
+ thenStatusCodeIs(HttpStatus.SC_OK);
+ thenBodyIs("Hello, user " + NORMAL_USER);
+ }
+
+ @Test
+ public void normalUserUserResourceIssuer() {
+ whenMakeRequestTo(HttpMethod.GET, "/user/issuer", getToken(NORMAL_USER, NORMAL_USER));
+ thenStatusCodeIs(HttpStatus.SC_OK);
+ thenBodyStartWith("user token issued by " + getKeycloak().getHost());
+ }
+
+ @Test
+ public void normalUserAdminResource() {
+ whenMakeRequestTo(HttpMethod.GET, "/admin", getToken(NORMAL_USER, NORMAL_USER));
+ thenStatusCodeIs(HttpStatus.SC_FORBIDDEN);
+ }
+
+ @Test
+ public void adminUserUserResource() {
+ whenMakeRequestTo(HttpMethod.GET, "/user", getToken(ADMIN_USER, ADMIN_USER));
+ thenStatusCodeIs(HttpStatus.SC_OK);
+ thenBodyIs("Hello, user " + ADMIN_USER);
+ }
+
+ @Test
+ public void adminUserAdminResource() {
+ whenMakeRequestTo(HttpMethod.GET, "/admin", getToken(ADMIN_USER, ADMIN_USER));
+ thenStatusCodeIs(HttpStatus.SC_OK);
+ thenBodyIs("Hello, admin " + ADMIN_USER);
+ }
+
+ @Test
+ public void adminUserAdminResourceIssuer() {
+ whenMakeRequestTo(HttpMethod.GET, "/admin/issuer", getToken(ADMIN_USER, ADMIN_USER));
+ thenStatusCodeIs(HttpStatus.SC_OK);
+ thenBodyStartWith("admin token issued by " + getKeycloak().getHost());
+ }
+
+ @Test
+ public void noUserUserResource() {
+ whenMakeRequestTo(HttpMethod.GET, "/user", "");
+ thenStatusCodeIs(HttpStatus.SC_UNAUTHORIZED);
+ }
+
+ @Test
+ public void noUserAdminResource() {
+ whenMakeRequestTo(HttpMethod.GET, "/admin", "");
+ thenStatusCodeIs(HttpStatus.SC_UNAUTHORIZED);
+ }
+
+ private void thenBodyIs(String expectedBody) {
+ String bodyAsString = response.await().assertCompleted().getItem().bodyAsString();
+ assertThat(expectedBody, equalTo(bodyAsString));
+ }
+
+ private void thenBodyStartWith(String expectedBody) {
+ String bodyAsString = response.await().assertCompleted().getItem().bodyAsString();
+ assertThat(bodyAsString, startsWith(expectedBody));
+ }
+
+ private void thenStatusCodeIs(int expectedCode) {
+ int statusCode = response.await().assertCompleted().getItem().statusCode();
+ assertThat(expectedCode, equalTo(statusCode));
+ }
+
+ private void whenMakeRequestTo(HttpMethod method, String path, String bearerToken) {
+ HttpRequest req = createHttpRequest(method, path);
+ if (StringUtils.isNotBlank(bearerToken)) {
+ addHttpRequestBearerToken(req, bearerToken);
+ }
+
+ response = req.send().subscribe().withSubscriber(UniAssertSubscriber.create());
+ }
+
+ private HttpRequest createHttpRequest(HttpMethod method, String path) {
+ return getApp().mutiny().request(method, path);
+ }
+
+ private HttpRequest addHttpRequestBearerToken(HttpRequest req, String token) {
+ return req.bearerTokenAuthentication(token);
+ }
+
+ protected abstract KeycloakService getKeycloak();
+
+ protected abstract RestService getApp();
+
+ private String getToken(String userName, String password) {
+ return authzClient.obtainAccessToken(userName, password).getToken();
+ }
+}
diff --git a/security/keycloak-authz-reactive/src/test/java/io/quarkus/ts/security/keycloak/authz/KeycloakAuthzSecurityIT.java b/security/keycloak-authz-reactive/src/test/java/io/quarkus/ts/security/keycloak/authz/KeycloakAuthzSecurityIT.java
new file mode 100644
index 000000000..75647b2b6
--- /dev/null
+++ b/security/keycloak-authz-reactive/src/test/java/io/quarkus/ts/security/keycloak/authz/KeycloakAuthzSecurityIT.java
@@ -0,0 +1,32 @@
+package io.quarkus.ts.security.keycloak.authz;
+
+import io.quarkus.test.bootstrap.KeycloakService;
+import io.quarkus.test.bootstrap.RestService;
+import io.quarkus.test.scenarios.QuarkusScenario;
+import io.quarkus.test.services.Container;
+import io.quarkus.test.services.QuarkusApplication;
+
+@QuarkusScenario
+public class KeycloakAuthzSecurityIT extends BaseAuthzSecurityIT {
+
+ static final int KEYCLOAK_PORT = 8080;
+
+ @Container(image = "${keycloak.image}", expectedLog = "Http management interface listening", port = KEYCLOAK_PORT)
+ static KeycloakService keycloak = new KeycloakService("/keycloak-realm.json", REALM_DEFAULT);
+
+ @QuarkusApplication
+ static RestService app = new RestService()
+ .withProperty("quarkus.oidc.auth-server-url", () -> keycloak.getRealmUrl())
+ .withProperty("quarkus.oidc.client-id", CLIENT_ID_DEFAULT)
+ .withProperty("quarkus.oidc.credentials.secret", CLIENT_SECRET_DEFAULT);
+
+ @Override
+ protected KeycloakService getKeycloak() {
+ return keycloak;
+ }
+
+ @Override
+ protected RestService getApp() {
+ return app;
+ }
+}
diff --git a/security/keycloak-authz-reactive/src/test/java/io/quarkus/ts/security/keycloak/authz/OpenShiftRhSso73AuthzSecurityIT.java b/security/keycloak-authz-reactive/src/test/java/io/quarkus/ts/security/keycloak/authz/OpenShiftRhSso73AuthzSecurityIT.java
new file mode 100644
index 000000000..5f930a954
--- /dev/null
+++ b/security/keycloak-authz-reactive/src/test/java/io/quarkus/ts/security/keycloak/authz/OpenShiftRhSso73AuthzSecurityIT.java
@@ -0,0 +1,33 @@
+package io.quarkus.ts.security.keycloak.authz;
+
+import io.quarkus.test.bootstrap.KeycloakService;
+import io.quarkus.test.bootstrap.RestService;
+import io.quarkus.test.scenarios.OpenShiftScenario;
+import io.quarkus.test.services.Container;
+import io.quarkus.test.services.QuarkusApplication;
+
+@OpenShiftScenario
+public class OpenShiftRhSso73AuthzSecurityIT extends BaseAuthzSecurityIT {
+
+ static final int KEYCLOAK_PORT = 8080;
+
+ @Container(image = "${rhsso.73.image}", expectedLog = "Http management interface listening", port = KEYCLOAK_PORT)
+ static KeycloakService keycloak = new KeycloakService(REALM_DEFAULT)
+ .withProperty("SSO_IMPORT_FILE", "resource::/keycloak-realm.json");
+
+ @QuarkusApplication
+ static RestService app = new RestService()
+ .withProperty("quarkus.oidc.auth-server-url", () -> keycloak.getRealmUrl())
+ .withProperty("quarkus.oidc.client-id", CLIENT_ID_DEFAULT)
+ .withProperty("quarkus.oidc.credentials.secret", CLIENT_SECRET_DEFAULT);
+
+ @Override
+ protected KeycloakService getKeycloak() {
+ return keycloak;
+ }
+
+ @Override
+ protected RestService getApp() {
+ return app;
+ }
+}
diff --git a/security/keycloak-authz-reactive/src/test/java/io/quarkus/ts/security/keycloak/authz/OpenShiftRhSso74AuthzSecurityIT.java b/security/keycloak-authz-reactive/src/test/java/io/quarkus/ts/security/keycloak/authz/OpenShiftRhSso74AuthzSecurityIT.java
new file mode 100644
index 000000000..1851436b7
--- /dev/null
+++ b/security/keycloak-authz-reactive/src/test/java/io/quarkus/ts/security/keycloak/authz/OpenShiftRhSso74AuthzSecurityIT.java
@@ -0,0 +1,36 @@
+package io.quarkus.ts.security.keycloak.authz;
+
+import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
+
+import io.quarkus.test.bootstrap.KeycloakService;
+import io.quarkus.test.bootstrap.RestService;
+import io.quarkus.test.scenarios.OpenShiftScenario;
+import io.quarkus.test.services.Container;
+import io.quarkus.test.services.QuarkusApplication;
+
+@OpenShiftScenario
+@EnabledIfSystemProperty(named = "ts.redhat.registry.enabled", matches = "true")
+public class OpenShiftRhSso74AuthzSecurityIT extends BaseAuthzSecurityIT {
+
+ static final int KEYCLOAK_PORT = 8080;
+
+ @Container(image = "${rhsso.74.image}", expectedLog = "Http management interface listening", port = KEYCLOAK_PORT)
+ static KeycloakService keycloak = new KeycloakService(REALM_DEFAULT)
+ .withProperty("SSO_IMPORT_FILE", "resource::/keycloak-realm.json");
+
+ @QuarkusApplication
+ static RestService app = new RestService()
+ .withProperty("quarkus.oidc.auth-server-url", () -> keycloak.getRealmUrl())
+ .withProperty("quarkus.oidc.client-id", CLIENT_ID_DEFAULT)
+ .withProperty("quarkus.oidc.credentials.secret", CLIENT_SECRET_DEFAULT);
+
+ @Override
+ protected KeycloakService getKeycloak() {
+ return keycloak;
+ }
+
+ @Override
+ protected RestService getApp() {
+ return app;
+ }
+}
diff --git a/security/keycloak-authz-reactive/src/test/resources/keycloak-realm.json b/security/keycloak-authz-reactive/src/test/resources/keycloak-realm.json
new file mode 100644
index 000000000..b37fd1f0f
--- /dev/null
+++ b/security/keycloak-authz-reactive/src/test/resources/keycloak-realm.json
@@ -0,0 +1,105 @@
+{
+ "realm": "test-realm",
+ "enabled": true,
+ "sslRequired": "none",
+ "roles": {
+ "realm": [
+ {
+ "name": "test-user-role"
+ },
+ {
+ "name": "test-admin-role"
+ }
+ ]
+ },
+ "users": [
+ {
+ "username": "test-normal-user",
+ "enabled": true,
+ "credentials": [
+ {
+ "type": "password",
+ "value": "test-normal-user"
+ }
+ ],
+ "realmRoles": [
+ "test-user-role"
+ ]
+ },
+ {
+ "username": "test-admin-user",
+ "enabled": true,
+ "credentials": [
+ {
+ "type": "password",
+ "value": "test-admin-user"
+ }
+ ],
+ "realmRoles": [
+ "test-admin-role",
+ "test-user-role"
+ ]
+ }
+ ],
+ "clients": [
+ {
+ "clientId": "test-application-client",
+ "enabled": true,
+ "protocol": "openid-connect",
+ "standardFlowEnabled": false,
+ "implicitFlowEnabled": false,
+ "directAccessGrantsEnabled": true,
+ "clientAuthenticatorType": "client-secret",
+ "secret": "test-application-client-secret",
+ "authorizationSettings": {
+ "resources": [
+ {
+ "name": "test-user-resource",
+ "uris": [
+ "/user/*",
+ "/user-details/*"
+ ]
+ },
+ {
+ "name": "test-admin-resource",
+ "uris": [
+ "/admin/*"
+ ]
+ }
+ ],
+ "policies": [
+ {
+ "name": "test-user-policy",
+ "type": "role",
+ "config": {
+ "roles": "[{\"id\":\"test-user-role\",\"required\":true}]"
+ }
+ },
+ {
+ "name": "test-admin-policy",
+ "type": "role",
+ "config": {
+ "roles": "[{\"id\":\"test-admin-role\",\"required\":true}]"
+ }
+ },
+ {
+ "name": "test-user-permission",
+ "type": "resource",
+ "config": {
+ "resources": "[\"test-user-resource\"]",
+ "applyPolicies": "[\"test-user-policy\"]"
+ }
+ },
+ {
+ "name": "test-admin-permission",
+ "type": "resource",
+ "config": {
+ "resources": "[\"test-admin-resource\"]",
+ "applyPolicies": "[\"test-admin-policy\"]"
+ }
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/security/keycloak-authz-reactive/src/test/resources/test.properties b/security/keycloak-authz-reactive/src/test/resources/test.properties
new file mode 100644
index 000000000..66b0dc508
--- /dev/null
+++ b/security/keycloak-authz-reactive/src/test/resources/test.properties
@@ -0,0 +1,2 @@
+ts.keycloak.log.enable=true
+ts.app.log.enable=true