diff --git a/bom/runtime/pom.xml b/bom/runtime/pom.xml
index 452734184b14a..5836fb654cf7a 100644
--- a/bom/runtime/pom.xml
+++ b/bom/runtime/pom.xml
@@ -473,6 +473,11 @@
quarkus-security
${project.version}
+
+ io.quarkus
+ quarkus-security-runtime-spi
+ ${project.version}
+
io.quarkus
quarkus-security-deployment
@@ -1787,6 +1792,11 @@
quarkus-junit5
${project.version}
+
+ io.quarkus
+ quarkus-test-security
+ ${project.version}
+
io.quarkus
quarkus-junit5-internal
diff --git a/docs/src/main/asciidoc/security.adoc b/docs/src/main/asciidoc/security.adoc
index 2b6b769c755bb..a014dcafa4230 100644
--- a/docs/src/main/asciidoc/security.adoc
+++ b/docs/src/main/asciidoc/security.adoc
@@ -411,3 +411,50 @@ This will allow you to propagate the identity throughout the reactive callbacks.
are using an executor that is capable of propagating the identity (e.g. no `CompletableFuture.supplyAsync`),
to make sure that quarkus can propagate it. For more information see the
link:context-propagation[Context Propagation Guide].
+
+== Testing Security
+
+Quarkus provides explicit support for testing with different users, and with the security subsystem disabled. To use
+this you must include the `quarkus-test-security` artifact:
+
+[source,xml]
+----
+
+ io.quarkus
+ quarkus-test-security
+ test
+
+----
+
+This artifact provides the `io.quarkus.test.security.TestSecurity` annotation, that can be applied to test methods and
+test classes to control the security context that the test is run with. This allows you to do two things, you can disable
+authorization so tests can access secured endpoints without needing to be authenticated, and you can specify the identity
+that you want the tests to run under.
+
+A test that runs with authorization disabled can just set the enabled property to false:
+
+[source,java]
+----
+@Test
+@TestSecurity(authorizationEnabled = false)
+void someTestMethod() {
+...
+}
+----
+
+This will disable all access checks, which allows the test to access secured endpoints without needing to authenticate.
+
+You can also use this to configure the current user that the test will run as:
+
+[source,java]
+----
+@Test
+@TestSecurity(user = "testUser", roles = {"admin", "user"})
+void someTestMethod() {
+...
+}
+----
+
+This will run the test with an identity with the given username and roles. Note that these can be combined, so you can
+disable authorisation and also provide an identity to run the test under, which can be userful if the endpoint expects an
+identity to be present.
\ No newline at end of file
diff --git a/extensions/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/UnauthorizedExceptionMapper.java b/extensions/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/UnauthorizedExceptionMapper.java
index 70d305e404bc7..0220d06890b22 100644
--- a/extensions/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/UnauthorizedExceptionMapper.java
+++ b/extensions/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/UnauthorizedExceptionMapper.java
@@ -43,11 +43,15 @@ public Response toResponse(UnauthorizedException exception) {
if (authenticator != null) {
ChallengeData challengeData = authenticator.getChallenge(context)
.await().indefinitely();
- Response.ResponseBuilder status = Response.status(challengeData.status);
- if (challengeData.headerName != null) {
- status.header(challengeData.headerName.toString(), challengeData.headerContent);
+ if (challengeData != null) {
+ Response.ResponseBuilder status = Response.status(challengeData.status);
+ if (challengeData.headerName != null) {
+ status.header(challengeData.headerName.toString(), challengeData.headerContent);
+ }
+ return status.build();
+ } else {
+ return Response.status(401).build();
}
- return status.build();
}
}
return Response.status(401).entity("Not authorized").build();
diff --git a/extensions/security/deployment/pom.xml b/extensions/security/deployment/pom.xml
index 4539e6116bfee..9214343675a7a 100644
--- a/extensions/security/deployment/pom.xml
+++ b/extensions/security/deployment/pom.xml
@@ -18,6 +18,10 @@
io.quarkus
quarkus-arc-deployment
+
+ io.quarkus
+ quarkus-security-runtime-spi
+
io.quarkus
quarkus-security
diff --git a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java
index e2606144bd777..96d417eb6ce4b 100644
--- a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java
+++ b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java
@@ -63,6 +63,7 @@
import io.quarkus.security.runtime.interceptor.SecurityHandler;
import io.quarkus.security.runtime.interceptor.check.SecurityCheck;
import io.quarkus.security.spi.AdditionalSecuredClassesBuildIem;
+import io.quarkus.security.spi.runtime.AuthorizationController;
public class SecurityProcessor {
@@ -83,6 +84,11 @@ void services(BuildProducer jcaProviders) {
}
}
+ @BuildStep
+ AdditionalBeanBuildItem authorizationController() {
+ return AdditionalBeanBuildItem.builder().addBeanClass(AuthorizationController.class).build();
+ }
+
/**
* Register the classes for reflection in the requested named providers
*
diff --git a/extensions/security/pom.xml b/extensions/security/pom.xml
index 0bdc3350a88df..022f2c65b4252 100644
--- a/extensions/security/pom.xml
+++ b/extensions/security/pom.xml
@@ -18,5 +18,6 @@
runtime
spi
test-utils
+ runtime-spi
diff --git a/extensions/security/runtime-spi/pom.xml b/extensions/security/runtime-spi/pom.xml
new file mode 100644
index 0000000000000..2f71d8bdae548
--- /dev/null
+++ b/extensions/security/runtime-spi/pom.xml
@@ -0,0 +1,39 @@
+
+
+
+ quarkus-security-parent
+ io.quarkus
+ 999-SNAPSHOT
+ ../
+
+ 4.0.0
+
+ quarkus-security-runtime-spi
+ Quarkus - Security - Runtime SPI
+
+
+
+ io.quarkus
+ quarkus-core
+
+
+
+
+
+
+ maven-compiler-plugin
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${project.version}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/AuthorizationController.java b/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/AuthorizationController.java
new file mode 100644
index 0000000000000..97c441257d855
--- /dev/null
+++ b/extensions/security/runtime-spi/src/main/java/io/quarkus/security/spi/runtime/AuthorizationController.java
@@ -0,0 +1,14 @@
+package io.quarkus.security.spi.runtime;
+
+import javax.inject.Singleton;
+
+/**
+ * controller that allows authorization to be disabled in tests.
+ */
+@Singleton
+public class AuthorizationController {
+
+ public boolean isAuthorizationEnabled() {
+ return true;
+ }
+}
diff --git a/extensions/security/runtime/pom.xml b/extensions/security/runtime/pom.xml
index 186fc9fcfc1ab..ca9190ddff144 100644
--- a/extensions/security/runtime/pom.xml
+++ b/extensions/security/runtime/pom.xml
@@ -18,6 +18,10 @@
io.quarkus
quarkus-arc
+
+ io.quarkus
+ quarkus-security-runtime-spi
+
jakarta.interceptor
jakarta.interceptor-api
diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/AuthenticatedInterceptor.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/AuthenticatedInterceptor.java
index cb900f3efc093..59aa863b2bc73 100644
--- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/AuthenticatedInterceptor.java
+++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/AuthenticatedInterceptor.java
@@ -7,6 +7,7 @@
import javax.interceptor.InvocationContext;
import io.quarkus.security.Authenticated;
+import io.quarkus.security.spi.runtime.AuthorizationController;
/**
* @author Michal Szynkiewicz, michal.l.szynkiewicz@gmail.com
@@ -19,8 +20,15 @@ public class AuthenticatedInterceptor {
@Inject
SecurityHandler handler;
+ @Inject
+ AuthorizationController controller;
+
@AroundInvoke
public Object intercept(InvocationContext ic) throws Exception {
- return handler.handle(ic);
+ if (controller.isAuthorizationEnabled()) {
+ return handler.handle(ic);
+ } else {
+ return ic.proceed();
+ }
}
}
diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/RolesAllowedInterceptor.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/RolesAllowedInterceptor.java
index 3d86997dc63f1..a0931f204fa38 100644
--- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/RolesAllowedInterceptor.java
+++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/RolesAllowedInterceptor.java
@@ -7,6 +7,8 @@
import javax.interceptor.Interceptor;
import javax.interceptor.InvocationContext;
+import io.quarkus.security.spi.runtime.AuthorizationController;
+
/**
* @author Michal Szynkiewicz, michal.l.szynkiewicz@gmail.com
*/
@@ -18,8 +20,15 @@ public class RolesAllowedInterceptor {
@Inject
SecurityHandler handler;
+ @Inject
+ AuthorizationController controller;
+
@AroundInvoke
public Object intercept(InvocationContext ic) throws Exception {
- return handler.handle(ic);
+ if (controller.isAuthorizationEnabled()) {
+ return handler.handle(ic);
+ } else {
+ return ic.proceed();
+ }
}
}
diff --git a/extensions/vertx-http/runtime/pom.xml b/extensions/vertx-http/runtime/pom.xml
index f6db46911cb02..8c46a6ea1869e 100644
--- a/extensions/vertx-http/runtime/pom.xml
+++ b/extensions/vertx-http/runtime/pom.xml
@@ -18,6 +18,10 @@
io.quarkus
quarkus-core
+
+ io.quarkus
+ quarkus-security-runtime-spi
+
io.quarkus
quarkus-development-mode-spi
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthorizer.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthorizer.java
index 300f5ee54f57c..2f91fd7df1863 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthorizer.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthorizer.java
@@ -13,6 +13,7 @@
import io.quarkus.runtime.ExecutorRecorder;
import io.quarkus.security.identity.IdentityProviderManager;
import io.quarkus.security.identity.SecurityIdentity;
+import io.quarkus.security.spi.runtime.AuthorizationController;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.subscription.UniEmitter;
import io.smallrye.mutiny.subscription.UniSubscriber;
@@ -31,6 +32,9 @@ public class HttpAuthorizer {
@Inject
IdentityProviderManager identityProviderManager;
+ @Inject
+ AuthorizationController controller;
+
final List policies;
@Inject
@@ -87,6 +91,10 @@ public void run() {
*
*/
public void checkPermission(RoutingContext routingContext) {
+ if (!controller.isAuthorizationEnabled()) {
+ routingContext.next();
+ return;
+ }
//check their permissions
doPermissionCheck(routingContext, QuarkusHttpUser.getSecurityIdentity(routingContext, identityProviderManager), 0, null,
policies);
diff --git a/integration-tests/elytron-resteasy/pom.xml b/integration-tests/elytron-resteasy/pom.xml
index c3800d0f4400c..b05480a300df5 100644
--- a/integration-tests/elytron-resteasy/pom.xml
+++ b/integration-tests/elytron-resteasy/pom.xml
@@ -30,6 +30,11 @@
quarkus-junit5
test
+
+ io.quarkus
+ quarkus-test-security
+ test
+
io.rest-assured
rest-assured
diff --git a/integration-tests/elytron-resteasy/src/main/java/io/quarkus/it/resteasy/elytron/RootResource.java b/integration-tests/elytron-resteasy/src/main/java/io/quarkus/it/resteasy/elytron/RootResource.java
index 868445796d51b..a95deb79c4457 100644
--- a/integration-tests/elytron-resteasy/src/main/java/io/quarkus/it/resteasy/elytron/RootResource.java
+++ b/integration-tests/elytron-resteasy/src/main/java/io/quarkus/it/resteasy/elytron/RootResource.java
@@ -1,5 +1,6 @@
package io.quarkus.it.resteasy.elytron;
+import javax.annotation.security.RolesAllowed;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
@@ -9,6 +10,8 @@
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.SecurityContext;
+import io.quarkus.security.Authenticated;
+
@Path("/")
public class RootResource {
@@ -32,4 +35,19 @@ public String approval(@Context SecurityContext sec) {
}
return "get success";
}
+
+ @GET
+ @Path("/secure")
+ @Authenticated
+ public String getSecure() {
+ return "secure";
+ }
+
+ @GET
+ @Path("/user")
+ @RolesAllowed("user")
+ public String user(@Context SecurityContext sec) {
+ return sec.getUserPrincipal().getName();
+ }
+
}
diff --git a/integration-tests/elytron-resteasy/src/main/resources/application.properties b/integration-tests/elytron-resteasy/src/main/resources/application.properties
index 8a7b602db668f..d56f07473671d 100644
--- a/integration-tests/elytron-resteasy/src/main/resources/application.properties
+++ b/integration-tests/elytron-resteasy/src/main/resources/application.properties
@@ -5,5 +5,5 @@ quarkus.security.users.embedded.users.mary=mary
quarkus.security.users.embedded.roles.mary=managers
quarkus.security.users.embedded.users.poul=poul
quarkus.security.users.embedded.roles.poul=interns
-quarkus.security.users.embedded.auth-mechanism=BASIC
quarkus.security.users.embedded.plain-text=true
+quarkus.http.auth.basic=true
\ No newline at end of file
diff --git a/integration-tests/elytron-resteasy/src/test/java/io/quarkus/it/resteasy/elytron/TestSecurityTestCase.java b/integration-tests/elytron-resteasy/src/test/java/io/quarkus/it/resteasy/elytron/TestSecurityTestCase.java
new file mode 100644
index 0000000000000..bf1fca0974877
--- /dev/null
+++ b/integration-tests/elytron-resteasy/src/test/java/io/quarkus/it/resteasy/elytron/TestSecurityTestCase.java
@@ -0,0 +1,66 @@
+package io.quarkus.it.resteasy.elytron;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.Matchers.is;
+
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.security.TestSecurity;
+
+@QuarkusTest
+class TestSecurityTestCase {
+
+ @Test
+ @TestSecurity(authorizationEnabled = false)
+ void testGet() {
+ given()
+ .when()
+ .get("/secure")
+ .then()
+ .statusCode(200)
+ .body(is("secure"));
+ }
+
+ @Test
+ @TestSecurity
+ void testGetWithSecEnabled() {
+ given()
+ .when()
+ .get("/secure")
+ .then()
+ .statusCode(401);
+ }
+
+ @Test
+ @TestSecurity(user = "testUser", roles = "wrong")
+ void testGetWithTestUser() {
+ given()
+ .when()
+ .get("/secure")
+ .then()
+ .statusCode(200);
+ }
+
+ @Test
+ @TestSecurity(user = "testUser", roles = "wrong")
+ void testGetWithTestUserwrongRole() {
+ given()
+ .when()
+ .get("/user")
+ .then()
+ .statusCode(403);
+ }
+
+ @Test
+ @TestSecurity(user = "testUser", roles = "user")
+ void testTestUserCorrectRole() {
+ given()
+ .when()
+ .get("/user")
+ .then()
+ .statusCode(200)
+ .body(is("testUser"));
+ }
+
+}
diff --git a/test-framework/pom.xml b/test-framework/pom.xml
index 4f10d79f93fe2..58e65f93aa880 100644
--- a/test-framework/pom.xml
+++ b/test-framework/pom.xml
@@ -28,6 +28,7 @@
maven
vault
ldap
+ security
diff --git a/test-framework/security/pom.xml b/test-framework/security/pom.xml
new file mode 100644
index 0000000000000..56d34268f01da
--- /dev/null
+++ b/test-framework/security/pom.xml
@@ -0,0 +1,55 @@
+
+
+
+ io.quarkus
+ quarkus-test-framework
+ 999-SNAPSHOT
+ ../pom.xml
+
+
+ 4.0.0
+ quarkus-test-security
+ Quarkus - Test Framework - Security
+ Module that contains utilities to test Quarkus security
+
+
+
+ io.quarkus
+ quarkus-junit5
+
+
+ io.quarkus
+ quarkus-security
+
+
+ io.quarkus
+ quarkus-vertx-http
+
+
+ org.junit.jupiter
+ junit-jupiter
+ compile
+
+
+
+
+
+
+
+ org.jboss.jandex
+ jandex-maven-plugin
+
+
+ make-index
+
+ jandex
+
+
+
+
+
+
+
+
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
new file mode 100644
index 0000000000000..ee0d1998ee96d
--- /dev/null
+++ b/test-framework/security/src/main/java/io/quarkus/test/security/QuarkusSecurityTestExtension.java
@@ -0,0 +1,61 @@
+package io.quarkus.test.security;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.HashSet;
+
+import javax.enterprise.inject.spi.CDI;
+
+import io.quarkus.security.runtime.QuarkusPrincipal;
+import io.quarkus.security.runtime.QuarkusSecurityIdentity;
+import io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback;
+import io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback;
+import io.quarkus.test.junit.callback.QuarkusTestMethodContext;
+
+public class QuarkusSecurityTestExtension implements QuarkusTestBeforeEachCallback, QuarkusTestAfterEachCallback {
+
+ @Override
+ public void afterEach(QuarkusTestMethodContext context) {
+ CDI.current().select(TestAuthController.class).get().setEnabled(true);
+ CDI.current().select(TestIdentityAssociation.class).get().setTestIdentity(null);
+ }
+
+ @Override
+ public void beforeEach(QuarkusTestMethodContext context) {
+ try {
+ //the usual ClassLoader hacks to get our copy of the TestSecurity annotation
+ ClassLoader cl = QuarkusSecurityTestExtension.class.getClassLoader();
+ Class> original = cl.loadClass(context.getTestMethod().getDeclaringClass().getName());
+ Class> test = cl.loadClass(context.getTestInstance().getClass().getName());
+ Method method = original.getDeclaredMethod(context.getTestMethod().getName(),
+ Arrays.stream(context.getTestMethod().getParameterTypes()).map(s -> {
+ try {
+ return cl.loadClass(s.getName());
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }).toArray(Class>[]::new));
+ TestSecurity testSecurity = method.getAnnotation(TestSecurity.class);
+ if (testSecurity == null) {
+ testSecurity = test.getAnnotation(TestSecurity.class);
+ }
+ if (testSecurity == null) {
+ return;
+ }
+ CDI.current().select(TestAuthController.class).get().setEnabled(testSecurity.authorizationEnabled());
+ if (testSecurity.user().isEmpty()) {
+ if (testSecurity.roles().length != 0) {
+ throw new RuntimeException("Cannot specify roles without a username in @TestSecurity");
+ }
+ } else {
+ QuarkusSecurityIdentity user = QuarkusSecurityIdentity.builder()
+ .setPrincipal(new QuarkusPrincipal(testSecurity.user()))
+ .addRoles(new HashSet<>(Arrays.asList(testSecurity.roles()))).build();
+ CDI.current().select(TestIdentityAssociation.class).get().setTestIdentity(user);
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Unable to setup @TestSecurity", e);
+ }
+
+ }
+}
diff --git a/test-framework/security/src/main/java/io/quarkus/test/security/TestAuthController.java b/test-framework/security/src/main/java/io/quarkus/test/security/TestAuthController.java
new file mode 100644
index 0000000000000..f9960aec31103
--- /dev/null
+++ b/test-framework/security/src/main/java/io/quarkus/test/security/TestAuthController.java
@@ -0,0 +1,40 @@
+package io.quarkus.test.security;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.Priority;
+import javax.enterprise.context.ApplicationScoped;
+import javax.enterprise.inject.Alternative;
+import javax.interceptor.Interceptor;
+
+import io.quarkus.runtime.LaunchMode;
+import io.quarkus.security.spi.runtime.AuthorizationController;
+
+@Alternative
+@Priority(Interceptor.Priority.LIBRARY_AFTER)
+@ApplicationScoped
+public class TestAuthController extends AuthorizationController {
+
+ @PostConstruct
+ public void check() {
+ if (LaunchMode.current() != LaunchMode.TEST) {
+ //paranoid check
+ throw new RuntimeException("TestAuthController can only be used in tests");
+ }
+ }
+
+ private boolean enabled = true;
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public TestAuthController setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ return this;
+ }
+
+ @Override
+ public boolean isAuthorizationEnabled() {
+ return enabled;
+ }
+}
diff --git a/test-framework/security/src/main/java/io/quarkus/test/security/TestHttpAuthenticationMechanism.java b/test-framework/security/src/main/java/io/quarkus/test/security/TestHttpAuthenticationMechanism.java
new file mode 100644
index 0000000000000..2e77b2c4b7882
--- /dev/null
+++ b/test-framework/security/src/main/java/io/quarkus/test/security/TestHttpAuthenticationMechanism.java
@@ -0,0 +1,53 @@
+package io.quarkus.test.security;
+
+import java.util.Collections;
+import java.util.Set;
+
+import javax.annotation.PostConstruct;
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
+
+import io.quarkus.runtime.LaunchMode;
+import io.quarkus.security.identity.IdentityProviderManager;
+import io.quarkus.security.identity.SecurityIdentity;
+import io.quarkus.security.identity.request.AuthenticationRequest;
+import io.quarkus.vertx.http.runtime.security.ChallengeData;
+import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
+import io.quarkus.vertx.http.runtime.security.HttpCredentialTransport;
+import io.smallrye.mutiny.Uni;
+import io.vertx.ext.web.RoutingContext;
+
+@ApplicationScoped
+public class TestHttpAuthenticationMechanism implements HttpAuthenticationMechanism {
+
+ @Inject
+ TestIdentityAssociation testIdentityAssociation;
+
+ @PostConstruct
+ public void check() {
+ if (LaunchMode.current() != LaunchMode.TEST) {
+ //paranoid check
+ throw new RuntimeException("TestAuthController can only be used in tests");
+ }
+ }
+
+ @Override
+ public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) {
+ return Uni.createFrom().item(testIdentityAssociation.getTestIdentity());
+ }
+
+ @Override
+ public Uni getChallenge(RoutingContext context) {
+ return Uni.createFrom().nullItem();
+ }
+
+ @Override
+ public Set> getCredentialTypes() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public HttpCredentialTransport getCredentialTransport() {
+ return null;
+ }
+}
diff --git a/test-framework/security/src/main/java/io/quarkus/test/security/TestIdentityAssociation.java b/test-framework/security/src/main/java/io/quarkus/test/security/TestIdentityAssociation.java
new file mode 100644
index 0000000000000..3556adf608879
--- /dev/null
+++ b/test-framework/security/src/main/java/io/quarkus/test/security/TestIdentityAssociation.java
@@ -0,0 +1,77 @@
+package io.quarkus.test.security;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.Priority;
+import javax.enterprise.context.ApplicationScoped;
+import javax.enterprise.context.RequestScoped;
+import javax.enterprise.inject.Alternative;
+import javax.inject.Inject;
+import javax.interceptor.Interceptor;
+
+import io.quarkus.runtime.LaunchMode;
+import io.quarkus.security.identity.SecurityIdentity;
+import io.quarkus.security.runtime.SecurityIdentityAssociation;
+import io.smallrye.mutiny.Uni;
+
+@Alternative
+@Priority(Interceptor.Priority.LIBRARY_AFTER)
+@ApplicationScoped
+public class TestIdentityAssociation extends SecurityIdentityAssociation {
+
+ @PostConstruct
+ public void check() {
+ if (LaunchMode.current() != LaunchMode.TEST) {
+ //paranoid check
+ throw new RuntimeException("TestAuthController can only be used in tests");
+ }
+ }
+
+ SecurityIdentity testIdentity;
+
+ /**
+ * A request scoped delegate that allows the system to function as normal when
+ * the user has not been explicitly overridden
+ */
+ @Inject
+ DelegateSecurityIdentityAssociation delegate;
+
+ public SecurityIdentity getTestIdentity() {
+ return testIdentity;
+ }
+
+ public TestIdentityAssociation setTestIdentity(SecurityIdentity testIdentity) {
+ this.testIdentity = testIdentity;
+ return this;
+ }
+
+ @Override
+ public void setIdentity(SecurityIdentity identity) {
+ delegate.setIdentity(identity);
+ }
+
+ @Override
+ public void setIdentity(Uni identity) {
+ delegate.setIdentity(identity);
+ }
+
+ @Override
+ public Uni getDeferredIdentity() {
+ if (testIdentity != null) {
+ return Uni.createFrom().item(testIdentity);
+ }
+ return delegate.getDeferredIdentity();
+ }
+
+ @Override
+ public SecurityIdentity getIdentity() {
+ if (testIdentity != null) {
+ return testIdentity;
+ }
+ return delegate.getIdentity();
+ }
+}
+
+@RequestScoped
+class DelegateSecurityIdentityAssociation extends SecurityIdentityAssociation {
+
+}
diff --git a/test-framework/security/src/main/java/io/quarkus/test/security/TestSecurity.java b/test-framework/security/src/main/java/io/quarkus/test/security/TestSecurity.java
new file mode 100644
index 0000000000000..c2f4414d82102
--- /dev/null
+++ b/test-framework/security/src/main/java/io/quarkus/test/security/TestSecurity.java
@@ -0,0 +1,27 @@
+package io.quarkus.test.security;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.TYPE, ElementType.METHOD })
+public @interface TestSecurity {
+
+ /**
+ * If this is false then all security constraints are disabled.
+ */
+ boolean authorizationEnabled() default true;
+
+ /**
+ * If this is non-zero then the test will be run with a SecurityIdentity with the specified username.
+ */
+ String user() default "";
+
+ /**
+ * Used in combination with {@link #user()} to specify the users roles.
+ */
+ String[] roles() default {};
+
+}
diff --git a/test-framework/security/src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback b/test-framework/security/src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback
new file mode 100644
index 0000000000000..3dec78b0521ba
--- /dev/null
+++ b/test-framework/security/src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback
@@ -0,0 +1 @@
+io.quarkus.test.security.QuarkusSecurityTestExtension
\ No newline at end of file
diff --git a/test-framework/security/src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback b/test-framework/security/src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback
new file mode 100644
index 0000000000000..3dec78b0521ba
--- /dev/null
+++ b/test-framework/security/src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback
@@ -0,0 +1 @@
+io.quarkus.test.security.QuarkusSecurityTestExtension
\ No newline at end of file