diff --git a/core/runtime/src/main/java/io/quarkus/runtime/LaunchMode.java b/core/runtime/src/main/java/io/quarkus/runtime/LaunchMode.java index c5d4a908a1566..f578a7b212835 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/LaunchMode.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/LaunchMode.java @@ -20,6 +20,10 @@ public boolean isDevOrTest() { return this != NORMAL; } + public static boolean isDev() { + return current() == DEVELOPMENT; + } + /** * Returns true if the current launch is the server side of remote dev. */ diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AuthenticationFailureResponseBodyDevModeTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AuthenticationFailureResponseBodyDevModeTest.java new file mode 100644 index 0000000000000..c5dbec45f2bea --- /dev/null +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AuthenticationFailureResponseBodyDevModeTest.java @@ -0,0 +1,128 @@ +package io.quarkus.resteasy.test.security; + +import java.util.Collections; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.Authenticated; +import io.quarkus.security.AuthenticationCompletionException; +import io.quarkus.security.AuthenticationFailedException; +import io.quarkus.security.identity.IdentityProviderManager; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.request.AuthenticationRequest; +import io.quarkus.security.identity.request.CertificateAuthenticationRequest; +import io.quarkus.test.QuarkusDevModeTest; +import io.quarkus.vertx.http.runtime.security.ChallengeData; +import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; +import io.restassured.RestAssured; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +public class AuthenticationFailureResponseBodyDevModeTest { + + private static final String RESPONSE_BODY = "failure"; + + public enum AuthFailure { + AUTH_FAILED_WITH_BODY(() -> new AuthenticationFailedException(RESPONSE_BODY), true), + AUTH_COMPLETION_WITH_BODY(() -> new AuthenticationCompletionException(RESPONSE_BODY), true), + AUTH_FAILED_WITHOUT_BODY(AuthenticationFailedException::new, false), + AUTH_COMPLETION_WITHOUT_BODY(AuthenticationCompletionException::new, false); + + public final Supplier failureSupplier; + private final boolean expectBody; + + AuthFailure(Supplier failureSupplier, boolean expectBody) { + this.failureSupplier = failureSupplier; + this.expectBody = expectBody; + } + } + + @RegisterExtension + static QuarkusDevModeTest runner = new QuarkusDevModeTest() + .withApplicationRoot((jar) -> jar + .addClasses(SecuredResource.class, FailingAuthenticator.class, AuthFailure.class) + .addAsResource(new StringAsset(""" + quarkus.http.auth.proactive=false + """), "application.properties")); + + @Test + public void testAuthenticationFailedExceptionBody() { + assertExceptionBody(AuthFailure.AUTH_FAILED_WITHOUT_BODY, false); + assertExceptionBody(AuthFailure.AUTH_FAILED_WITHOUT_BODY, true); + assertExceptionBody(AuthFailure.AUTH_FAILED_WITH_BODY, false); + assertExceptionBody(AuthFailure.AUTH_FAILED_WITH_BODY, true); + } + + @Test + public void testAuthenticationCompletionExceptionBody() { + assertExceptionBody(AuthFailure.AUTH_COMPLETION_WITHOUT_BODY, false); + assertExceptionBody(AuthFailure.AUTH_COMPLETION_WITH_BODY, false); + } + + private static void assertExceptionBody(AuthFailure failure, boolean challenge) { + RestAssured + .given() + .redirects().follow(false) + .header("auth-failure", failure.toString()) + .header("challenge-data", challenge) + .get("/secured") + .then() + .statusCode(challenge ? 302 : 401) + .body(failure.expectBody ? Matchers.equalTo(RESPONSE_BODY) + : Matchers.not(Matchers.containsString(RESPONSE_BODY))); + } + + @Authenticated + @Path("secured") + public static class SecuredResource { + + @GET + public String ignored() { + return "ignored"; + } + + } + + @ApplicationScoped + public static class FailingAuthenticator implements HttpAuthenticationMechanism { + + @Override + public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { + return Uni.createFrom().failure(getFailureProducer(context)); + } + + private static Supplier getFailureProducer(RoutingContext context) { + return getAuthFailure(context).failureSupplier; + } + + private static AuthFailure getAuthFailure(RoutingContext context) { + return AuthFailure.valueOf(context.request().getHeader("auth-failure")); + } + + @Override + public Set> getCredentialTypes() { + // so that we don't need to implement an identity provider + return Collections.singleton(CertificateAuthenticationRequest.class); + } + + @Override + public Uni getChallenge(RoutingContext context) { + if (Boolean.parseBoolean(context.request().getHeader("challenge-data"))) { + return Uni.createFrom().item(new ChallengeData(302, null, null)); + } else { + return Uni.createFrom().optional(Optional.empty()); + } + } + + } +} diff --git a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/AuthenticationCompletionExceptionMapper.java b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/AuthenticationCompletionExceptionMapper.java index 4d8e1d83915c4..05b268cb7c31d 100644 --- a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/AuthenticationCompletionExceptionMapper.java +++ b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/AuthenticationCompletionExceptionMapper.java @@ -6,6 +6,7 @@ import org.jboss.logging.Logger; +import io.quarkus.runtime.LaunchMode; import io.quarkus.security.AuthenticationCompletionException; @Provider @@ -16,6 +17,9 @@ public class AuthenticationCompletionExceptionMapper implements ExceptionMapper< @Override public Response toResponse(AuthenticationCompletionException ex) { log.debug("Authentication has failed, returning HTTP status 401"); + if (LaunchMode.isDev() && ex.getMessage() != null) { + return Response.status(401).entity(ex.getMessage()).build(); + } return Response.status(401).build(); } diff --git a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/AuthenticationFailedExceptionMapper.java b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/AuthenticationFailedExceptionMapper.java index 77cd8b809611b..fc25985627d76 100644 --- a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/AuthenticationFailedExceptionMapper.java +++ b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/AuthenticationFailedExceptionMapper.java @@ -9,6 +9,7 @@ import org.jboss.logging.Logger; +import io.quarkus.runtime.LaunchMode; import io.quarkus.security.AuthenticationFailedException; import io.quarkus.vertx.http.runtime.CurrentVertxRequest; import io.quarkus.vertx.http.runtime.security.ChallengeData; @@ -19,7 +20,6 @@ @Priority(Priorities.USER + 1) public class AuthenticationFailedExceptionMapper implements ExceptionMapper { private static final Logger log = Logger.getLogger(AuthenticationFailedExceptionMapper.class.getName()); - private volatile CurrentVertxRequest currentVertxRequest; CurrentVertxRequest currentVertxRequest() { @@ -37,18 +37,25 @@ public Response toResponse(AuthenticationFailedException 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); + int statusCode = challengeData == null ? 401 : challengeData.status; + Response.ResponseBuilder responseBuilder = Response.status(statusCode); + if (challengeData != null && challengeData.headerName != null) { + responseBuilder.header(challengeData.headerName.toString(), challengeData.headerContent); + } + if (LaunchMode.isDev() && exception.getMessage() != null) { + responseBuilder.entity(exception.getMessage()); } - log.debugf("Returning an authentication challenge, status code: %d", challengeData.status); - return status.build(); + log.debugf("Returning an authentication challenge, status code: %d", statusCode); + return responseBuilder.build(); } else { log.error("HttpAuthenticator is not found, returning HTTP status 401"); } } else { log.error("RoutingContext is not found, returning HTTP status 401"); } + if (LaunchMode.isDev() && exception.getMessage() != null) { + return Response.status(401).entity(exception.getMessage()).build(); + } return Response.status(401).entity("Not Authenticated").build(); } } diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AbstractAuthFailureResponseBodyDevModeTest.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AbstractAuthFailureResponseBodyDevModeTest.java new file mode 100644 index 0000000000000..a5a2b14f8e594 --- /dev/null +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/AbstractAuthFailureResponseBodyDevModeTest.java @@ -0,0 +1,117 @@ +package io.quarkus.resteasy.reactive.server.test.security; + +import java.util.Collections; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import io.quarkus.security.Authenticated; +import io.quarkus.security.AuthenticationCompletionException; +import io.quarkus.security.AuthenticationFailedException; +import io.quarkus.security.identity.IdentityProviderManager; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.request.AuthenticationRequest; +import io.quarkus.security.identity.request.CertificateAuthenticationRequest; +import io.quarkus.vertx.http.runtime.security.ChallengeData; +import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; +import io.restassured.RestAssured; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +public abstract class AbstractAuthFailureResponseBodyDevModeTest { + + private static final String RESPONSE_BODY = "failure"; + + public enum AuthFailure { + AUTH_FAILED_WITH_BODY(() -> new AuthenticationFailedException(RESPONSE_BODY), true), + AUTH_COMPLETION_WITH_BODY(() -> new AuthenticationCompletionException(RESPONSE_BODY), true), + AUTH_FAILED_WITHOUT_BODY(AuthenticationFailedException::new, false), + AUTH_COMPLETION_WITHOUT_BODY(AuthenticationCompletionException::new, false); + + public final Supplier failureSupplier; + private final boolean expectBody; + + AuthFailure(Supplier failureSupplier, boolean expectBody) { + this.failureSupplier = failureSupplier; + this.expectBody = expectBody; + } + } + + @Test + public void testAuthenticationFailedExceptionBody() { + assertExceptionBody(AuthFailure.AUTH_FAILED_WITHOUT_BODY, false); + assertExceptionBody(AuthFailure.AUTH_FAILED_WITHOUT_BODY, true); + assertExceptionBody(AuthFailure.AUTH_FAILED_WITH_BODY, false); + assertExceptionBody(AuthFailure.AUTH_FAILED_WITH_BODY, true); + } + + @Test + public void testAuthenticationCompletionExceptionBody() { + assertExceptionBody(AuthFailure.AUTH_COMPLETION_WITHOUT_BODY, false); + assertExceptionBody(AuthFailure.AUTH_COMPLETION_WITH_BODY, false); + } + + private static void assertExceptionBody(AuthFailure failure, boolean challenge) { + RestAssured + .given() + .redirects().follow(false) + .header("auth-failure", failure.toString()) + .header("challenge-data", challenge) + .get("/secured") + .then() + .statusCode(challenge ? 302 : 401) + .body(failure.expectBody ? Matchers.equalTo(RESPONSE_BODY) + : Matchers.not(Matchers.containsString(RESPONSE_BODY))); + } + + @Authenticated + @Path("secured") + public static class SecuredResource { + + @GET + public String ignored() { + return "ignored"; + } + + } + + @ApplicationScoped + public static class FailingAuthenticator implements HttpAuthenticationMechanism { + + @Override + public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { + return Uni.createFrom().failure(getFailureProducer(context)); + } + + private static Supplier getFailureProducer(RoutingContext context) { + return getAuthFailure(context).failureSupplier; + } + + private static AuthFailure getAuthFailure(RoutingContext context) { + return AuthFailure.valueOf(context.request().getHeader("auth-failure")); + } + + @Override + public Set> getCredentialTypes() { + // so that we don't need to implement an identity provider + return Collections.singleton(CertificateAuthenticationRequest.class); + } + + @Override + public Uni getChallenge(RoutingContext context) { + if (Boolean.parseBoolean(context.request().getHeader("challenge-data"))) { + return Uni.createFrom().item(new ChallengeData(302, null, null)); + } else { + return Uni.createFrom().optional(Optional.empty()); + } + } + + } +} diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/LazyAuthFailureResponseBodyDevModeTest.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/LazyAuthFailureResponseBodyDevModeTest.java new file mode 100644 index 0000000000000..79c64df423fdd --- /dev/null +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/LazyAuthFailureResponseBodyDevModeTest.java @@ -0,0 +1,18 @@ +package io.quarkus.resteasy.reactive.server.test.security; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; + +public class LazyAuthFailureResponseBodyDevModeTest extends AbstractAuthFailureResponseBodyDevModeTest { + + @RegisterExtension + static QuarkusDevModeTest runner = new QuarkusDevModeTest() + .withApplicationRoot((jar) -> jar + .addClasses(SecuredResource.class, FailingAuthenticator.class, AuthFailure.class) + .addAsResource(new StringAsset(""" + quarkus.http.auth.proactive=false + """), "application.properties")); + +} diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthFailureResponseBodyDevModeTest.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthFailureResponseBodyDevModeTest.java new file mode 100644 index 0000000000000..3f37bd6d2aa30 --- /dev/null +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/ProactiveAuthFailureResponseBodyDevModeTest.java @@ -0,0 +1,14 @@ +package io.quarkus.resteasy.reactive.server.test.security; + +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; + +public class ProactiveAuthFailureResponseBodyDevModeTest extends AbstractAuthFailureResponseBodyDevModeTest { + + @RegisterExtension + static QuarkusDevModeTest runner = new QuarkusDevModeTest() + .withApplicationRoot((jar) -> jar + .addClasses(SecuredResource.class, FailingAuthenticator.class, AuthFailure.class)); + +} diff --git a/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/exceptionmappers/AuthenticationCompletionExceptionMapper.java b/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/exceptionmappers/AuthenticationCompletionExceptionMapper.java index acf80b9c11b8b..40a7df50e73b1 100644 --- a/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/exceptionmappers/AuthenticationCompletionExceptionMapper.java +++ b/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/exceptionmappers/AuthenticationCompletionExceptionMapper.java @@ -3,12 +3,16 @@ import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.ExceptionMapper; +import io.quarkus.runtime.LaunchMode; import io.quarkus.security.AuthenticationCompletionException; public class AuthenticationCompletionExceptionMapper implements ExceptionMapper { @Override public Response toResponse(AuthenticationCompletionException ex) { + if (LaunchMode.isDev() && ex.getMessage() != null) { + return Response.status(Response.Status.UNAUTHORIZED).entity(ex.getMessage()).build(); + } return Response.status(Response.Status.UNAUTHORIZED).build(); } diff --git a/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/exceptionmappers/AuthenticationFailedExceptionMapper.java b/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/exceptionmappers/AuthenticationFailedExceptionMapper.java index bb2148a87c7a7..e3fd961eb64f6 100644 --- a/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/exceptionmappers/AuthenticationFailedExceptionMapper.java +++ b/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/exceptionmappers/AuthenticationFailedExceptionMapper.java @@ -5,6 +5,7 @@ import org.jboss.resteasy.reactive.server.ServerExceptionMapper; +import io.quarkus.runtime.LaunchMode; import io.quarkus.security.AuthenticationFailedException; import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; @@ -12,7 +13,8 @@ public class AuthenticationFailedExceptionMapper { @ServerExceptionMapper(value = AuthenticationFailedException.class, priority = Priorities.USER + 1) - public Uni handle(RoutingContext routingContext) { - return SecurityExceptionMapperUtil.handleWithAuthenticator(routingContext); + public Uni handle(RoutingContext routingContext, AuthenticationFailedException exception) { + return SecurityExceptionMapperUtil.handleWithAuthenticator(routingContext, + LaunchMode.isDev() ? exception.getMessage() : null); } } diff --git a/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/exceptionmappers/SecurityExceptionMapperUtil.java b/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/exceptionmappers/SecurityExceptionMapperUtil.java index 4872aeb141470..7f79527b4d1b6 100644 --- a/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/exceptionmappers/SecurityExceptionMapperUtil.java +++ b/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/exceptionmappers/SecurityExceptionMapperUtil.java @@ -16,7 +16,7 @@ final class SecurityExceptionMapperUtil { private SecurityExceptionMapperUtil() { } - static Uni handleWithAuthenticator(RoutingContext routingContext) { + static Uni handleWithAuthenticator(RoutingContext routingContext, String exceptionMessage) { HttpAuthenticator authenticator = routingContext.get(HttpAuthenticator.class.getName()); if (authenticator != null) { Uni challenge = authenticator.getChallenge(routingContext); @@ -24,17 +24,26 @@ static Uni handleWithAuthenticator(RoutingContext routingContext) { @Override public Response apply(ChallengeData challengeData) { if (challengeData == null) { - return DEFAULT_UNAUTHORIZED_RESPONSE; + return exceptionMessage != null ? createResponse(exceptionMessage) : DEFAULT_UNAUTHORIZED_RESPONSE; } - Response.ResponseBuilder status = Response.status(challengeData.status); + Response.ResponseBuilder responseBuilder = Response.status(challengeData.status); if (challengeData.headerName != null) { - status.header(challengeData.headerName.toString(), challengeData.headerContent); + responseBuilder.header(challengeData.headerName.toString(), challengeData.headerContent); } - return status.build(); + if (exceptionMessage != null) { + responseBuilder.entity(exceptionMessage); + } + return responseBuilder.build(); } - }).onFailure().recoverWithItem(DEFAULT_UNAUTHORIZED_RESPONSE); + }).onFailure().recoverWithItem( + exceptionMessage != null ? createResponse(exceptionMessage) : DEFAULT_UNAUTHORIZED_RESPONSE); } else { - return Uni.createFrom().item(DEFAULT_UNAUTHORIZED_RESPONSE); + return Uni.createFrom() + .item(exceptionMessage != null ? createResponse(exceptionMessage) : DEFAULT_UNAUTHORIZED_RESPONSE); } } + + private static Response createResponse(String responseBody) { + return Response.status(Response.Status.UNAUTHORIZED).entity(responseBody).build(); + } } diff --git a/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/exceptionmappers/UnauthorizedExceptionMapper.java b/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/exceptionmappers/UnauthorizedExceptionMapper.java index 12678be09af16..77eba516f3db1 100644 --- a/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/exceptionmappers/UnauthorizedExceptionMapper.java +++ b/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/exceptionmappers/UnauthorizedExceptionMapper.java @@ -13,6 +13,6 @@ public class UnauthorizedExceptionMapper { @ServerExceptionMapper(value = UnauthorizedException.class, priority = Priorities.USER + 1) public Uni handle(RoutingContext routingContext) { - return SecurityExceptionMapperUtil.handleWithAuthenticator(routingContext); + return SecurityExceptionMapperUtil.handleWithAuthenticator(routingContext, null); } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java index 76ff73fd935d8..d7c493124604f 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java @@ -27,6 +27,7 @@ import io.quarkus.runtime.logging.DecorateStackUtil; import io.quarkus.security.AuthenticationCompletionException; import io.quarkus.security.AuthenticationException; +import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.ForbiddenException; import io.quarkus.security.UnauthorizedException; import io.quarkus.vertx.http.runtime.security.HttpAuthenticator; @@ -76,14 +77,15 @@ public QuarkusErrorHandler(boolean showStack, boolean decorateStack, @Override public void handle(RoutingContext event) { + Throwable exception = event.failure(); try { - if (event.failure() == null) { + if (exception == null) { event.response().setStatusCode(event.statusCode()); event.response().end(); return; } //this can happen if there is no auth mechanisms - if (event.failure() instanceof UnauthorizedException) { + if (exception instanceof UnauthorizedException) { HttpAuthenticator authenticator = event.get(HttpAuthenticator.class.getName()); if (authenticator != null) { authenticator.sendChallenge(event).subscribe().with(new Consumer<>() { @@ -102,12 +104,12 @@ public void accept(Throwable throwable) { } return; } - if (event.failure() instanceof ForbiddenException) { + if (exception instanceof ForbiddenException) { event.response().setStatusCode(HttpResponseStatus.FORBIDDEN.code()).end(); return; } - if (event.failure() instanceof AuthenticationException) { + if (exception instanceof AuthenticationException) { if (event.response().getStatusCode() == HttpResponseStatus.OK.code()) { //set 401 if status wasn't set upstream event.response().setStatusCode(HttpResponseStatus.UNAUTHORIZED.code()); @@ -118,17 +120,18 @@ public void accept(Throwable throwable) { //disabled, this should be handled elsewhere and if we get to this point bad things have happened, //so it is better to send a response than to hang - if (event.failure() instanceof AuthenticationCompletionException - && event.failure().getMessage() != null - && LaunchMode.current() == LaunchMode.DEVELOPMENT) { - event.response().end(event.failure().getMessage()); + if ((exception instanceof AuthenticationCompletionException + || exception instanceof AuthenticationFailedException) + && exception.getMessage() != null + && LaunchMode.isDev()) { + event.response().end(exception.getMessage()); } else { event.response().end(); } return; } - if (event.failure() instanceof RejectedExecutionException) { + if (exception instanceof RejectedExecutionException) { log.warn( "Worker thread pool exhaustion, no more worker threads available - returning a `503 - SERVICE UNAVAILABLE` response."); event.response().setStatusCode(HttpResponseStatus.SERVICE_UNAVAILABLE.code()).end(); @@ -150,7 +153,6 @@ public void accept(Throwable throwable) { String uuid = LazyHolder.BASE_ID + ERROR_COUNT.incrementAndGet(); String details; String stack = ""; - Throwable exception = event.failure(); String responseContentType = null; try { responseContentType = ContentTypes.pickFirstSupportedAndAcceptedContentType(event); @@ -167,7 +169,7 @@ public void accept(Throwable throwable) { } else { details = generateHeaderMessage(uuid); } - if (event.failure() instanceof IOException) { + if (exception instanceof IOException) { log.debugf(exception, "IOError processing HTTP request to %s failed, the client likely terminated the connection. Error id: %s", event.request().uri(), uuid); diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java index da171acff63df..24e7a5e9c9ef3 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java @@ -2,6 +2,7 @@ import static io.quarkus.security.spi.runtime.SecurityEventHelper.AUTHENTICATION_FAILURE; import static io.quarkus.security.spi.runtime.SecurityEventHelper.AUTHENTICATION_SUCCESS; +import static io.quarkus.vertx.http.runtime.security.HttpSecurityRecorder.DefaultAuthFailureHandler.DEV_MODE_AUTHENTICATION_FAILURE_BODY; import static io.quarkus.vertx.http.runtime.security.HttpSecurityUtils.SECURITY_IDENTITIES_ATTRIBUTE; import static io.quarkus.vertx.http.runtime.security.HttpSecurityUtils.getSecurityIdentities; import static io.quarkus.vertx.http.runtime.security.RolesMapping.ROLES_MAPPING_KEY; @@ -301,7 +302,12 @@ public Uni apply(Boolean authDone) { if (!authDone) { log.debug("Authentication has not been done, returning HTTP status 401"); routingContext.response().setStatusCode(401); - routingContext.response().end(); + if (routingContext.get(DEV_MODE_AUTHENTICATION_FAILURE_BODY) == null) { + routingContext.response().end(); + } else { + final String authenticationFailureBody = routingContext.get(DEV_MODE_AUTHENTICATION_FAILURE_BODY); + routingContext.response().end(authenticationFailureBody); + } } return Uni.createFrom().item(authDone); } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java index 410980a1fac66..af35e85f94cd6 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java @@ -177,6 +177,7 @@ public static abstract class DefaultAuthFailureHandler implements BiConsumer() {