diff --git a/extensions/csrf-reactive/deployment/src/main/java/io/quarkus/csrf/reactive/CsrfReactiveBuildStep.java b/extensions/csrf-reactive/deployment/src/main/java/io/quarkus/csrf/reactive/CsrfReactiveBuildStep.java index 1028bfac08b5e..886289a8c1810 100644 --- a/extensions/csrf-reactive/deployment/src/main/java/io/quarkus/csrf/reactive/CsrfReactiveBuildStep.java +++ b/extensions/csrf-reactive/deployment/src/main/java/io/quarkus/csrf/reactive/CsrfReactiveBuildStep.java @@ -1,31 +1,15 @@ package io.quarkus.csrf.reactive; -import java.util.Collections; -import java.util.List; -import java.util.Map; import java.util.function.BooleanSupplier; -import org.jboss.jandex.ClassInfo; -import org.jboss.jandex.MethodInfo; -import org.jboss.resteasy.reactive.server.model.FixedHandlersChainCustomizer; -import org.jboss.resteasy.reactive.server.model.HandlerChainCustomizer; -import org.jboss.resteasy.reactive.server.processor.scanning.MethodScanner; - import io.quarkus.arc.deployment.AdditionalBeanBuildItem; -import io.quarkus.csrf.reactive.runtime.CsrfHandler; -import io.quarkus.csrf.reactive.runtime.CsrfReactiveConfig; -import io.quarkus.csrf.reactive.runtime.CsrfRecorder; -import io.quarkus.csrf.reactive.runtime.CsrfResponseFilter; +import io.quarkus.csrf.reactive.runtime.CsrfRequestResponseReactiveFilter; import io.quarkus.csrf.reactive.runtime.CsrfTokenParameterProvider; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.BuildSteps; -import io.quarkus.deployment.annotations.ExecutionTime; -import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.AdditionalIndexedClassesBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; -import io.quarkus.resteasy.reactive.server.spi.HandlerConfigurationProviderBuildItem; -import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem; @BuildSteps(onlyIf = CsrfReactiveBuildStep.IsEnabled.class) public class CsrfReactiveBuildStep { @@ -34,33 +18,11 @@ public class CsrfReactiveBuildStep { void registerProvider(BuildProducer additionalBeans, BuildProducer reflectiveClass, BuildProducer additionalIndexedClassesBuildItem) { - additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(CsrfResponseFilter.class)); - reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, CsrfResponseFilter.class)); - additionalIndexedClassesBuildItem - .produce(new AdditionalIndexedClassesBuildItem(CsrfResponseFilter.class.getName())); - + additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(CsrfRequestResponseReactiveFilter.class)); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, CsrfRequestResponseReactiveFilter.class)); additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(CsrfTokenParameterProvider.class)); - } - - @BuildStep - public MethodScannerBuildItem configureHandler() { - return new MethodScannerBuildItem(new MethodScanner() { - @Override - public List scan(MethodInfo method, ClassInfo actualEndpointClass, - Map methodContext) { - return Collections.singletonList( - new FixedHandlersChainCustomizer( - List.of(new CsrfHandler()), - HandlerChainCustomizer.Phase.BEFORE_METHOD_INVOKE)); - } - }); - } - - @BuildStep - @Record(ExecutionTime.RUNTIME_INIT) - public HandlerConfigurationProviderBuildItem applyRuntimeConfig(CsrfRecorder recorder, - CsrfReactiveConfig csrfReactiveConfig) { - return new HandlerConfigurationProviderBuildItem(CsrfReactiveConfig.class, recorder.configure(csrfReactiveConfig)); + additionalIndexedClassesBuildItem + .produce(new AdditionalIndexedClassesBuildItem(CsrfRequestResponseReactiveFilter.class.getName())); } public static class IsEnabled implements BooleanSupplier { diff --git a/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRecorder.java b/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRecorder.java deleted file mode 100644 index f1580c9b023dd..0000000000000 --- a/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRecorder.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.quarkus.csrf.reactive.runtime; - -import java.util.function.Supplier; - -import io.quarkus.runtime.annotations.Recorder; - -@Recorder -public class CsrfRecorder { - - public Supplier configure(CsrfReactiveConfig csrfReactiveConfig) { - return new Supplier() { - @Override - public CsrfReactiveConfig get() { - return csrfReactiveConfig; - } - }; - } - -} diff --git a/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfHandler.java b/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRequestResponseReactiveFilter.java similarity index 64% rename from extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfHandler.java rename to extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRequestResponseReactiveFilter.java index ac8f2c340fdf7..29e7b1458c65c 100644 --- a/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfHandler.java +++ b/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRequestResponseReactiveFilter.java @@ -1,23 +1,29 @@ package io.quarkus.csrf.reactive.runtime; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.VarHandle; import java.security.SecureRandom; import java.util.Base64; +import javax.enterprise.inject.Instance; +import javax.inject.Inject; import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerResponseContext; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.server.ServerRequestFilter; +import org.jboss.resteasy.reactive.server.ServerResponseFilter; +import org.jboss.resteasy.reactive.server.WithFormRead; import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; -import org.jboss.resteasy.reactive.server.spi.GenericRuntimeConfigurableServerRestHandler; +import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveContainerRequestContext; import io.vertx.core.http.Cookie; +import io.vertx.core.http.impl.CookieImpl; +import io.vertx.core.http.impl.ServerCookie; import io.vertx.ext.web.RoutingContext; -public class CsrfHandler implements GenericRuntimeConfigurableServerRestHandler { - private static final Logger LOG = Logger.getLogger(CsrfHandler.class); +public class CsrfRequestResponseReactiveFilter { + private static final Logger LOG = Logger.getLogger(CsrfRequestResponseReactiveFilter.class); /** * CSRF token key. @@ -30,27 +36,12 @@ public class CsrfHandler implements GenericRuntimeConfigurableServerRestHandler< */ private static final String CSRF_TOKEN_VERIFIED = "csrf_token_verified"; - // although technically the field does not need to be volatile (since the access mode is determined by the VarHandle use) - // it is a recommended practice by Doug Lea meant to catch cases where the field is accessed directly (by accident) - @SuppressWarnings("unused") - private volatile SecureRandom secureRandom; - - // use a VarHandle to access the secureRandom as the value is written only by the main thread - // and all other threads simply read the value, and thus we can use the Release / Acquire access mode - private static final VarHandle SECURE_RANDOM_VH; - - static { - try { - SECURE_RANDOM_VH = MethodHandles.lookup().findVarHandle(CsrfHandler.class, "secureRandom", - SecureRandom.class); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new Error(e); - } - } + private final SecureRandom secureRandom = new SecureRandom(); - private CsrfReactiveConfig config; + @Inject + Instance configInstance; - public CsrfHandler() { + public CsrfRequestResponseReactiveFilter() { } /** @@ -68,10 +59,10 @@ public CsrfHandler() { * {@value #CSRF_TOKEN_KEY} and value that is equal to the one supplied in the cookie. * */ - public void handle(ResteasyReactiveRequestContext reactiveRequestContext) { - final ContainerRequestContext requestContext = reactiveRequestContext.getContainerRequestContext(); - - final RoutingContext routing = reactiveRequestContext.serverRequest().unwrap(RoutingContext.class); + @ServerRequestFilter + @WithFormRead + public void filter(ResteasyReactiveContainerRequestContext requestContext, RoutingContext routing) { + final CsrfReactiveConfig config = this.configInstance.get(); String cookieToken = getCookieToken(routing, config); if (cookieToken != null) { @@ -99,7 +90,7 @@ public void handle(ResteasyReactiveRequestContext reactiveRequestContext) { if (cookieToken == null && isCsrfTokenRequired(routing, config)) { // Set the CSRF cookie with a randomly generated value byte[] tokenBytes = new byte[config.tokenSize]; - getSecureRandom().nextBytes(tokenBytes); + secureRandom.nextBytes(tokenBytes); routing.put(CSRF_TOKEN_BYTES_KEY, tokenBytes); routing.put(CSRF_TOKEN_KEY, Base64.getUrlEncoder().withoutPadding().encodeToString(tokenBytes)); } @@ -115,7 +106,6 @@ public void handle(ResteasyReactiveRequestContext reactiveRequestContext) { } else { LOG.debugf("Request has the media type: %s, skipping the token verification", requestContext.getMediaType().toString()); - requestContext.abortWith(badClientRequest()); return; } } @@ -132,8 +122,9 @@ public void handle(ResteasyReactiveRequestContext reactiveRequestContext) { return; } - String csrfToken = (String) reactiveRequestContext.getFormParameter(config.formFieldName, true, true); - + ResteasyReactiveRequestContext rrContext = (ResteasyReactiveRequestContext) requestContext + .getServerRequestContext(); + String csrfToken = (String) rrContext.getFormParameter(config.formFieldName, true, false); if (csrfToken == null) { LOG.debug("CSRF token is not found"); requestContext.abortWith(badClientRequest()); @@ -148,6 +139,7 @@ public void handle(ResteasyReactiveRequestContext reactiveRequestContext) { return; } else { routing.put(CSRF_TOKEN_VERIFIED, true); + return; } } } else if (cookieToken == null) { @@ -156,10 +148,6 @@ public void handle(ResteasyReactiveRequestContext reactiveRequestContext) { } } - private SecureRandom getSecureRandom() { - return (SecureRandom) SECURE_RANDOM_VH.getAcquire(this); - } - private static boolean isMatchingMediaType(MediaType contentType, MediaType expectedType) { return contentType.getType().equals(expectedType.getType()) && contentType.getSubtype().equals(expectedType.getSubtype()); @@ -169,6 +157,47 @@ private static Response badClientRequest() { return Response.status(400).build(); } + /** + * If the requirements below are true, sets a cookie by the name {@value #CSRF_TOKEN_KEY} that contains a CSRF token. + *
    + *
  • The request method is {@code GET}.
  • + *
  • The request does not contain a valid CSRF token cookie.
  • + *
+ * + * @throws IllegalStateException if the {@link RoutingContext} does not have a value for the key {@value #CSRF_TOKEN_KEY} + * and a cookie needs to be set. + */ + @ServerResponseFilter + public void filter(ContainerRequestContext requestContext, + ContainerResponseContext responseContext, RoutingContext routing) { + final CsrfReactiveConfig config = configInstance.get(); + if (requestContext.getMethod().equals("GET") && isCsrfTokenRequired(routing, config) + && getCookieToken(routing, config) == null) { + + String cookieValue = null; + if (config.tokenSignatureKey.isPresent()) { + byte[] csrfTokenBytes = (byte[]) routing.get(CSRF_TOKEN_BYTES_KEY); + + if (csrfTokenBytes == null) { + throw new IllegalStateException( + "CSRF Filter should have set the property " + CSRF_TOKEN_KEY + ", but it is null"); + } + cookieValue = CsrfTokenUtils.signCsrfToken(csrfTokenBytes, config.tokenSignatureKey.get()); + } else { + String csrfToken = (String) routing.get(CSRF_TOKEN_KEY); + + if (csrfToken == null) { + throw new IllegalStateException( + "CSRF Filter should have set the property " + CSRF_TOKEN_KEY + ", but it is null"); + } + cookieValue = csrfToken; + } + + createCookie(cookieValue, routing, config); + } + + } + /** * Gets the CSRF token from the CSRF cookie from the current {@code RoutingContext}. * @@ -189,6 +218,19 @@ private boolean isCsrfTokenRequired(RoutingContext routing, CsrfReactiveConfig c return config.createTokenPath.isPresent() ? config.createTokenPath.get().contains(routing.request().path()) : true; } + private void createCookie(String csrfToken, RoutingContext routing, CsrfReactiveConfig config) { + + ServerCookie cookie = new CookieImpl(config.cookieName, csrfToken); + cookie.setHttpOnly(true); + cookie.setSecure(config.cookieForceSecure || routing.request().isSSL()); + cookie.setMaxAge(config.cookieMaxAge.toSeconds()); + cookie.setPath(config.cookiePath); + if (config.cookieDomain.isPresent()) { + cookie.setDomain(config.cookieDomain.get()); + } + routing.response().addCookie(cookie); + } + private static boolean requestMethodIsSafe(ContainerRequestContext context) { switch (context.getMethod()) { case "GET": @@ -199,14 +241,4 @@ private static boolean requestMethodIsSafe(ContainerRequestContext context) { return false; } } - - public void configure(CsrfReactiveConfig configuration) { - this.config = configuration; - SECURE_RANDOM_VH.setRelease(this, new SecureRandom()); - } - - @Override - public Class getConfigurationClass() { - return CsrfReactiveConfig.class; - } } diff --git a/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfResponseFilter.java b/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfResponseFilter.java deleted file mode 100644 index 7cb34dbd5eaa7..0000000000000 --- a/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfResponseFilter.java +++ /dev/null @@ -1,104 +0,0 @@ -package io.quarkus.csrf.reactive.runtime; - -import javax.enterprise.inject.Instance; -import javax.inject.Inject; -import javax.ws.rs.container.ContainerRequestContext; -import javax.ws.rs.container.ContainerResponseContext; - -import org.jboss.logging.Logger; -import org.jboss.resteasy.reactive.server.ServerResponseFilter; - -import io.vertx.core.http.Cookie; -import io.vertx.core.http.impl.CookieImpl; -import io.vertx.core.http.impl.ServerCookie; -import io.vertx.ext.web.RoutingContext; - -public class CsrfResponseFilter { - private static final Logger LOG = Logger.getLogger(CsrfResponseFilter.class); - - /** - * CSRF token key. - */ - private static final String CSRF_TOKEN_KEY = "csrf_token"; - private static final String CSRF_TOKEN_BYTES_KEY = "csrf_token_bytes"; - - @Inject - Instance config; - - public CsrfResponseFilter() { - } - - /** - * If the requirements below are true, sets a cookie by the name {@value #CSRF_TOKEN_KEY} that contains a CSRF token. - *
    - *
  • The request method is {@code GET}.
  • - *
  • The request does not contain a valid CSRF token cookie.
  • - *
- * - * @throws IllegalStateException if the {@link RoutingContext} does not have a value for the key {@value #CSRF_TOKEN_KEY} - * and a cookie needs to be set. - */ - @ServerResponseFilter - public void filter(ContainerRequestContext requestContext, - ContainerResponseContext responseContext, RoutingContext routing) { - if (requestContext.getMethod().equals("GET") && isCsrfTokenRequired(routing, config.get()) - && getCookieToken(routing, config.get()) == null) { - - String cookieValue = null; - if (config.get().tokenSignatureKey.isPresent()) { - byte[] csrfTokenBytes = (byte[]) routing.get(CSRF_TOKEN_BYTES_KEY); - - if (csrfTokenBytes == null) { - throw new IllegalStateException( - "CSRF Filter should have set the property " + CSRF_TOKEN_KEY + ", but it is null"); - } - cookieValue = CsrfTokenUtils.signCsrfToken(csrfTokenBytes, config.get().tokenSignatureKey.get()); - } else { - String csrfToken = (String) routing.get(CSRF_TOKEN_KEY); - - if (csrfToken == null) { - throw new IllegalStateException( - "CSRF Filter should have set the property " + CSRF_TOKEN_KEY + ", but it is null"); - } - cookieValue = csrfToken; - } - - createCookie(cookieValue, routing, config.get()); - } - - } - - /** - * Gets the CSRF token from the CSRF cookie from the current {@code RoutingContext}. - * - * @return An Optional containing the token, or an empty Optional if the token cookie is not present or is invalid - */ - private String getCookieToken(RoutingContext routing, CsrfReactiveConfig config) { - Cookie cookie = routing.getCookie(config.cookieName); - - if (cookie == null) { - LOG.debug("CSRF token cookie is not set"); - return null; - } - - return cookie.getValue(); - } - - private boolean isCsrfTokenRequired(RoutingContext routing, CsrfReactiveConfig config) { - return config.createTokenPath.isPresent() ? config.createTokenPath.get().contains(routing.request().path()) : true; - } - - private void createCookie(String csrfToken, RoutingContext routing, CsrfReactiveConfig config) { - - ServerCookie cookie = new CookieImpl(config.cookieName, csrfToken); - cookie.setHttpOnly(true); - cookie.setSecure(config.cookieForceSecure || routing.request().isSSL()); - cookie.setMaxAge(config.cookieMaxAge.toSeconds()); - cookie.setPath(config.cookiePath); - if (config.cookieDomain.isPresent()) { - cookie.setDomain(config.cookieDomain.get()); - } - routing.response().addCookie(cookie); - } - -} diff --git a/integration-tests/csrf-reactive/src/main/java/io/quarkus/it/csrf/TestResource.java b/integration-tests/csrf-reactive/src/main/java/io/quarkus/it/csrf/TestResource.java index 9814e1d4fe4ae..d882897fcb82f 100644 --- a/integration-tests/csrf-reactive/src/main/java/io/quarkus/it/csrf/TestResource.java +++ b/integration-tests/csrf-reactive/src/main/java/io/quarkus/it/csrf/TestResource.java @@ -27,6 +27,9 @@ public class TestResource { @Inject Template csrfTokenForm; + @Inject + Template csrfTokenWithFormRead; + @Inject Template csrfTokenMultipart; @@ -40,6 +43,13 @@ public TemplateInstance getCsrfTokenForm() { return csrfTokenForm.instance(); } + @GET + @Path("/csrfTokenWithFormRead") + @Produces(MediaType.TEXT_HTML) + public TemplateInstance getCsrfTokenWithFormRead() { + return csrfTokenWithFormRead.instance(); + } + @POST @Path("/csrfTokenForm") @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @@ -48,6 +58,14 @@ public String postCsrfTokenForm(@FormParam("name") String name) { return name + ":" + routingContext.get("csrf_token_verified", false); } + @POST + @Path("/csrfTokenWithFormRead") + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Produces(MediaType.TEXT_PLAIN) + public String postCsrfTokenWithFormRead() { + return "verified:" + routingContext.get("csrf_token_verified", false); + } + @GET @Path("/csrfTokenMultipart") @Produces(MediaType.TEXT_HTML) diff --git a/integration-tests/csrf-reactive/src/main/resources/application.properties b/integration-tests/csrf-reactive/src/main/resources/application.properties index d84f200ebf014..add9b0a990af0 100644 --- a/integration-tests/csrf-reactive/src/main/resources/application.properties +++ b/integration-tests/csrf-reactive/src/main/resources/application.properties @@ -1,4 +1,4 @@ quarkus.csrf-reactive.cookie-name=csrftoken -quarkus.csrf-reactive.create-token-path=/service/csrfTokenForm,/service/csrfTokenMultipart +quarkus.csrf-reactive.create-token-path=/service/csrfTokenForm,/service/csrfTokenWithFormRead,/service/csrfTokenMultipart quarkus.csrf-reactive.token-signature-key=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow diff --git a/integration-tests/csrf-reactive/src/main/resources/templates/csrfTokenWithFormRead.html b/integration-tests/csrf-reactive/src/main/resources/templates/csrfTokenWithFormRead.html new file mode 100644 index 0000000000000..d68a90c3849f3 --- /dev/null +++ b/integration-tests/csrf-reactive/src/main/resources/templates/csrfTokenWithFormRead.html @@ -0,0 +1,17 @@ + + + + +CSRF Token With Form Read Test + + +

CSRF Test

+ +
+ + +

Your Name:

+

+
+ + diff --git a/integration-tests/csrf-reactive/src/test/java/io/quarkus/it/csrf/CsrfReactiveTest.java b/integration-tests/csrf-reactive/src/test/java/io/quarkus/it/csrf/CsrfReactiveTest.java index f1976362b4cac..9c0302b75f8da 100644 --- a/integration-tests/csrf-reactive/src/test/java/io/quarkus/it/csrf/CsrfReactiveTest.java +++ b/integration-tests/csrf-reactive/src/test/java/io/quarkus/it/csrf/CsrfReactiveTest.java @@ -46,6 +46,31 @@ public void testCsrfTokenInForm() throws Exception { } } + @Test + public void testCsrfTokenWithFormRead() throws Exception { + try (final WebClient webClient = createWebClient()) { + + HtmlPage htmlPage = webClient.getPage("http://localhost:8081/service/csrfTokenWithFormRead"); + + assertEquals("CSRF Token With Form Read Test", htmlPage.getTitleText()); + + HtmlForm loginForm = htmlPage.getForms().get(0); + + loginForm.getInputByName("name").setValueAttribute("alice"); + + assertNotNull(webClient.getCookieManager().getCookie("csrftoken")); + + TextPage textPage = loginForm.getInputByName("submit").click(); + + assertEquals("verified:true", textPage.getContent()); + + textPage = webClient.getPage("http://localhost:8081/service/hello"); + assertEquals("hello", textPage.getContent()); + + webClient.getCookieManager().clearCookies(); + } + } + @Test public void testCsrfTokenInFormButNoCookie() throws Exception { try (final WebClient webClient = createWebClient()) { @@ -148,6 +173,25 @@ public void testWrongCsrfTokenFormValue() throws Exception { } } + @Test + public void testWrongCsrfTokenWithFormRead() throws Exception { + try (final WebClient webClient = createWebClient()) { + + HtmlPage htmlPage = webClient.getPage("http://localhost:8081/service/csrfTokenWithFormRead"); + + assertEquals("CSRF Token With Form Read Test", htmlPage.getTitleText()); + + assertNotNull(webClient.getCookieManager().getCookie("csrftoken")); + + RestAssured.given().urlEncodingEnabled(true) + .param("csrf-token", "wrong-value") + .post("/service/csrfTokenWithFormRead") + .then().statusCode(400); + + webClient.getCookieManager().clearCookies(); + } + } + private WebClient createWebClient() { WebClient webClient = new WebClient(); webClient.setCssErrorHandler(new SilentCssErrorHandler());