Skip to content

Commit c32fbb0

Browse files
authored
fix: resolve flaky CSRF token verification tests (#4)
1 parent 28e4459 commit c32fbb0

File tree

2 files changed

+29
-14
lines changed

2 files changed

+29
-14
lines changed

example.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ describe("Elysia CSRF Plugin", () => {
7878
expect(invalidRes.status).toBe(403);
7979
});
8080

81-
test.skip("should accept POST with valid token", async () => {
81+
test("should accept POST with valid token", async () => {
8282
const app = new Elysia()
8383
.use(csrf({ cookie: true }))
8484
.get("/token", ({ csrfToken }) => ({ token: csrfToken() }))
@@ -104,7 +104,7 @@ describe("Elysia CSRF Plugin", () => {
104104
expect(data).toHaveProperty("success", true);
105105
});
106106

107-
test.skip("should extract token from custom header", async () => {
107+
test("should extract token from custom header", async () => {
108108
const app = new Elysia()
109109
.use(
110110
csrf({
@@ -143,7 +143,7 @@ describe("Elysia CSRF Plugin", () => {
143143
expect(successRes.status).toBe(200);
144144
});
145145

146-
test.skip("should allow custom ignored methods", async () => {
146+
test("should allow custom ignored methods", async () => {
147147
const app = new Elysia()
148148
.use(
149149
csrf({
@@ -166,7 +166,7 @@ describe("Elysia CSRF Plugin", () => {
166166
expect(getRes.status).toBe(200);
167167
});
168168

169-
test.skip("should apply custom cookie configuration", async () => {
169+
test("should apply custom cookie configuration", async () => {
170170
const app = new Elysia()
171171
.use(
172172
csrf({
@@ -191,7 +191,7 @@ describe("Elysia CSRF Plugin", () => {
191191
expect(cookies).toContain("SameSite=Strict");
192192
});
193193

194-
test.skip("should allow token reuse across requests", async () => {
194+
test("should allow token reuse across requests", async () => {
195195
const app = new Elysia()
196196
.use(csrf({ cookie: true }))
197197
.get("/token", ({ csrfToken }) => ({ token: csrfToken() }))
@@ -227,7 +227,7 @@ describe("Elysia CSRF Plugin", () => {
227227
expect(req2.status).toBe(200);
228228
});
229229

230-
test.skip("should support multiple token sources", async () => {
230+
test("should support multiple token sources", async () => {
231231
const app = new Elysia()
232232
.use(
233233
csrf({
@@ -279,7 +279,7 @@ describe("Elysia CSRF Plugin", () => {
279279
expect(headerRes.status).toBe(200);
280280
});
281281

282-
test.skip("should work with HTML forms", async () => {
282+
test("should work with HTML forms", async () => {
283283
const app = new Elysia()
284284
.use(csrf({ cookie: true }))
285285
.get("/form", ({ csrfToken }) => {
@@ -320,7 +320,7 @@ describe("Elysia CSRF Plugin", () => {
320320
expect(submitRes.status).toBe(200);
321321
});
322322

323-
test.skip("should support SPA pattern", async () => {
323+
test("should support SPA pattern", async () => {
324324
const app = new Elysia()
325325
.use(
326326
csrf({

src/index.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@ function tokenize(secret: string, salt: string): string {
4242
/**
4343
* Verify if a given token is valid for a given secret.
4444
*/
45-
function verifyToken(secret: string, token: string): boolean {
45+
function verifyToken(
46+
secret: string,
47+
token: string,
48+
saltLength: number
49+
): boolean {
4650
if (!secret || typeof secret !== "string") {
4751
return false;
4852
}
@@ -51,13 +55,14 @@ function verifyToken(secret: string, token: string): boolean {
5155
return false;
5256
}
5357

54-
const index = token.indexOf("-");
55-
56-
if (index === -1) {
58+
// The token format is: {salt}-{hash}
59+
// where salt is exactly saltLength characters
60+
// We need to check if there's a dash at the expected position
61+
if (token.length < saltLength + 1 || token[saltLength] !== "-") {
5762
return false;
5863
}
5964

60-
const salt = token.slice(0, index);
65+
const salt = token.slice(0, saltLength);
6166
const expected = tokenize(secret, salt);
6267

6368
// Constant-time comparison
@@ -207,6 +212,9 @@ export const csrf = (options: CsrfOptions = {}) => {
207212
},
208213
})
209214
.derive({ as: "scoped" }, ({ cookie }) => {
215+
// Cache the secret within this request context to avoid race conditions
216+
let cachedSecret: string | undefined;
217+
210218
// Helper to set cookie attributes
211219
const setCookieAttributes = (cookieValue: any) => {
212220
if (cookieConfig?.path) cookieValue.path = cookieConfig.path;
@@ -222,6 +230,11 @@ export const csrf = (options: CsrfOptions = {}) => {
222230

223231
// Helper to get or create secret from cookie
224232
const getOrCreateSecret = (): string => {
233+
// Return cached secret if available
234+
if (cachedSecret) {
235+
return cachedSecret;
236+
}
237+
225238
if (!cookieConfig) {
226239
throw new Error("CSRF: Cookie storage must be enabled");
227240
}
@@ -239,6 +252,8 @@ export const csrf = (options: CsrfOptions = {}) => {
239252
setCookieAttributes(cookieObj);
240253
}
241254

255+
// Cache the secret for this request context
256+
cachedSecret = secret;
242257
return secret;
243258
};
244259

@@ -296,7 +311,7 @@ export const csrf = (options: CsrfOptions = {}) => {
296311
}
297312

298313
// Verify token
299-
if (!verifyToken(secret, tokenValue)) {
314+
if (!verifyToken(secret, tokenValue, saltLength)) {
300315
return new Response("Invalid CSRF token", { status: 403 });
301316
}
302317
}

0 commit comments

Comments
 (0)