diff --git a/extensions/resteasy-reactive/rest-csrf/deployment/pom.xml b/extensions/resteasy-reactive/rest-csrf/deployment/pom.xml
index ac74515d94073..963702bc639df 100644
--- a/extensions/resteasy-reactive/rest-csrf/deployment/pom.xml
+++ b/extensions/resteasy-reactive/rest-csrf/deployment/pom.xml
@@ -48,6 +48,11 @@
rest-assured
test
+
+ org.assertj
+ assertj-core
+ test
+
diff --git a/extensions/resteasy-reactive/rest-csrf/deployment/src/test/java/io/quarkus/csrf/reactive/CsrfDevModeTest.java b/extensions/resteasy-reactive/rest-csrf/deployment/src/test/java/io/quarkus/csrf/reactive/CsrfDevModeTest.java
new file mode 100644
index 0000000000000..d73a6611e291b
--- /dev/null
+++ b/extensions/resteasy-reactive/rest-csrf/deployment/src/test/java/io/quarkus/csrf/reactive/CsrfDevModeTest.java
@@ -0,0 +1,159 @@
+package io.quarkus.csrf.reactive;
+
+import static io.restassured.RestAssured.given;
+import static io.restassured.RestAssured.when;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Objects;
+import java.util.logging.LogRecord;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.FormParam;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+
+import org.hamcrest.Matchers;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.qute.Template;
+import io.quarkus.qute.TemplateInstance;
+import io.quarkus.test.QuarkusDevModeTest;
+import io.restassured.RestAssured;
+import io.restassured.config.EncoderConfig;
+import io.restassured.config.RestAssuredConfig;
+import io.restassured.http.ContentType;
+import io.restassured.response.ValidatableResponse;
+import io.smallrye.mutiny.Uni;
+
+public class CsrfDevModeTest {
+
+ private final static String COOKIE_NAME = "csrf-token";
+
+ @RegisterExtension
+ static final QuarkusDevModeTest test = new QuarkusDevModeTest()
+ .withApplicationRoot((jar) -> jar
+ .addClasses(TestResource.class)
+ .addAsResource(new StringAsset("quarkus.rest-csrf.token-size=32"), "application.properties")
+ .addAsResource("templates/csrfToken.html"))
+ .setLogRecordPredicate(r -> true);
+
+ @Test
+ void testGeneratedTokenSizeChange() {
+ String token = getToken();
+ testForm(token).statusCode(200).body(Matchers.equalTo("testName"));
+ assertTokenSize(token, 32);
+ assertTokenSizeLesserThan(token, 64);
+ test.modifyResourceFile("application.properties", s -> s.replace("32", "64"));
+
+ // old token size is wrong: 32 != 64
+ testForm(token).statusCode(400);
+ RestAssured.given()
+ .cookie(COOKIE_NAME, token)
+ .get("/csrfTokenForm")
+ .then()
+ .statusCode(400);
+
+ // new token size is correct, therefore expect success
+ token = getToken();
+ testForm(token).statusCode(200).body(Matchers.equalTo("testName"));
+ assertTokenSize(token, 64);
+
+ // assert hint to users that previously generated cookie is not valid anymore
+ var logMessages = test.getLogRecords().stream().map(LogRecord::getMessage).filter(Objects::nonNull).toList();
+ assertThat(logMessages).anyMatch(m -> m.contains("Make sure the browser cache is cleared"));
+ }
+
+ private static void assertTokenSize(String token, int expectedTokenSizeInBytes) {
+ byte[] tokenInBytes = token.getBytes();
+ int actualTokenSizeInBytes = tokenInBytes.length;
+ // encoded token bytes are always of equal or greater length than expected bytes
+ assertTrue(actualTokenSizeInBytes >= expectedTokenSizeInBytes,
+ () -> "Expected token size in bytes to be at least %d, but was %d: %s".formatted(expectedTokenSizeInBytes,
+ actualTokenSizeInBytes, token));
+ }
+
+ private static void assertTokenSizeLesserThan(String token, int expectedMaxTokenSize) {
+ byte[] tokenInBytes = token.getBytes();
+ int actualTokenSizeInBytes = tokenInBytes.length;
+ assertTrue(actualTokenSizeInBytes < expectedMaxTokenSize,
+ () -> "Expected token size in bytes to be lesser than %d, but was %d: %s".formatted(expectedMaxTokenSize,
+ actualTokenSizeInBytes, token));
+ }
+
+ private static ValidatableResponse testForm(String token) {
+ EncoderConfig encoderConfig = EncoderConfig.encoderConfig().encodeContentTypeAs("multipart/form-data",
+ ContentType.TEXT);
+ RestAssuredConfig restAssuredConfig = RestAssured.config().encoderConfig(encoderConfig);
+
+ //no token
+ given()
+ .cookie(COOKIE_NAME, token)
+ .config(restAssuredConfig)
+ .formParam("name", "testName")
+ .contentType(ContentType.URLENC)
+ .when()
+ .post("csrfTokenForm")
+ .then()
+ .statusCode(400);
+
+ //wrong token
+ given()
+ .cookie(COOKIE_NAME, token)
+ .config(restAssuredConfig)
+ .formParam(COOKIE_NAME, "WRONG")
+ .formParam("name", "testName")
+ .contentType(ContentType.URLENC)
+ .when()
+ .post("csrfTokenForm")
+ .then()
+ .statusCode(400);
+
+ //given token
+ return given()
+ .cookie(COOKIE_NAME, token)
+ .config(restAssuredConfig)
+ .formParam(COOKIE_NAME, token)
+ .formParam("name", "testName")
+ .contentType(ContentType.URLENC)
+ .when()
+ .post("csrfTokenForm")
+ .then();
+ }
+
+ private static String getToken() {
+ return when()
+ .get("/csrfTokenForm")
+ .then()
+ .statusCode(200)
+ .cookie(COOKIE_NAME)
+ .extract()
+ .cookie(COOKIE_NAME);
+ }
+
+ @Path("/csrfTokenForm")
+ public static class TestResource {
+
+ @Inject
+ Template csrfToken;
+
+ @GET
+ @Produces(MediaType.TEXT_HTML)
+ public TemplateInstance getCsrfTokenForm() {
+ return csrfToken.instance();
+ }
+
+ @POST
+ @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+ @Produces(MediaType.TEXT_PLAIN)
+ public Uni postCsrfTokenForm(@FormParam("name") String userName) {
+ return Uni.createFrom().item(userName);
+ }
+ }
+}
diff --git a/extensions/resteasy-reactive/rest-csrf/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRequestResponseReactiveFilter.java b/extensions/resteasy-reactive/rest-csrf/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRequestResponseReactiveFilter.java
index b833ed32702d1..ef9668ea4003c 100644
--- a/extensions/resteasy-reactive/rest-csrf/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRequestResponseReactiveFilter.java
+++ b/extensions/resteasy-reactive/rest-csrf/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRequestResponseReactiveFilter.java
@@ -16,6 +16,7 @@
import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext;
import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveContainerRequestContext;
+import io.quarkus.runtime.LaunchMode;
import io.vertx.core.http.Cookie;
import io.vertx.core.http.impl.CookieImpl;
import io.vertx.core.http.impl.ServerCookie;
@@ -71,8 +72,14 @@ public void filter(ResteasyReactiveContainerRequestContext requestContext, Routi
// HMAC SHA256 output is 32 bytes long
int expectedCookieTokenSize = config.tokenSignatureKey().isPresent() ? 32 : config.tokenSize();
if (cookieTokenSize != expectedCookieTokenSize) {
- LOG.debugf("Invalid CSRF token cookie size: expected %d, got %d", expectedCookieTokenSize,
- cookieTokenSize);
+ if (LaunchMode.current() == LaunchMode.DEVELOPMENT) {
+ LOG.infof(
+ "Invalid CSRF token cookie size: expected %d, got %d. Make sure the browser cache is cleared.",
+ expectedCookieTokenSize, cookieTokenSize);
+ } else {
+ LOG.debugf("Invalid CSRF token cookie size: expected %d, got %d", expectedCookieTokenSize,
+ cookieTokenSize);
+ }
requestContext.abortWith(badClientRequest());
return;
}