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 16, 2022
1 parent d5f8007 commit 8ed165f
Show file tree
Hide file tree
Showing 14 changed files with 447 additions and 62 deletions.
Expand Up @@ -187,7 +187,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<AuthenticationFailedException> {
@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]
----
Expand All @@ -209,7 +241,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 +251,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 8ed165f

Please sign in to comment.