Skip to content

Commit

Permalink
Merge pull request quarkusio#44977 from michalvavrik/feature/wrap-int…
Browse files Browse the repository at this point in the history
…ernal-errors-during-auth

Apply custom JAX-RS exception mappers for non-authentication exceptions raised during proactive auth or auth required by HTTP permissions
  • Loading branch information
geoand authored Dec 9, 2024
2 parents d2669c9 + 75f11fb commit be70099
Show file tree
Hide file tree
Showing 9 changed files with 452 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package io.quarkus.resteasy.test.security;

import java.util.Map;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.SecurityContext;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;

import org.hamcrest.Matchers;
import org.jboss.resteasy.spi.UnhandledException;
import org.junit.jupiter.api.Test;

import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.IdentityProvider;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.SecurityIdentityAugmentor;
import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest;
import io.quarkus.security.runtime.QuarkusPrincipal;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils;
import io.restassured.RestAssured;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;

/**
* Tests internal server errors and other custom exceptions raised during
* proactive authentication can be handled by the exception mappers.
* For lazy authentication, it is important that these exceptions raised during authentication
* required by HTTP permissions are also propagated.
*/
public abstract class AbstractCustomExceptionMapperTest {

@Test
public void testNoExceptions() {
RestAssured.given()
.auth().preemptive().basic("gaston", "gaston-password")
.get("/hello")
.then()
.statusCode(200)
.body(Matchers.is("Hello Gaston"));
RestAssured.given()
.get("/hello")
.then()
.statusCode(401);
}

@Test
public void testUnhandledRuntimeException() {
// UnhandledRuntimeException has no exception mapper therefore RESTEasy would wrap it in UnhandledException
// if we started RESTEasy even though there is no matching exception mapper
RestAssured.given()
.auth().preemptive().basic("gaston", "gaston-password")
.header("fail-unhandled", "true")
.get("/hello")
.then()
.statusCode(500)
.body(Matchers.not(Matchers.is(UnhandledException.class.getName())))
.body(Matchers.containsString(UnhandledRuntimeException.class.getName()))
.body(Matchers.containsString("Expected unhandled failure"));
}

@Test
public void testCustomExceptionInIdentityProvider() {
RestAssured.given()
.auth().preemptive().basic("gaston", "gaston-password")
.header("fail-authentication", "true")
.get("/hello")
.then()
.statusCode(500)
.body(Matchers.is("Expected authentication failure"));
}

@Test
public void testCustomExceptionInIdentityAugmentor() {
RestAssured.given()
.auth().preemptive().basic("gaston", "gaston-password")
.header("fail-augmentation", "true")
.get("/hello")
.then()
.statusCode(500)
.body(Matchers.is("Expected identity augmentation failure"));
}

@Path("/hello")
public static class HelloResource {
@GET
public String hello(@Context SecurityContext context) {
var principalName = context.getUserPrincipal() == null ? "" : " " + context.getUserPrincipal().getName();
return "Hello" + principalName;
}

}

@Provider
public static class CustomRuntimeExceptionMapper implements ExceptionMapper<CustomRuntimeException> {
@Override
public Response toResponse(CustomRuntimeException exception) {
return Response.serverError().entity(exception.getMessage()).build();
}
}

@ApplicationScoped
public static class CustomIdentityAugmentor implements SecurityIdentityAugmentor {
@Override
public Uni<SecurityIdentity> augment(SecurityIdentity securityIdentity,
AuthenticationRequestContext authenticationRequestContext) {
return augment(securityIdentity, authenticationRequestContext, Map.of());
}

@Override
public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context,
Map<String, Object> attributes) {
final RoutingContext routingContext = HttpSecurityUtils.getRoutingContextAttribute(attributes);
if (routingContext.request().headers().contains("fail-augmentation")) {
return Uni.createFrom().failure(new CustomRuntimeException("Expected identity augmentation failure"));
}
return Uni.createFrom().item(identity);
}
}

public static class CustomRuntimeException extends RuntimeException {
public CustomRuntimeException(String message) {
super(message);
}
}

public static class UnhandledRuntimeException extends RuntimeException {
public UnhandledRuntimeException(String message) {
super(message);
}
}

