diff --git a/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRequestResponseReactiveFilter.java b/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRequestResponseReactiveFilter.java index 25df381ac7b59..f8f00b715f8a1 100644 --- a/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRequestResponseReactiveFilter.java +++ b/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRequestResponseReactiveFilter.java @@ -87,12 +87,26 @@ public void filter(ResteasyReactiveContainerRequestContext requestContext, Routi if (requestMethodIsSafe(requestContext)) { // safe HTTP method, tolerate the absence of a token if (isCsrfTokenRequired(routing, config)) { - // Set the CSRF cookie with a randomly generated value - byte[] tokenBytes = new byte[config.tokenSize]; - secureRandom.nextBytes(tokenBytes); - routing.put(CSRF_TOKEN_BYTES_KEY, tokenBytes); - routing.put(CSRF_TOKEN_KEY, Base64.getUrlEncoder().withoutPadding().encodeToString(tokenBytes)); - + if (cookieToken == null) { + generateNewCsrfToken(routing, config); + } else { + String csrfTokenHeaderParam = requestContext.getHeaderString(config.tokenHeaderName); + if (csrfTokenHeaderParam != null) { + LOG.debugf("CSRF token found in the token header"); + // Verify the header, make sure the header value, possibly signed, is returned as the next cookie value + verifyCsrfToken(requestContext, routing, config, cookieToken, csrfTokenHeaderParam); + } else if (!config.tokenSignatureKey.isEmpty()) { + // If the signature is required, then we can not use the current cookie value + // as the HTML form token key because it represents a signed value of the previous key + // and it will lead to the double-signing issue if this value is reused as the key. + // It should be fine for simple HTML forms anyway + generateNewCsrfToken(routing, config); + } else { + // Make sure the same cookie value is returned + routing.put(CSRF_TOKEN_KEY, cookieToken); + routing.put(CSRF_TOKEN_BYTES_KEY, Base64.getUrlDecoder().decode(cookieToken)); + } + } routing.put(NEW_COOKIE_REQUIRED, true); } } else if (config.verifyToken) { @@ -139,6 +153,14 @@ public void filter(ResteasyReactiveContainerRequestContext requestContext, Routi } } + private void generateNewCsrfToken(RoutingContext routing, CsrfReactiveConfig config) { + // Set the CSRF cookie with a randomly generated value + byte[] tokenBytes = new byte[config.tokenSize]; + secureRandom.nextBytes(tokenBytes); + routing.put(CSRF_TOKEN_BYTES_KEY, tokenBytes); + routing.put(CSRF_TOKEN_KEY, Base64.getUrlEncoder().withoutPadding().encodeToString(tokenBytes)); + } + private void verifyCsrfToken(ResteasyReactiveContainerRequestContext requestContext, RoutingContext routing, CsrfReactiveConfig config, String cookieToken, String csrfToken) { if (cookieToken == null) { @@ -160,6 +182,7 @@ private void verifyCsrfToken(ResteasyReactiveContainerRequestContext requestCont return; } else { routing.put(CSRF_TOKEN_KEY, csrfToken); + routing.put(CSRF_TOKEN_BYTES_KEY, Base64.getUrlDecoder().decode(csrfToken)); routing.put(CSRF_TOKEN_VERIFIED, true); // reset the cookie routing.put(NEW_COOKIE_REQUIRED, true); 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 a8a937e1906a8..e317f75c71eb3 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 @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import java.net.URL; @@ -56,6 +57,8 @@ public void testCsrfTokenInForm() throws Exception { assertNotNull(htmlPage.getWebResponse().getResponseHeaderValue("Set-Cookie")); assertEquals("alice:true:tokenHeaderIsSet=false", textPage.getContent()); + Cookie cookie1 = webClient.getCookieManager().getCookie("csrftoken"); + // This request which returns String is not CSRF protected textPage = webClient.getPage("http://localhost:8081/service/hello"); assertEquals("hello", textPage.getContent()); @@ -67,6 +70,11 @@ public void testCsrfTokenInForm() throws Exception { assertNotNull(htmlPage.getWebResponse().getResponseHeaderValue("Set-Cookie")); assertEquals("alice:true:tokenHeaderIsSet=false", textPage.getContent()); + Cookie cookie2 = webClient.getCookieManager().getCookie("csrftoken"); + + assertEquals(cookie1.getValue(), cookie2.getValue()); + assertTrue(cookie1.getExpires().before(cookie2.getExpires())); + webClient.getCookieManager().clearCookies(); } } @@ -366,6 +374,12 @@ private void assurePostFormPath(io.vertx.ext.web.client.WebClient vertxWebClient if (responseBody != null) { assertEquals(responseBody, result.result().bodyAsString(), path); } + if (expectedStatus != 400) { + String[] nextCookie = result.result().cookies().get(0).split(";"); + String[] cookieNameValue = nextCookie[0].trim().split("="); + assertEquals(csrfCookie.getName(), cookieNameValue[0]); + assertEquals(csrfCookie.getValue(), cookieNameValue[1]); + } } private void assurePostJsonPath(io.vertx.ext.web.client.WebClient vertxWebClient, String path,