Skip to content

Commit

Permalink
Resolve Sec. Identity in Reactive routes when Proactive Auth disabled
Browse files Browse the repository at this point in the history
f ix #23547 for Reactive routes
  • Loading branch information
michalvavrik committed Jul 3, 2022
1 parent cc89f6c commit 922a661
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 7 deletions.
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

0 comments on commit 922a661

Please sign in to comment.