@ApplicationScoped
public static class BasicIdentityProvider implements IdentityProvider<UsernamePasswordAuthenticationRequest> {

@Override
public Class<UsernamePasswordAuthenticationRequest> getRequestType() {
return UsernamePasswordAuthenticationRequest.class;
}

@Override
public Uni<SecurityIdentity> authenticate(UsernamePasswordAuthenticationRequest authRequest,
AuthenticationRequestContext authRequestCtx) {
if (!"gaston".equals(authRequest.getUsername())) {
return Uni.createFrom().nullItem();
}

final RoutingContext routingContext = HttpSecurityUtils.getRoutingContextAttribute(authRequest);
if (routingContext.request().headers().contains("fail-authentication")) {
return Uni.createFrom().failure(new CustomRuntimeException("Expected authentication failure"));
}
if (routingContext.request().headers().contains("fail-unhandled")) {
return Uni.createFrom().failure(new UnhandledRuntimeException("Expected unhandled failure"));
}
return Uni.createFrom()
.item(QuarkusSecurityIdentity.builder().setPrincipal(new QuarkusPrincipal("Gaston")).build());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.quarkus.resteasy.test.security;

import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;

public class LazyAuthCustomExceptionMapperTest extends AbstractCustomExceptionMapperTest {

@RegisterExtension
static QuarkusUnitTest runner = new QuarkusUnitTest().withApplicationRoot(jar -> jar
.addAsResource(new StringAsset("""
quarkus.http.auth.permission.authentication.paths=*
quarkus.http.auth.permission.authentication.policy=authenticated
quarkus.http.auth.proactive=false
"""), "application.properties"));

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.quarkus.resteasy.test.security;

import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;

public class ProactiveAuthCustomExceptionMapperTest extends AbstractCustomExceptionMapperTest {

@RegisterExtension
static QuarkusUnitTest runner = new QuarkusUnitTest().withApplicationRoot(jar -> jar
.addAsResource(new StringAsset("""
quarkus.http.auth.permission.authentication.paths=*
quarkus.http.auth.permission.authentication.policy=authenticated
"""), "application.properties"));

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package io.quarkus.resteasy.runtime.standalone;

import static io.quarkus.vertx.http.runtime.security.HttpSecurityRecorder.DefaultAuthFailureHandler.extractRootCause;
import static io.quarkus.vertx.http.runtime.security.HttpSecurityRecorder.DefaultAuthFailureHandler.isOtherAuthenticationFailure;
import static io.quarkus.vertx.http.runtime.security.HttpSecurityRecorder.DefaultAuthFailureHandler.markIfOtherAuthenticationFailure;
import static io.quarkus.vertx.http.runtime.security.HttpSecurityRecorder.DefaultAuthFailureHandler.removeMarkAsOtherAuthenticationFailure;

import java.lang.annotation.Annotation;
import java.lang.reflect.Proxy;
Expand Down Expand Up @@ -37,6 +40,7 @@
import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.AuthenticationRedirectException;
import io.quarkus.security.ForbiddenException;
import io.quarkus.security.UnauthorizedException;
import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig;
import io.quarkus.vertx.http.runtime.HttpCompressionHandler;
import io.quarkus.vertx.http.runtime.HttpConfiguration;
Expand Down Expand Up @@ -163,8 +167,16 @@ public void handle(RoutingContext request) {
}
}

if (request.failure() instanceof AuthenticationException
|| request.failure() instanceof ForbiddenException) {
final Throwable failure = request.failure();
final boolean isOtherAuthFailure = isOtherAuthenticationFailure(request)
&& isFailureHandledByExceptionMappers(failure);
if (isOtherAuthFailure) {
// prevent circular reference for unhandled exceptions
// (which is unnecessary if everything here is done right)
removeMarkAsOtherAuthenticationFailure(request);
super.handle(request);
} else if (failure instanceof AuthenticationException || failure instanceof UnauthorizedException
|| failure instanceof ForbiddenException) {
super.handle(request);
} else {
request.next();
Expand All @@ -179,6 +191,11 @@ protected void setCurrentIdentityAssociation(RoutingContext routingContext) {
}
}

private boolean isFailureHandledByExceptionMappers(Throwable failure) {
return failure != null && deployment != null
&& deployment.getProviderFactory().getExceptionMapper(failure.getClass()) != null;
}

public Handler<RoutingContext> defaultAuthFailureHandler() {
return new Handler<RoutingContext>() {
@Override
Expand All @@ -204,6 +221,7 @@ public void handle(RoutingContext event) {

@Override
public void accept(RoutingContext event, Throwable throwable) {
markIfOtherAuthenticationFailure(event, throwable);
if (!event.failed()) {
event.fail(extractRootCause(throwable));
}
Expand Down
Loading

0 comments on commit be70099

Please sign in to comment.