Skip to content

Commit

Permalink
RR - use exception mappers on auth failure exceptions for proactive auth
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed Dec 15, 2022
1 parent b0d399d commit 8721761
Show file tree
Hide file tree
Showing 14 changed files with 448 additions and 62 deletions.
Expand Up @@ -187,7 +187,40 @@ 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<AuthenticationFailedException> {
@Context
UriInfo uriInfo;
@Override
public Response toResponse(AuthenticationFailedException exception) {
return Response.status(401).header("WWW-Authenticate", "Basic realm=\"Quarkus\"").build();
}
}
----

CAUTION: By default, authentication mechanisms like `io.quarkus.oidc.runtime.OidcAuthenticationMechanism` needs to handle Quarkus Security authentication exceptions themselves in order redirect request and set correct response status.
For that reason, built-in exception mappers that handles authentication failures challenge authentication mechanisms.
See xref:security-customization.adoc#dealing-with-more-than-one-http-auth-mechanisms[Dealing with more than one HttpAuthenticationMechanism] section for more details on authentication mechanism challenge and more specifically {quarkus-blob-url}/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/exceptionmappers/SecurityExceptionMapperUtil.java[SecurityExceptionMapperUtil.java] for an example how RESTEasy Reactive sends a challenge.

Perhaps safer way with enabled Proactive Authentication is to provide a failure handler, as event comes to the handler with correct response status and headers,
therefore only thing that needs to be done is to customize response like this:

[source,java]
----
Expand All @@ -209,7 +242,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();
}
Expand All @@ -219,34 +252,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<AuthenticationFailedException> {
@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]
1 change: 1 addition & 0 deletions docs/src/main/asciidoc/security-customization.adoc
Expand Up @@ -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.
Expand Down
Expand Up @@ -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<RoutingContext> 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(
Expand Down
Expand Up @@ -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 {

Expand Down
@@ -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<AuthenticationCompletionException> {

@Override
public Response toResponse(AuthenticationCompletionException e) {
return Response.status(UNAUTHORIZED).entity(AUTHENTICATION_COMPLETION_EX).build();
}
}
}
@@ -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<RoutingContext>() {
@Override
public void handle(RoutingContext event) {
if (event.failure() instanceof ForbiddenException) {
event.response().setStatusCode(FORBIDDEN.getStatusCode()).end(CUSTOM_FORBIDDEN_EXCEPTION_HANDLER);
} else {
event.next();
}
}
});
}

}

}
Expand Up @@ -1204,7 +1204,10 @@ public void setupDeployment(BeanContainerBuildItem beanContainerBuildItem,
RuntimeValue<RestInitialHandler> restInitialHandler = recorder.restInitialHandler(deployment);
Handler<RoutingContext> handler = recorder.handler(restInitialHandler);
Handler<RoutingContext> 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()
Expand Down
Expand Up @@ -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)
Expand Down
@@ -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();
}

}

}

0 comments on commit 8721761

Please sign in to comment.