diff --git a/tests/doublecsrf.test.ts b/tests/doublecsrf.test.ts index 8e5566f..2c77011 100644 --- a/tests/doublecsrf.test.ts +++ b/tests/doublecsrf.test.ts @@ -60,7 +60,7 @@ describe("csrf-csrf token-rotation", () => { const SECRET1 = "secret1" const SECRET2 = "secret2" - const generateMocksWithMultipleSecrets = (secrets: string[] | string) => { + const generateMocksWithMultipleSecrets = async (secrets: string[] | string) => { const { generateToken, validateRequest } = doubleCsrf({ ...doubleCsrfOptions, getSecret: () => secrets, @@ -68,7 +68,7 @@ describe("csrf-csrf token-rotation", () => { }) return { - ...generateMocksWithToken({ + ...await generateMocksWithToken({ cookieName, generateToken, validateRequest, @@ -78,62 +78,67 @@ describe("csrf-csrf token-rotation", () => { } } - describe("validating requests with combination of different secret/s", () => { + describe("validating requests with combination of different secret/s", async () => { // Generate request --> CSRF token with secret1 // We will then match a request with token and secret1 with other combinations of secrets - const { mockRequest, validateRequest } = generateMocksWithMultipleSecrets(SECRET1) - assert.isTrue(validateRequest(mockRequest)) + const { mockRequest, validateRequest } = await generateMocksWithMultipleSecrets(SECRET1) + assert.isTrue(await validateRequest(mockRequest)) - it("should be valid with 1 matching secret", () => { - assert.isTrue(generateMocksWithMultipleSecrets(SECRET1).validateRequest(mockRequest)) + it("should be valid with 1 matching secret", async () => { + const { validateRequest } = await generateMocksWithMultipleSecrets(SECRET1) + assert.isTrue(await validateRequest(mockRequest)) }) - it("should be valid with 1/1 matching secret in array", () => { - assert.isTrue(generateMocksWithMultipleSecrets([SECRET1]).validateRequest(mockRequest)) + it("should be valid with 1/1 matching secret in array", async () => { + const { validateRequest } = await generateMocksWithMultipleSecrets([SECRET1]) + assert.isTrue(await validateRequest(mockRequest)) }) - it("should be valid with 1/2 matching secrets in array, first secret matches", () => { - assert.isTrue(generateMocksWithMultipleSecrets([SECRET1, SECRET2]).validateRequest(mockRequest)) + it("should be valid with 1/2 matching secrets in array, first secret matches", async () => { + const { validateRequest } = await generateMocksWithMultipleSecrets([SECRET1, SECRET2]) + assert.isTrue(await validateRequest(mockRequest)) }) - it("should be valid with 1/2 matching secrets in array, second secret matches", () => { - assert.isTrue(generateMocksWithMultipleSecrets([SECRET2, SECRET1]).validateRequest(mockRequest)) + it("should be valid with 1/2 matching secrets in array, second secret matches", async () => { + const { validateRequest } = await generateMocksWithMultipleSecrets([SECRET2, SECRET1]) + assert.isTrue(await validateRequest(mockRequest)) }) - it("should be invalid with 0/1 matching secret in array", () => { - assert.isFalse(generateMocksWithMultipleSecrets([SECRET2]).validateRequest(mockRequest)) + it("should be invalid with 0/1 matching secret in array", async () => { + const { validateRequest } = await generateMocksWithMultipleSecrets([SECRET2]) + assert.isFalse(await validateRequest(mockRequest)) }) - it("should be invalid with 0/2 matching secrets in array", () => { - assert.isFalse(generateMocksWithMultipleSecrets(SECRET2).validateRequest(mockRequest)) + it("should be invalid with 0/2 matching secrets in array", async () => { + const { validateRequest } = await generateMocksWithMultipleSecrets(SECRET2) + assert.isFalse(await validateRequest(mockRequest)) }) - it("should be invalid with 0/3 matching secrets in array", () => { - assert.isFalse( - generateMocksWithMultipleSecrets(["invalid0", "invalid1", "invalid2"]).validateRequest(mockRequest), - ) + it("should be invalid with 0/3 matching secrets in array", async () => { + const { validateRequest } = await generateMocksWithMultipleSecrets(["invalid0", "invalid1", "invalid2"]) + assert.isFalse(await validateRequest(mockRequest)) }) }) - describe("should generate tokens correctly, simulating token rotations", () => { + describe("should generate tokens correctly, simulating token rotations", async () => { const getEmptyResponse = () => { const { mockResponse } = generateMocks() return mockResponse } - const { validateRequest: validateRequestWithSecret1 } = generateMocksWithMultipleSecrets(SECRET1) + const { validateRequest: validateRequestWithSecret1 } = await generateMocksWithMultipleSecrets(SECRET1) - const { validateRequest: validateRequestWithSecret2 } = generateMocksWithMultipleSecrets(SECRET2) + const { validateRequest: validateRequestWithSecret2 } = await generateMocksWithMultipleSecrets(SECRET2) - const { generateToken: generateTokenWithSecret1And2 } = generateMocksWithMultipleSecrets([SECRET1, SECRET2]) + const { generateToken: generateTokenWithSecret1And2 } = await generateMocksWithMultipleSecrets([SECRET1, SECRET2]) - const { generateToken: generateTokenWithSecret2And1 } = generateMocksWithMultipleSecrets([SECRET2, SECRET1]) + const { generateToken: generateTokenWithSecret2And1 } = await generateMocksWithMultipleSecrets([SECRET2, SECRET1]) - it("should reuse existing token on request with SECRET1, while current is [SECRET1, SECRET2]", () => { - const { mockRequest } = generateMocksWithMultipleSecrets(SECRET1) + it("should reuse existing token on request with SECRET1, while current is [SECRET1, SECRET2]", async () => { + const { mockRequest } = await generateMocksWithMultipleSecrets(SECRET1) const mockResponse = getEmptyResponse() - const token = generateTokenWithSecret1And2(mockRequest, mockResponse) + const token = await generateTokenWithSecret1And2(mockRequest, mockResponse) attachResponseValuesToRequest({ request: mockRequest, response: mockResponse, @@ -142,15 +147,15 @@ describe("csrf-csrf token-rotation", () => { bodyResponseToken: token, }) - assert.isTrue(validateRequestWithSecret1(mockRequest)) - assert.isFalse(validateRequestWithSecret2(mockRequest)) + assert.isTrue(await validateRequestWithSecret1(mockRequest)) + assert.isFalse(await validateRequestWithSecret2(mockRequest)) }) - it("should reuse existing token on request with SECRET1, while current is [SECRET2, SECRET1]", () => { - const { mockRequest } = generateMocksWithMultipleSecrets(SECRET1) + it("should reuse existing token on request with SECRET1, while current is [SECRET2, SECRET1]", async () => { + const { mockRequest } = await generateMocksWithMultipleSecrets(SECRET1) const mockResponse = getEmptyResponse() - const token = generateTokenWithSecret2And1(mockRequest, mockResponse) + const token = await generateTokenWithSecret2And1(mockRequest, mockResponse) attachResponseValuesToRequest({ request: mockRequest, response: mockResponse, @@ -159,16 +164,16 @@ describe("csrf-csrf token-rotation", () => { bodyResponseToken: token, }) - assert.isTrue(validateRequestWithSecret1(mockRequest)) - assert.isFalse(validateRequestWithSecret2(mockRequest)) + assert.isTrue(await validateRequestWithSecret1(mockRequest)) + assert.isFalse(await validateRequestWithSecret2(mockRequest)) }) - it("should generate new token (with secret 1) on request with SECRET2, while current is [SECRET1, SECRET2], if overwrite is true", () => { - const { mockRequest } = generateMocksWithMultipleSecrets(SECRET2) + it("should generate new token (with secret 1) on request with SECRET2, while current is [SECRET1, SECRET2], if overwrite is true", async () => { + const { mockRequest } = await generateMocksWithMultipleSecrets(SECRET2) const mockResponse = getEmptyResponse() - const token = generateTokenWithSecret1And2(mockRequest, mockResponse, { + const token = await generateTokenWithSecret1And2(mockRequest, mockResponse, { overwrite: true, }) @@ -180,16 +185,16 @@ describe("csrf-csrf token-rotation", () => { bodyResponseToken: token, }) - assert.isFalse(validateRequestWithSecret2(mockRequest)) - assert.isTrue(validateRequestWithSecret1(mockRequest)) + assert.isFalse(await validateRequestWithSecret2(mockRequest)) + assert.isTrue(await validateRequestWithSecret1(mockRequest)) }) - it("should generate new token (with secret 2) on request with SECRET2, while current is [SECRET2, SECRET1], if overwrite is true", () => { - const { mockRequest } = generateMocksWithMultipleSecrets(SECRET2) + it("should generate new token (with secret 2) on request with SECRET2, while current is [SECRET2, SECRET1], if overwrite is true", async () => { + const { mockRequest } = await generateMocksWithMultipleSecrets(SECRET2) const mockResponse = getEmptyResponse() - const token = generateTokenWithSecret2And1(mockRequest, mockResponse, { + const token = await generateTokenWithSecret2And1(mockRequest, mockResponse, { overwrite: true, }) @@ -201,8 +206,8 @@ describe("csrf-csrf token-rotation", () => { bodyResponseToken: token, }) - assert.isTrue(validateRequestWithSecret2(mockRequest)) - assert.isFalse(validateRequestWithSecret1(mockRequest)) + assert.isTrue(await validateRequestWithSecret2(mockRequest)) + assert.isFalse(await validateRequestWithSecret1(mockRequest)) }) }) }) diff --git a/tests/testsuite.ts b/tests/testsuite.ts index 21f991b..3eda3dc 100644 --- a/tests/testsuite.ts +++ b/tests/testsuite.ts @@ -44,7 +44,7 @@ export const createTestSuite: CreateTestSuite = (name, doubleCsrfOptions) => { }, } = doubleCsrfOptions - const generateMocksWithTokenInternal = () => + const generateMocksWithTokenInternal = async () => generateMocksWithToken({ cookieName, generateToken, @@ -59,8 +59,8 @@ export const createTestSuite: CreateTestSuite = (name, doubleCsrfOptions) => { }) describe("generateToken", () => { - it("should attach both a token and its hash to the response and return a token", () => { - const { mockRequest, decodedCookieValue, setCookie } = generateMocksWithTokenInternal() + it("should attach both a token and its hash to the response and return a token", async () => { + const { mockRequest, decodedCookieValue, setCookie } = await generateMocksWithTokenInternal() const cookieValue = `s:${sign(decodedCookieValue as string, COOKIE_SECRET)}` const expectedSetCookieValue = serializeCookie(cookieName, cookieValue as string, { @@ -72,27 +72,27 @@ export const createTestSuite: CreateTestSuite = (name, doubleCsrfOptions) => { assert.equal(setCookie, expectedSetCookieValue) }) - it("should reuse a csrf token if a csrf cookie is already present, and overwrite is set to false", () => { - const { mockRequest, mockResponse, csrfToken, cookieValue: oldCookieValue } = generateMocksWithTokenInternal() + it("should reuse a csrf token if a csrf cookie is already present, and overwrite is set to false", async () => { + const { mockRequest, mockResponse, csrfToken, cookieValue: oldCookieValue } = await generateMocksWithTokenInternal() // reset the mock response to have no cookies (in reality this would just be a new instance of Response) mockResponse.setHeader("set-cookie", []) // overwrite is false by default - const generatedToken = generateToken(mockRequest, mockResponse) + const generatedToken = await generateToken(mockRequest, mockResponse) const newCookieValue = getCookieFromResponse(mockResponse) assert.equal(generatedToken, csrfToken) assert.equal(newCookieValue, oldCookieValue) }) - it("should generate a new token even if a csrf cookie is already present, if overwrite is set to true", () => { - const { mockRequest, mockResponse, csrfToken, cookieValue: oldCookieValue } = generateMocksWithTokenInternal() + it("should generate a new token even if a csrf cookie is already present, if overwrite is set to true", async () => { + const { mockRequest, mockResponse, csrfToken, cookieValue: oldCookieValue } = await generateMocksWithTokenInternal() // reset the mock response to have no cookies (in reality this would just be a new instance of Response) mockResponse.setHeader("set-cookie", []) - const generatedToken = generateToken(mockRequest, mockResponse, { + const generatedToken = await generateToken(mockRequest, mockResponse, { overwrite: true, }) const newCookieValue = getCookieFromResponse(mockResponse) @@ -101,38 +101,38 @@ export const createTestSuite: CreateTestSuite = (name, doubleCsrfOptions) => { assert.notEqual(generatedToken, csrfToken) }) - it("should throw if csrf cookie is present and invalid, overwrite is false, and validateOnReuse is enabled", () => { - const { mockRequest, mockResponse, decodedCookieValue } = generateMocksWithTokenInternal() + it("should throw if csrf cookie is present and invalid, overwrite is false, and validateOnReuse is enabled", async () => { + const { mockRequest, mockResponse, decodedCookieValue } = await generateMocksWithTokenInternal() // modify the cookie to make the token/hash pair invalid const cookieJar = mockRequest.cookies cookieJar[cookieName] = new Cookie(`${(decodedCookieValue as string).split("|")[0]}|invalid-hash`) - expect(() => + await expect( generateToken(mockRequest, mockResponse, { overwrite: false, validateOnReuse: true, }), - ).to.throw(invalidCsrfTokenError.message) + ).rejects.toThrow(invalidCsrfTokenError.message) // just an invalid value in the cookie cookieJar[cookieName] = new Cookie("invalid-value") - expect(() => + await expect( generateToken(mockRequest, mockResponse, { overwrite: false, validateOnReuse: true, }), - ).to.throw(invalidCsrfTokenError.message) + ).rejects.toThrow(invalidCsrfTokenError.message) }) - it("should not throw if csrf cookie is present and invalid when overwrite is false, and validateOnReuse is disabled", () => { + it("should not throw if csrf cookie is present and invalid when overwrite is false, and validateOnReuse is disabled", async () => { const { mockRequest, mockResponse, decodedCookieValue, cookieValue: oldCookieValue, csrfToken, - } = generateMocksWithTokenInternal() + } = await generateMocksWithTokenInternal() let generatedToken = "" let newCookieValue = "" @@ -142,12 +142,15 @@ export const createTestSuite: CreateTestSuite = (name, doubleCsrfOptions) => { const cookieJar = mockRequest.cookies cookieJar[cookieName] = new Cookie(`${(decodedCookieValue as string).split("|")[0]}|invalid-hash`) - assert.doesNotThrow(() => { - generatedToken = generateToken(mockRequest, mockResponse, { + async function runGenerateToken() { + generatedToken = await generateToken(mockRequest, mockResponse, { overwrite: false, - validateOnReuse: false, + validateOnReuse: false }) - }) + } + + await expect(runGenerateToken()).resolves.not.toThrow() + newCookieValue = getCookieFromResponse(mockResponse) assert.notEqual(newCookieValue, oldCookieValue) assert.notEqual(generatedToken, csrfToken) @@ -155,12 +158,7 @@ export const createTestSuite: CreateTestSuite = (name, doubleCsrfOptions) => { // just an invalid value in the cookie cookieJar[cookieName] = new Cookie("invalid-value") - assert.doesNotThrow(() => { - generatedToken = generateToken(mockRequest, mockResponse, { - overwrite: false, - validateOnReuse: false, - }) - }) + await expect(runGenerateToken()).resolves.not.toThrow() newCookieValue = getCookieFromResponse(mockResponse) assert.notEqual(newCookieValue, oldCookieValue) @@ -169,91 +167,91 @@ export const createTestSuite: CreateTestSuite = (name, doubleCsrfOptions) => { }) describe("validateRequest", () => { - it("should return false when no token has been generated", () => { + it("should return false when no token has been generated", async () => { const { mockRequest } = generateMocks() - assert.isFalse(validateRequest(mockRequest)) + assert.isFalse(await validateRequest(mockRequest)) }) - it("should return false when a token is generated but not received in request", () => { - const { mockRequest, decodedCookieValue } = generateMocksWithTokenInternal() + it("should return false when a token is generated but not received in request", async () => { + const { mockRequest, decodedCookieValue } = await generateMocksWithTokenInternal() assert.equal(getCookieFromRequest(cookieName, mockRequest), decodedCookieValue) // Wipe token mockRequest.headers = {} - assert.isFalse(validateRequest(mockRequest)) + assert.isFalse(await validateRequest(mockRequest)) }) - it("should return false when token does not match", () => { - const { mockRequest } = generateMocksWithTokenInternal() + it("should return false when token does not match", async () => { + const { mockRequest } = await generateMocksWithTokenInternal() mockRequest.headers[HEADER_KEY] = TEST_TOKEN - assert.isFalse(validateRequest(mockRequest)) + assert.isFalse(await validateRequest(mockRequest)) }) - it("should return false when cookie is not present", () => { - const { mockRequest } = generateMocksWithTokenInternal() + it("should return false when cookie is not present", async () => { + const { mockRequest } = await generateMocksWithTokenInternal() // Wipe hash delete mockRequest.cookies[cookieName] - assert.isFalse(validateRequest(mockRequest)) + assert.isFalse(await validateRequest(mockRequest)) }) }) - describe("doubleCsrfProtection", () => { - const assertProtectionToThrow = (request: CSRFRequest, response: CSRFResponse) => { - expect(() => doubleCsrfProtection(request, response, next)).to.throw(invalidCsrfTokenError.message) + describe("doubleCsrfProtection", async () => { + const assertProtectionToThrow = async (request: CSRFRequest, response: CSRFResponse) => { + await expect(doubleCsrfProtection(request, response, next)).rejects.toThrow(invalidCsrfTokenError.message) } - const assertProtectionToNotThrow = (request: CSRFRequest, response: CSRFResponse) => { - expect(() => doubleCsrfProtection(request, response, next)).to.not.throw() + const assertProtectionToNotThrow = async (request: CSRFRequest, response: CSRFResponse) => { + await expect(doubleCsrfProtection(request, response, next)).resolves.not.toThrow() } - it("should allow requests with an ignored method", () => { + it("should allow requests with an ignored method", async () => { const { mockRequest, mockResponse } = generateMocks() mockRequest.method = "GET" - expect(() => doubleCsrfProtection(mockRequest, mockResponse, next)).to.not.throw() + await assertProtectionToNotThrow(mockRequest, mockResponse) // Show an invalid case - const { mockResponse: mockResponseWithToken } = generateMocksWithToken({ + const { mockResponse: mockResponseWithToken } = await generateMocksWithToken({ cookieName, generateToken, validateRequest, }) mockRequest.method = "POST" - assertProtectionToThrow(mockRequest, mockResponseWithToken) + await assertProtectionToThrow(mockRequest, mockResponseWithToken) // Works as get mockRequest.method = "GET" - assertProtectionToNotThrow(mockRequest, mockResponseWithToken) + await assertProtectionToNotThrow(mockRequest, mockResponseWithToken) }) - it("should allow a valid request", () => { - const { mockResponse, mockRequest } = generateMocksWithTokenInternal() - assertProtectionToNotThrow(mockRequest, mockResponse) + it("should allow a valid request", async () => { + const { mockResponse, mockRequest } = await generateMocksWithTokenInternal() + await assertProtectionToNotThrow(mockRequest, mockResponse) }) - it("should not allow request after secret rotation", () => { - const { mockResponse, mockRequest } = generateMocksWithTokenInternal() - assertProtectionToNotThrow(mockRequest, mockResponse) + it("should not allow request after secret rotation", async () => { + const { mockResponse, mockRequest } = await generateMocksWithTokenInternal() + await assertProtectionToNotThrow(mockRequest, mockResponse) switchSecret() - assertProtectionToThrow(mockRequest, mockResponse) + await assertProtectionToThrow(mockRequest, mockResponse) }) - it("should not allow a protected request with no cookie", () => { - const { mockResponse, mockRequest } = generateMocksWithTokenInternal() + it("should not allow a protected request with no cookie", async () => { + const { mockResponse, mockRequest } = await generateMocksWithTokenInternal() delete mockRequest.cookies[cookieName] - assertProtectionToThrow(mockRequest, mockResponse) + await assertProtectionToThrow(mockRequest, mockResponse) }) - it("should not allow a protected request with no token", () => { - const { mockResponse, mockRequest } = generateMocksWithTokenInternal() + it("should not allow a protected request with no token", async () => { + const { mockResponse, mockRequest } = await generateMocksWithTokenInternal() delete mockRequest.headers[HEADER_KEY] assert.isUndefined(mockRequest.headers[HEADER_KEY]) - assertProtectionToThrow(mockRequest, mockResponse) + await assertProtectionToThrow(mockRequest, mockResponse) }) - it("should not allow a protected request with a mismatching token and cookie", () => { - const { mockResponse, mockRequest } = generateMocksWithTokenInternal() - assertProtectionToNotThrow(mockRequest, mockResponse) + it("should not allow a protected request with a mismatching token and cookie", async () => { + const { mockResponse, mockRequest } = await generateMocksWithTokenInternal() + await assertProtectionToNotThrow(mockRequest, mockResponse) mockRequest.headers[HEADER_KEY] = TEST_TOKEN - assertProtectionToThrow(mockRequest, mockResponse) + await assertProtectionToThrow(mockRequest, mockResponse) }) }) }) diff --git a/tests/utils/mock.ts b/tests/utils/mock.ts index 841a43d..eeb7893 100644 --- a/tests/utils/mock.ts +++ b/tests/utils/mock.ts @@ -60,14 +60,14 @@ export type GenerateMocksWithTokenOptions = { // Generate the request and response mocks. // Set them up as if they have been pre-processed in a valid state. -export const generateMocksWithToken = ({ +export const generateMocksWithToken = async ({ cookieName, generateToken, validateRequest, }: GenerateMocksWithTokenOptions) => { const { mockRequest, mockResponse } = generateMocks() - const csrfToken = generateToken(mockRequest, mockResponse) + const csrfToken = await generateToken(mockRequest, mockResponse) const { setCookie, cookieValue } = getCookieValueFromResponse(mockResponse) mockRequest.headers.cookie = `${cookieName}=${cookieValue};` @@ -79,7 +79,7 @@ export const generateMocksWithToken = ({ mockRequest.headers[HEADER_KEY] = csrfToken // Once a token has been generated, the request should be setup as valid - assert.isTrue(validateRequest(mockRequest)) + assert.isTrue(await validateRequest(mockRequest)) return { csrfToken, cookieValue,