Skip to content

Commit 38a9abe

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

File tree

3 files changed

+178
-0
lines changed

3 files changed

+178
-0
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 hit 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("Generated token size has changed"));
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/RestCsrfConfigHolder.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,18 @@
44

55
import jakarta.enterprise.context.ApplicationScoped;
66

7+
import org.jboss.logging.Logger;
8+
9+
import io.quarkus.runtime.LaunchMode;
710
import io.quarkus.vertx.http.runtime.VertxHttpBuildTimeConfig;
811
import io.quarkus.vertx.http.runtime.VertxHttpConfig;
912

1013
@ApplicationScoped
1114
public class RestCsrfConfigHolder {
1215

16+
// used in DEV mode to detect changes in the generated token size
17+
private static volatile Integer previousTokenSize = null;
18+
1319
private final RestCsrfConfig config;
1420

1521
RestCsrfConfigHolder(RestCsrfConfig config, VertxHttpConfig httpConfig, VertxHttpBuildTimeConfig httpBuildTimeConfig) {
@@ -18,6 +24,14 @@ public class RestCsrfConfigHolder {
1824
} else {
1925
this.config = config;
2026
}
27+
if (LaunchMode.current() == LaunchMode.DEVELOPMENT) {
28+
if (previousTokenSize != null && previousTokenSize != this.config.tokenSize()) {
29+
Logger.getLogger(RestCsrfConfigHolder.class).infof("Generated token size has changed from %d to %d." +
30+
" Previously generated CSRF tokens are not valid anymore. Consider deleting the '%s' cookie in your browser.",
31+
previousTokenSize, this.config.tokenSize(), this.config.cookieName());
32+
}
33+
previousTokenSize = this.config.tokenSize();
34+
}
2135
}
2236

2337
RestCsrfConfig getConfig() {

0 commit comments

Comments
 (0)