Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolve Sec. Identity in Reactive routes when Proactive Auth disabled #26529

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/src/main/asciidoc/security-built-in-authentication.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ when authentication is complete and the identity is available.

NOTE: It's still possible to access the `SecurityIdentity` synchronously with `public SecurityIdentity getIdentity()`
in the xref:resteasy-reactive.adoc[RESTEasy Reactive] from endpoints annotated with `@RolesAllowed`, `@Authenticated`,
or with respective configuration authorization checks as authentication has already happened.
or with respective configuration authorization checks as authentication has already happened. The same is also valid
for the xref:reactive-routes.adoc[Reactive routes] if a route response is synchronous.

=== How to customize authentication exception responses

Expand Down
4 changes: 4 additions & 0 deletions extensions/reactive-routes/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security-test-utils</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,26 @@ public final class AnnotatedRouteHandlerBuildItem extends MultiBuildItem {
private final MethodInfo method;
private final boolean blocking;
private final HttpCompression compression;
/**
* If true, always attempt to authenticate user right before the body handler is run
*/
private final boolean alwaysAuthenticateRoute;

public AnnotatedRouteHandlerBuildItem(BeanInfo bean, MethodInfo method, List<AnnotationInstance> routes,
AnnotationInstance routeBase) {
this(bean, method, routes, routeBase, false, HttpCompression.UNDEFINED);
this(bean, method, routes, routeBase, false, HttpCompression.UNDEFINED, false);
}

public AnnotatedRouteHandlerBuildItem(BeanInfo bean, MethodInfo method, List<AnnotationInstance> routes,
AnnotationInstance routeBase, boolean blocking, HttpCompression compression) {
AnnotationInstance routeBase, boolean blocking, HttpCompression compression, boolean alwaysAuthenticateRoute) {
super();
this.bean = bean;
this.routes = routes;
this.routeBase = routeBase;
this.method = method;
this.blocking = blocking;
this.compression = compression;
this.alwaysAuthenticateRoute = alwaysAuthenticateRoute;
}

public BeanInfo getBean() {
Expand All @@ -54,6 +59,10 @@ public boolean isBlocking() {
return blocking;
}

public boolean shouldAlwaysAuthenticateRoute() {
return alwaysAuthenticateRoute;
}

public HttpCompression getCompression() {
return compression;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import javax.annotation.security.DenyAll;
import javax.annotation.security.RolesAllowed;
import javax.enterprise.context.ContextNotActiveException;
import javax.enterprise.context.spi.Contextual;

Expand Down Expand Up @@ -94,6 +96,7 @@
import io.quarkus.runtime.LaunchMode;
import io.quarkus.runtime.TemplateHtmlBuilder;
import io.quarkus.runtime.util.HashUtil;
import io.quarkus.security.Authenticated;
import io.quarkus.vertx.http.deployment.FilterBuildItem;
import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem;
import io.quarkus.vertx.http.deployment.RequireBodyHandlerBuildItem;
Expand All @@ -102,6 +105,7 @@
import io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem;
import io.quarkus.vertx.http.deployment.devmode.RouteDescriptionBuildItem;
import io.quarkus.vertx.http.runtime.HandlerType;
import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig;
import io.quarkus.vertx.http.runtime.HttpCompression;
import io.quarkus.vertx.web.Param;
import io.quarkus.vertx.web.Route;
Expand Down Expand Up @@ -134,6 +138,9 @@ class ReactiveRoutesProcessor {
private static final String VALUE_ORDER = "order";
private static final String VALUE_TYPE = "type";
private static final String SLASH = "/";
private static final DotName ROLES_ALLOWED = DotName.createSimple(RolesAllowed.class.getName());
private static final DotName AUTHENTICATED = DotName.createSimple(Authenticated.class.getName());
private static final DotName DENY_ALL = DotName.createSimple(DenyAll.class.getName());

private static final List<ParameterInjector> PARAM_INJECTORS = initParamInjectors();

Expand Down Expand Up @@ -162,7 +169,8 @@ void validateBeanDeployment(
TransformedAnnotationsBuildItem transformedAnnotations,
BuildProducer<AnnotatedRouteHandlerBuildItem> routeHandlerBusinessMethods,
BuildProducer<AnnotatedRouteFilterBuildItem> routeFilterBusinessMethods,
BuildProducer<ValidationErrorBuildItem> errors) {
BuildProducer<ValidationErrorBuildItem> errors,
HttpBuildTimeConfig httpBuildTimeConfig) {

// Collect all business methods annotated with @Route and @RouteFilter
AnnotationStore annotationStore = validationPhase.getContext().get(BuildExtension.Key.ANNOTATION_STORE);
Expand Down Expand Up @@ -211,9 +219,28 @@ void validateBeanDeployment(
compression = HttpCompression.OFF;
}
}

// Authenticate user if the proactive authentication is disabled and the route is secured with
// an RBAC annotation that requires authentication as io.quarkus.security.runtime.interceptor.SecurityConstrainer
// access the SecurityIdentity in a synchronous manner
final boolean blocking = annotationStore.hasAnnotation(method, DotNames.BLOCKING);
final boolean alwaysAuthenticateRoute;
if (!httpBuildTimeConfig.auth.proactive && !blocking) {
final DotName returnTypeName = method.returnType().name();
// method either returns 'something' in a synchronous manner or void (in which case we can't tell)
final boolean possiblySynchronousResponse = !returnTypeName.equals(DotNames.UNI)
&& !returnTypeName.equals(DotNames.MULTI) && !returnTypeName.equals(DotNames.COMPLETION_STAGE);
final boolean hasRbacAnnotationThatRequiresAuth = annotationStore.hasAnnotation(method, ROLES_ALLOWED)
|| annotationStore.hasAnnotation(method, AUTHENTICATED)
|| annotationStore.hasAnnotation(method, DENY_ALL);
alwaysAuthenticateRoute = possiblySynchronousResponse && hasRbacAnnotationThatRequiresAuth;
} else {
alwaysAuthenticateRoute = false;
}

routeHandlerBusinessMethods
.produce(new AnnotatedRouteHandlerBuildItem(bean, method, routes, routeBaseAnnotation,
annotationStore.hasAnnotation(method, DotNames.BLOCKING), compression));
blocking, compression, alwaysAuthenticateRoute));
}
//
AnnotationInstance filterAnnotation = annotationStore.getAnnotation(method,
Expand Down Expand Up @@ -419,7 +446,7 @@ public boolean test(String name) {
RouteMatcher matcher = new RouteMatcher(path, regex, produces, consumes, methods, order);
matchers.put(matcher, businessMethod.getMethod());
Function<Router, io.vertx.ext.web.Route> routeFunction = recorder.createRouteFunction(matcher,
bodyHandler.getHandler());
bodyHandler.getHandler(), businessMethod.shouldAlwaysAuthenticateRoute());

//TODO This needs to be refactored to use routeFunction() taking a Consumer<Route> instead
RouteBuildItem.Builder builder = RouteBuildItem.builder()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package io.quarkus.vertx.web;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.is;

import javax.annotation.security.DenyAll;
import javax.annotation.security.RolesAllowed;
import javax.inject.Inject;

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

import io.quarkus.security.Authenticated;
import io.quarkus.security.runtime.SecurityIdentityAssociation;
import io.quarkus.security.test.utils.TestIdentityController;
import io.quarkus.security.test.utils.TestIdentityProvider;
import io.quarkus.test.QuarkusUnitTest;
import io.vertx.ext.web.RoutingContext;

public class LazyAuthRouteTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addAsResource(new StringAsset("quarkus.http.auth.proactive=false\n"), "application.properties")
.addClasses(TestIdentityProvider.class, TestIdentityController.class, HelloWorldBean.class));

@BeforeAll
public static void setupUsers() {
TestIdentityController.resetRoles()
.add("admin", "admin", "admin")
.add("user", "user", "user");
}

@Test
public void testAuthZInPlace() {
given().auth().basic("user", "user").when().get("/hello-ra").then().statusCode(403);
}

@Test
public void testRolesAllowedVoidMethod() {
given().auth().basic("admin", "admin").when().get("/hello-ra").then().statusCode(200).body(is("Hello admin"));
}

@Test
public void testRolesAllowedDirectResponse() {
given().auth().basic("admin", "admin").when().get("/hello-ra-direct").then().statusCode(200).body(is("Hello admin"));
}

@Test
public void testAuthenticated() {
given().auth().basic("user", "user").when().get("/hello-auth").then().statusCode(200);
}

@Test
public void testDenyAll() {
given().auth().basic("user", "user").when().get("/hello-deny").then().statusCode(403);
}

public static final class HelloWorldBean {

@Inject
SecurityIdentityAssociation securityIdentityAssociation;

@Authenticated
@Route(path = "/hello-auth", methods = Route.HttpMethod.GET)
public void greetingsAuth(RoutingContext rc) {
respond(rc);
}

@RolesAllowed("admin")
@Route(path = "/hello-ra", methods = Route.HttpMethod.GET)
public void greetingsRA(RoutingContext rc) {
respond(rc);
}

@RolesAllowed("admin")
@Route(path = "/hello-ra-direct", methods = Route.HttpMethod.GET)
public String greetingsRADirect() {
return hello();
}

@DenyAll
@Route(path = "/hello-deny", methods = Route.HttpMethod.GET)
public String greetingsDeny() {
return hello();
}

private void respond(RoutingContext rc) {
rc.response().end(hello());
}

private String hello() {
return "Hello " + securityIdentityAssociation.getIdentity().getPrincipal().getName();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;

import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.annotations.Recorder;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig;
import io.quarkus.vertx.http.runtime.HttpCompression;
import io.quarkus.vertx.http.runtime.HttpConfiguration;
import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.subscription.UniSubscriber;
import io.smallrye.mutiny.subscription.UniSubscription;
import io.vertx.core.Handler;
import io.vertx.core.http.HttpMethod;
import io.vertx.ext.web.Router;
Expand Down Expand Up @@ -56,7 +62,7 @@ public Handler<RoutingContext> compressRouteHandler(Handler<RoutingContext> rout
}

public Function<Router, io.vertx.ext.web.Route> createRouteFunction(RouteMatcher matcher,
Handler<RoutingContext> bodyHandler) {
Handler<RoutingContext> bodyHandler, boolean alwaysAuthenticateRoute) {
return new Function<Router, io.vertx.ext.web.Route>() {
@Override
public io.vertx.ext.web.Route apply(Router router) {
Expand Down Expand Up @@ -86,6 +92,38 @@ public io.vertx.ext.web.Route apply(Router router) {
route.consumes(consumes);
}
}
if (alwaysAuthenticateRoute) {
route = route.handler(routingContext -> {
// check auth haven't happened further up the handler chain
if (routingContext.user() == null) {
// authenticate -> on deferred identity (Uni's) termination user is set to the routing context,
// so SecurityIdentity will be accessible in a synchronous manner
routingContext.<Uni<SecurityIdentity>> get(QuarkusHttpUser.DEFERRED_IDENTITY_KEY)
.subscribe().withSubscriber(new UniSubscriber<Object>() {
@Override
public void onSubscribe(UniSubscription subscription) {
}

@Override
public void onItem(Object item) {
if (routingContext.response().ended()) {
return;
}
routingContext.next();
}

@Override
public void onFailure(Throwable failure) {
BiConsumer<RoutingContext, Throwable> handler = routingContext
.get(QuarkusHttpUser.AUTH_FAILURE_HANDLER);
if (handler != null) {
handler.accept(routingContext, failure);
}
}
});
}
});
}
if (bodyHandler != null) {
route.handler(bodyHandler);
}
Expand Down