Skip to content

Commit 48f96e9

Browse files
committed
feat(rest-csrf): inform in dev mode if token size changed
1 parent e25d7c6 commit 48f96e9

File tree

3 files changed

+173
-2
lines changed

3 files changed

+173
-2
lines changed

extensions/resteasy-reactive/rest-csrf/deployment/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@
4848
<artifactId>rest-assured</artifactId>
4949
<scope>test</scope>
5050
</dependency>
51+
<dependency>
52+
<groupId>org.assertj</groupId>
53+
<artifactId>assertj-core</artifactId>
54+
<scope>test</scope>
55+
</dependency>
5156
</dependencies>
5257

5358
<build>
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package io.quarkus.csrf.reactive;
2+
3+
import static io.restassured.RestAssured.given;
4+
import static io.restassured.RestAssured.when;
5+
import static org.assertj.core.api.Assertions.assertThat;
6+
import static org.junit.jupiter.api.Assertions.assertTrue;
7+
8+
import java.util.Objects;
9+
import java.util.logging.LogRecord;
10+
11+
import jakarta.inject.Inject;
12+
import jakarta.ws.rs.Consumes;
13+
import jakarta.ws.rs.FormParam;
14+
import jakarta.ws.rs.GET;
15+
import jakarta.ws.rs.POST;
16+
import jakarta.ws.rs.Path;
17+
import jakarta.ws.rs.Produces;
18+
import jakarta.ws.rs.core.MediaType;
19+
20+
import org.hamcrest.Matchers;
21+
import org.jboss.shrinkwrap.api.asset.StringAsset;
22+
import org.junit.jupiter.api.Test;
23+
import org.junit.jupiter.api.extension.RegisterExtension;
24+
25+
import io.quarkus.qute.Template;
26+
import io.quarkus.qute.TemplateInstance;
27+
import io.quarkus.test.QuarkusDevModeTest;
28+
import io.restassured.RestAssured;
29+
import io.restassured.config.EncoderConfig;
30+
import io.restassured.config.RestAssuredConfig;
31+
import io.restassured.http.ContentType;
32+
import io.restassured.response.ValidatableResponse;
33+
import io.smallrye.mutiny.Uni;
34+
35+
public class CsrfDevModeTest {
36+
37+
private final static String COOKIE_NAME = "csrf-token";
38+
39+
@RegisterExtension
40+
static final QuarkusDevModeTest test = new QuarkusDevModeTest()
41+
.withApplicationRoot((jar) -> jar
42+
.addClasses(TestResource.class)
43+
.addAsResource(new StringAsset("quarkus.rest-csrf.token-size=32"), "application.properties")
44+
.addAsResource("templates/csrfToken.html"))
45+
.setLogRecordPredicate(r -> true);
46+
47+
@Test
48+
void testGeneratedTokenSizeChange() {
49+
String token = getToken();
50+
testForm(token).statusCode(200).body(Matchers.equalTo("testName"));
51+
assertTokenSize(token, 32);
52+
assertTokenSizeLesserThan(token, 64);
53+
test.modifyResourceFile("application.properties", s -> s.replace("32", "64"));
54+
55+
// old token size is wrong: 32 != 64
56+
testForm(token).statusCode(400);
57+
RestAssured.given()
58+
.cookie(COOKIE_NAME, token)
59+
.get("/csrfTokenForm")
60+
.then()
61+
.statusCode(400);
62+
63+
// new token size is correct, therefore expect success
64+
token = getToken();
65+
testForm(token).statusCode(200).body(Matchers.equalTo("testName"));
66+
assertTokenSize(token, 64);
67+
68+
// assert hint to users that previously generated cookie is not valid anymore
69+
var logMessages = test.getLogRecords().stream().map(LogRecord::getMessage).filter(Objects::nonNull).toList();
70+
assertThat(logMessages).anyMatch(m -> m.contains("Make sure the browser cache is cleared"));
71+
}
72+
73+
private static void assertTokenSize(String token, int expectedTokenSizeInBytes) {
74+
byte[] tokenInBytes = token.getBytes();
75+
int actualTokenSizeInBytes = tokenInBytes.length;
76+
// encoded token bytes are always of equal or greater length than expected bytes
77+
assertTrue(actualTokenSizeInBytes >= expectedTokenSizeInBytes,
78+
() -> "Expected token size in bytes to be at least %d, but was %d: %s".formatted(expectedTokenSizeInBytes,
79+
actualTokenSizeInBytes, token));
80+
}
81+
82+
private static void assertTokenSizeLesserThan(String token, int expectedMaxTokenSize) {
83+
byte[] tokenInBytes = token.getBytes();
84+
int actualTokenSizeInBytes = tokenInBytes.length;
85+
assertTrue(actualTokenSizeInBytes < expectedMaxTokenSize,
86+
() -> "Expected token size in bytes to be lesser than %d, but was %d: %s".formatted(expectedMaxTokenSize,
87+
actualTokenSizeInBytes, token));
88+
}
89+
90+
private static ValidatableResponse testForm(String token) {
91+
EncoderConfig encoderConfig = EncoderConfig.encoderConfig().encodeContentTypeAs("multipart/form-data",
92+
ContentType.TEXT);
93+
RestAssuredConfig restAssuredConfig = RestAssured.config().encoderConfig(encoderConfig);
94+
95+
//no token
96+
given()
97+
.cookie(COOKIE_NAME, token)
98+
.config(restAssuredConfig)
99+
.formParam("name", "testName")
100+
.contentType(ContentType.URLENC)
101+
.when()
102+
.post("csrfTokenForm")
103+
.then()
104+
.statusCode(400);
105+
106+
//wrong token
107+
given()
108+
.cookie(COOKIE_NAME, token)
109+
.config(restAssuredConfig)
110+
.formParam(COOKIE_NAME, "WRONG")
111+
.formParam("name", "testName")
112+
.contentType(ContentType.URLENC)
113+
.when()
114+
.post("csrfTokenForm")
115+
.then()
116+
.statusCode(400);
117+
118+
//given token
119+
return given()
120+
.cookie(COOKIE_NAME, token)
121+
.config(restAssuredConfig)
122+
.formParam(COOKIE_NAME, token)
123+
.formParam("name", "testName")
124+
.contentType(ContentType.URLENC)
125+
.when()
126+
.post("csrfTokenForm")
127+
.then();
128+
}
129+
130+
private static String getToken() {
131+
return when()
132+
.get("/csrfTokenForm")
133+
.then()
134+
.statusCode(200)
135+
.cookie(COOKIE_NAME)
136+
.extract()
137+
.cookie(COOKIE_NAME);
138+
}
139+
140+
@Path("/csrfTokenForm")
141+
public static class TestResource {
142+
143+
@Inject
144+
Template csrfToken;
145+
146+
@GET
147+
@Produces(MediaType.TEXT_HTML)
148+
public TemplateInstance getCsrfTokenForm() {
149+
return csrfToken.instance();
150+
}
151+
152+
@POST
153+
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
154+
@Produces(MediaType.TEXT_PLAIN)
155+
public Uni<String> postCsrfTokenForm(@FormParam("name") String userName) {
156+
return Uni.createFrom().item(userName);
157+
}
158+
}
159+
}

