diff --git a/docs/src/main/asciidoc/security-built-in-authentication-support-concept.adoc b/docs/src/main/asciidoc/security-built-in-authentication-support-concept.adoc index c3f7ad629d664..d535842ec750a 100644 --- a/docs/src/main/asciidoc/security-built-in-authentication-support-concept.adoc +++ b/docs/src/main/asciidoc/security-built-in-authentication-support-concept.adoc @@ -175,7 +175,39 @@ public class HelloService { === How to customize authentication exception responses -By default, the authentication security constraints are enforced before the JAX-RS chain starts and only way to handle Quarkus Security authentication exceptions is to provide a failure handler like this one: +You can use JAX-RS `ExceptionMapper` to capture Quarkus Security authentication exceptions such as `io.quarkus.security.AuthenticationFailedException`, for example: + +[source,java] +---- +package io.quarkus.it.keycloak; + +import javax.annotation.Priority; +import javax.ws.rs.Priorities; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +import io.quarkus.security.AuthenticationFailedException; + +@Provider +@Priority(Priorities.AUTHENTICATION) +public class AuthenticationFailedExceptionMapper implements ExceptionMapper { + + @Context + UriInfo uriInfo; + + @Override + public Response toResponse(AuthenticationFailedException exception) { + return Response.status(401).header("WWW-Authenticate", "Basic realm=\"Quarkus\"").build(); + } +} +---- + +CAUTION: Some HTTP authentication mechanisms need to handle authentication exceptions themselves in order to create a correct authentication challenge. +For example, `io.quarkus.oidc.runtime.CodeAuthenticationMechanism` which manages OpenId Connect authorization code flow authentication, needs to build a correct redirect URL, cookies, etc. +For that reason, using custom exception mappers to customize authentication exceptions thrown by such mechanisms is not recommended. +In such cases, a safer way to customize authentication exceptions is to make sure the proactive authentication is not disabled and use Vert.x HTTP route failure handlers, as events come to the handler with the correct response status and headers. +To that end, the only thing that needs to be done is to customize the response like this: [source,java] ---- @@ -197,7 +229,7 @@ public class AuthenticationFailedExceptionHandler { @Override public void handle(RoutingContext event) { if (event.failure() instanceof AuthenticationFailedException) { - event.response().setStatusCode(401).end(CUSTOMIZED_RESPONSE); + event.response().end("CUSTOMIZED_RESPONSE"); } else { event.next(); } @@ -207,34 +239,6 @@ public class AuthenticationFailedExceptionHandler { } ---- -Disabling the proactive authentication effectively shifts this process to the moment when the JAX-RS chain starts running thus making it possible to use JAX-RS `ExceptionMapper` to capture Quarkus Security authentication exceptions such as `io.quarkus.security.AuthenticationFailedException`, for example: - -[source,java] ----- -package io.quarkus.it.keycloak; - -import javax.annotation.Priority; -import javax.ws.rs.Priorities; -import javax.ws.rs.core.Response; -import javax.ws.rs.ext.ExceptionMapper; -import javax.ws.rs.ext.Provider; - -import io.quarkus.security.AuthenticationFailedException; - -@Provider -@Priority(Priorities.AUTHENTICATION) -public class AuthenticationFailedExceptionMapper implements ExceptionMapper { - - @Context - UriInfo uriInfo; - - @Override - public Response toResponse(AuthenticationFailedException exception) { - return Response.status(401).header("WWW-Authenticate", "Basic realm=\"Quarkus\"").build(); - } -} ----- - == References * xref:security-overview-concept.adoc[Quarkus Security overview] diff --git a/docs/src/main/asciidoc/security-customization.adoc b/docs/src/main/asciidoc/security-customization.adoc index 0760c51cd7c12..ff3345fa31444 100644 --- a/docs/src/main/asciidoc/security-customization.adoc +++ b/docs/src/main/asciidoc/security-customization.adoc @@ -70,6 +70,7 @@ public class CustomAwareJWTAuthMechanism implements HttpAuthenticationMechanism } ---- +[[dealing-with-more-than-one-http-auth-mechanisms]] == Dealing with more than one HttpAuthenticationMechanism More than one `HttpAuthenticationMechanism` can be combined, for example, the built-in `Basic` or `JWT` mechanism provided by `quarkus-smallrye-jwt` has to be used to verify the service clients credentials passed as the HTTP `Authorization` `Basic` or `Bearer` scheme values while the `Authorization Code` mechanism provided by `quarkus-oidc` has to be used to authenticate the users with Keycloak or other OpenID Connect providers. diff --git a/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyStandaloneBuildStep.java b/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyStandaloneBuildStep.java index 7a8a444f88f68..a78f85e87933d 100644 --- a/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyStandaloneBuildStep.java +++ b/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyStandaloneBuildStep.java @@ -91,10 +91,11 @@ public void boot(ShutdownContextBuildItem shutdown, executorBuildItem.getExecutorProxy(), resteasyVertxConfig); // failure handler for auth failures that occurred before the handler defined right above started processing the request + // we add the failure handler right before QuarkusErrorHandler + // so that user can define failure handlers that precede exception mappers final Handler failureHandler = recorder.vertxFailureHandler(vertx.getVertx(), executorBuildItem.getExecutorProxy(), resteasyVertxConfig); - filterBuildItemBuildProducer.produce(new FilterBuildItem(failureHandler, - VertxHttpRecorder.AFTER_DEFAULT_ROUTE_ORDER_MARK + REST_ROUTE_ORDER_OFFSET, true)); + filterBuildItemBuildProducer.produce(FilterBuildItem.ofAuthenticationFailureHandler(failureHandler)); // Exact match for resources matched to the root path routes.produce( diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/ProactiveAuthCompletionExceptionHandlerTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/ProactiveAuthCompletionExceptionHandlerTest.java index ac93652557433..44dbffff92614 100644 --- a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/ProactiveAuthCompletionExceptionHandlerTest.java +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/ProactiveAuthCompletionExceptionHandlerTest.java @@ -61,9 +61,6 @@ public void testAuthCompletionExMapper() { .body(Matchers.equalTo(AUTHENTICATION_COMPLETION_EX)); } - /** - * Use failure handler as when proactive security is enabled, JAX-RS exception mappers won't do. - */ @ApplicationScoped public static final class CustomAuthCompletionExceptionHandler { diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/ProactiveAuthCompletionExceptionMapperTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/ProactiveAuthCompletionExceptionMapperTest.java new file mode 100644 index 0000000000000..f5372c7166fb1 --- /dev/null +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/ProactiveAuthCompletionExceptionMapperTest.java @@ -0,0 +1,86 @@ +package io.quarkus.resteasy.test.security; + +import static javax.ws.rs.core.Response.Status.UNAUTHORIZED; + +import java.util.function.Supplier; + +import javax.annotation.Priority; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Priorities; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.AuthenticationCompletionException; +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.restassured.filter.cookie.CookieFilter; + +public class ProactiveAuthCompletionExceptionMapperTest { + + private static final String AUTHENTICATION_COMPLETION_EX = "AuthenticationCompletionException"; + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(TestIdentityProvider.class, TestIdentityController.class) + .addAsResource(new StringAsset("quarkus.http.auth.form.enabled=true\n"), "application.properties"); + } + }); + + @BeforeAll + public static void setup() { + TestIdentityController.resetRoles().add("a d m i n", "a d m i n", "a d m i n"); + } + + @Test + public void testAuthCompletionExMapper() { + RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); + RestAssured + .given() + .filter(new CookieFilter()) + .redirects().follow(false) + .when() + .formParam("j_username", "a d m i n") + .formParam("j_password", "a d m i n") + .cookie("quarkus-redirect-location", "https://quarkus.io/guides") + .post("/j_security_check") + .then() + .assertThat() + .statusCode(401) + .body(Matchers.equalTo(AUTHENTICATION_COMPLETION_EX)); + } + + @Path("/hello") + public static class HelloResource { + + @GET + public String hello() { + return "Hello"; + } + + } + + @Priority(Priorities.USER) + @Provider + public static class CustomAuthCompletionExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(AuthenticationCompletionException e) { + return Response.status(UNAUTHORIZED).entity(AUTHENTICATION_COMPLETION_EX).build(); + } + } +} diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/ProactiveAuthHttpPolicyCustomForbiddenExHandlerTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/ProactiveAuthHttpPolicyCustomForbiddenExHandlerTest.java new file mode 100644 index 0000000000000..34f90cd676b0e --- /dev/null +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/ProactiveAuthHttpPolicyCustomForbiddenExHandlerTest.java @@ -0,0 +1,92 @@ +package io.quarkus.resteasy.test.security; + +import static io.quarkus.resteasy.test.security.ProactiveAuthHttpPolicyCustomForbiddenExHandlerTest.CustomForbiddenFailureHandler.CUSTOM_FORBIDDEN_EXCEPTION_HANDLER; +import static javax.ws.rs.core.Response.Status.FORBIDDEN; +import static org.hamcrest.Matchers.equalTo; + +import java.util.function.Supplier; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Observes; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.ForbiddenException; +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.vertx.core.Handler; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; + +public class ProactiveAuthHttpPolicyCustomForbiddenExHandlerTest { + + private static final String PROPERTIES = "quarkus.http.auth.basic=true\n" + + "quarkus.http.auth.policy.user-policy.roles-allowed=user\n" + + "quarkus.http.auth.permission.roles.paths=/secured\n" + + "quarkus.http.auth.permission.roles.policy=user-policy"; + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(TestIdentityProvider.class, TestIdentityController.class) + .addAsResource(new StringAsset(PROPERTIES), "application.properties"); + } + }); + + @BeforeAll + public static void setup() { + TestIdentityController.resetRoles().add("a d m i n", "a d m i n", "a d m i n"); + } + + @Test + public void testDeniedAccessAdminResource() { + RestAssured.given() + .auth().basic("a d m i n", "a d m i n") + .when().get("/secured") + .then() + .statusCode(403) + .body(equalTo(CUSTOM_FORBIDDEN_EXCEPTION_HANDLER)); + } + + @Path("/secured") + public static class SecuredResource { + + @GET + public String get() { + throw new IllegalStateException(); + } + + } + + @ApplicationScoped + public static final class CustomForbiddenFailureHandler { + + public static final String CUSTOM_FORBIDDEN_EXCEPTION_HANDLER = CustomForbiddenFailureHandler.class.getName(); + + public void init(@Observes Router router) { + router.route().failureHandler(new Handler() { + @Override + public void handle(RoutingContext event) { + if (event.failure() instanceof ForbiddenException) { + event.response().setStatusCode(FORBIDDEN.getStatusCode()).end(CUSTOM_FORBIDDEN_EXCEPTION_HANDLER); + } else { + event.next(); + } + } + }); + } + + } + +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java index 407faa9ee5491..92dfa45304f3e 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java @@ -1204,7 +1204,10 @@ public void setupDeployment(BeanContainerBuildItem beanContainerBuildItem, RuntimeValue restInitialHandler = recorder.restInitialHandler(deployment); Handler handler = recorder.handler(restInitialHandler); Handler failureHandler = recorder.failureHandler(restInitialHandler); - filterBuildItemBuildProducer.produce(new FilterBuildItem(failureHandler, order, true)); + + // we add failure handler right before QuarkusErrorHandler + // so that user can define failure handlers that precede exception mappers + filterBuildItemBuildProducer.produce(FilterBuildItem.ofAuthenticationFailureHandler(failureHandler)); // Exact match for resources matched to the root path routes.produce(RouteBuildItem.builder() diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthCompletionExceptionHandlerTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthCompletionExceptionHandlerTest.java index 946144889fbb4..49adcf5d43756 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthCompletionExceptionHandlerTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthCompletionExceptionHandlerTest.java @@ -59,9 +59,6 @@ public void testAuthCompletionExMapper() { .body(Matchers.equalTo(AUTHENTICATION_COMPLETION_EX)); } - /** - * Use failure handler as when proactive security is enabled, JAX-RS exception mappers won't do. - */ public static final class CustomAuthCompletionExceptionHandler { @Route(type = Route.HandlerType.FAILURE) diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthCompletionExceptionMapperTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthCompletionExceptionMapperTest.java new file mode 100644 index 0000000000000..61f250e251ee3 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthCompletionExceptionMapperTest.java @@ -0,0 +1,72 @@ +package io.quarkus.resteasy.reactive.server.test.security; + +import static javax.ws.rs.core.Response.Status.UNAUTHORIZED; + +import java.util.function.Supplier; + +import javax.ws.rs.core.Response; + +import org.hamcrest.Matchers; +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.AuthenticationCompletionException; +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.restassured.filter.cookie.CookieFilter; + +public class ProactiveAuthCompletionExceptionMapperTest { + + private static final String AUTHENTICATION_COMPLETION_EX = "AuthenticationCompletionException"; + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(TestIdentityProvider.class, TestIdentityController.class, + CustomAuthCompletionExceptionMapper.class) + .addAsResource(new StringAsset("quarkus.http.auth.form.enabled=true\n"), "application.properties"); + } + }); + + @BeforeAll + public static void setup() { + TestIdentityController.resetRoles().add("a d m i n", "a d m i n", "a d m i n"); + } + + @Test + public void testAuthCompletionExMapper() { + RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); + RestAssured + .given() + .filter(new CookieFilter()) + .redirects().follow(false) + .when() + .formParam("j_username", "a d m i n") + .formParam("j_password", "a d m i n") + .cookie("quarkus-redirect-location", "https://quarkus.io/guides") + .post("/j_security_check") + .then() + .assertThat() + .statusCode(401) + .body(Matchers.equalTo(AUTHENTICATION_COMPLETION_EX)); + } + + public static final class CustomAuthCompletionExceptionMapper { + + @ServerExceptionMapper(value = AuthenticationCompletionException.class) + public Response unauthorized() { + return Response.status(UNAUTHORIZED).entity(AUTHENTICATION_COMPLETION_EX).build(); + } + + } + +} \ No newline at end of file diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthHttpPolicyForbiddenExMapperTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthHttpPolicyForbiddenExMapperTest.java new file mode 100644 index 0000000000000..e16c8b2ee787f --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthHttpPolicyForbiddenExMapperTest.java @@ -0,0 +1,79 @@ +package io.quarkus.resteasy.reactive.server.test.security; + +import static javax.ws.rs.core.Response.Status.FORBIDDEN; +import static org.hamcrest.Matchers.equalTo; + +import java.util.function.Supplier; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; + +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.ForbiddenException; +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class ProactiveAuthHttpPolicyForbiddenExMapperTest { + + private static final String PROPERTIES = "quarkus.http.auth.basic=true\n" + + "quarkus.http.auth.policy.user-policy.roles-allowed=user\n" + + "quarkus.http.auth.permission.roles.paths=/secured\n" + + "quarkus.http.auth.permission.roles.policy=user-policy"; + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(TestIdentityProvider.class, TestIdentityController.class, CustomForbiddenExceptionMapper.class) + .addAsResource(new StringAsset(PROPERTIES), "application.properties"); + } + }); + + @BeforeAll + public static void setup() { + TestIdentityController.resetRoles().add("a d m i n", "a d m i n", "a d m i n"); + } + + @Test + public void testDeniedAccessAdminResource() { + RestAssured.given() + .auth().basic("a d m i n", "a d m i n") + .when().get("/secured") + .then() + .statusCode(403) + .body(equalTo(CustomForbiddenExceptionMapper.CUSTOM_FORBIDDEN_EXCEPTION_MAPPER)); + } + + @Path("/secured") + public static class SecuredResource { + + @GET + public String get() { + throw new IllegalStateException(); + } + + } + + public static final class CustomForbiddenExceptionMapper { + + public static final String CUSTOM_FORBIDDEN_EXCEPTION_MAPPER = CustomForbiddenExceptionMapper.class.getName(); + + @ServerExceptionMapper(value = ForbiddenException.class) + public Response forbidden() { + return Response.status(FORBIDDEN).entity(CUSTOM_FORBIDDEN_EXCEPTION_MAPPER).build(); + } + + } + +} \ No newline at end of file diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthHttpPolicyForbiddenHandlerTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthHttpPolicyForbiddenHandlerTest.java index 81678e9cddb4d..15ce1ca3a4b96 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthHttpPolicyForbiddenHandlerTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthHttpPolicyForbiddenHandlerTest.java @@ -65,9 +65,6 @@ public String get() { } - /** - * Use failure handler as when proactive security is enabled, JAX-RS exception mappers won't do. - */ public static final class CustomForbiddenFailureHandler { public static final String CUSTOM_FORBIDDEN_EXCEPTION_MAPPER = CustomForbiddenFailureHandler.class.getName(); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java index c0a81b7d277bb..6cc91a1899e50 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java @@ -54,6 +54,7 @@ import io.quarkus.security.AuthenticationCompletionException; import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.AuthenticationRedirectException; +import io.quarkus.security.ForbiddenException; import io.quarkus.security.identity.CurrentIdentityAssociation; import io.quarkus.vertx.http.runtime.CurrentVertxRequest; import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; @@ -218,16 +219,10 @@ public Handler failureHandler(RuntimeValue r @Override public void handle(RoutingContext event) { - // this condition prevent exception mappers from handling auth failure exceptions when proactive - // security is enabled as for now, community decided that's expected behavior and only way for - // users to handle the exceptions is to define their own failure handler as in Reactive Routes - // more info here: https://github.com/quarkusio/quarkus/pull/28648#issuecomment-1287203946 - final boolean eventFailedByRESTEasyReactive = event - .get(QuarkusHttpUser.AUTH_FAILURE_HANDLER) instanceof FailingDefaultAuthFailureHandler; - - if (eventFailedByRESTEasyReactive && (event.failure() instanceof AuthenticationFailedException + if (event.failure() instanceof AuthenticationFailedException || event.failure() instanceof AuthenticationCompletionException - || event.failure() instanceof AuthenticationRedirectException)) { + || event.failure() instanceof AuthenticationRedirectException + || event.failure() instanceof ForbiddenException) { restInitialHandler.beginProcessing(event, event.failure()); } else { event.next(); diff --git a/extensions/smallrye-jwt/deployment/src/test/java/io/quarkus/jwt/test/EnabledProactiveAuthFailedExceptionMapperTest.java b/extensions/smallrye-jwt/deployment/src/test/java/io/quarkus/jwt/test/EnabledProactiveAuthFailedExceptionMapperTest.java new file mode 100644 index 0000000000000..ebd85a97539b8 --- /dev/null +++ b/extensions/smallrye-jwt/deployment/src/test/java/io/quarkus/jwt/test/EnabledProactiveAuthFailedExceptionMapperTest.java @@ -0,0 +1,47 @@ +package io.quarkus.jwt.test; + +import javax.ws.rs.core.Response; + +import org.hamcrest.Matchers; +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.AuthenticationFailedException; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class EnabledProactiveAuthFailedExceptionMapperTest { + + private static final String CUSTOMIZED_RESPONSE = "AuthenticationFailedException"; + protected static final Class[] classes = { JsonValuejectionEndpoint.class, TokenUtils.class, + AuthFailedExceptionMapper.class }; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(classes) + .addAsResource(new StringAsset("quarkus.http.auth.proactive=true\n"), "application.properties")); + + @Test + public void testExMapperCustomizedResponse() { + RestAssured + .given() + .auth().oauth2("absolute-nonsense") + .get("/endp/verifyInjectedIssuer").then() + .statusCode(401) + .body(Matchers.equalTo(CUSTOMIZED_RESPONSE)); + } + + public static class AuthFailedExceptionMapper { + + @ServerExceptionMapper(value = AuthenticationFailedException.class) + public Response unauthorized() { + return Response + .status(401) + .entity(CUSTOMIZED_RESPONSE).build(); + } + + } +} \ No newline at end of file diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/FilterBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/FilterBuildItem.java index 5e3253279e06c..ab7c6f4fd9883 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/FilterBuildItem.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/FilterBuildItem.java @@ -15,6 +15,7 @@ public final class FilterBuildItem extends MultiBuildItem { public static final int CORS = 300; public static final int AUTHENTICATION = 200; public static final int AUTHORIZATION = 100; + private static final int AUTH_FAILURE_HANDLER = Integer.MIN_VALUE + 1; private final Handler handler; private final int priority; @@ -35,18 +36,22 @@ public FilterBuildItem(Handler handler, int priority) { } /** - * Creates a new instance of {@link FilterBuildItem}. + * Creates a new instance of {@link FilterBuildItem} with an authentication failure handler. * - * @param handler the handler, if {@code null} the filter won't be used. - * @param priority the priority, higher priority gets invoked first. Priority is only used to sort filters, user - * routes are called afterwards. Must be positive. - * @param isFailureHandler whether an HTTP request or failure should be routed to a handler. + * @param authFailureHandler authentication failure handler */ - public FilterBuildItem(Handler handler, int priority, boolean isFailureHandler) { - this.handler = handler; - checkPriority(priority); - this.priority = priority; - this.isFailureHandler = isFailureHandler; + private FilterBuildItem(Handler authFailureHandler) { + this.handler = authFailureHandler; + this.isFailureHandler = true; + this.priority = AUTH_FAILURE_HANDLER; + } + + /** + * Creates a new instance of {@link FilterBuildItem} with an authentication failure handler. + * The handler will be added as next to last, right before {@link io.quarkus.vertx.http.runtime.QuarkusErrorHandler}. + */ + public static FilterBuildItem ofAuthenticationFailureHandler(Handler authFailureHandler) { + return new FilterBuildItem(authFailureHandler); } private void checkPriority(int priority) { @@ -71,7 +76,16 @@ public boolean isFailureHandler() { * @return a filter object wrapping the handler and priority. */ public Filter toFilter() { - return new Filters.SimpleFilter(handler, priority, isFailureHandler); + if (isFailureHandler && priority == AUTH_FAILURE_HANDLER) { + // create filter for penultimate auth failure handler + final Filters.SimpleFilter filter = new Filters.SimpleFilter(); + filter.setPriority(AUTH_FAILURE_HANDLER); + filter.setFailureHandler(true); + filter.setHandler(handler); + return filter; + } else { + return new Filters.SimpleFilter(handler, priority, isFailureHandler); + } } }