extensions/resteasy-reactive/rest-csrf/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRequestResponseReactiveFilter.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext;
1717
import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveContainerRequestContext;
1818

19+
import io.quarkus.runtime.LaunchMode;
1920
import io.vertx.core.http.Cookie;
2021
import io.vertx.core.http.impl.CookieImpl;
2122
import io.vertx.core.http.impl.ServerCookie;
@@ -71,8 +72,14 @@ public void filter(ResteasyReactiveContainerRequestContext requestContext, Routi
7172
// HMAC SHA256 output is 32 bytes long
7273
int expectedCookieTokenSize = config.tokenSignatureKey().isPresent() ? 32 : config.tokenSize();
7374
if (cookieTokenSize != expectedCookieTokenSize) {
74-
LOG.debugf("Invalid CSRF token cookie size: expected %d, got %d", expectedCookieTokenSize,
75-
cookieTokenSize);
75+
if (LaunchMode.current() == LaunchMode.DEVELOPMENT) {
76+
LOG.infof(
77+
"Invalid CSRF token cookie size: expected %d, got %d. Make sure the browser cache is cleared.",
78+
expectedCookieTokenSize, cookieTokenSize);
79+
} else {
80+
LOG.debugf("Invalid CSRF token cookie size: expected %d, got %d", expectedCookieTokenSize,
81+
cookieTokenSize);
82+
}
7683
requestContext.abortWith(badClientRequest());
7784
return;
7885
}

0 commit comments

Comments
 (0)