-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: support optional stateless association of token with session
Added the getSessionIdentifier parameter to the csrf-csrf configuration. By providing the getSessionIdentifier callback, generated tokens will only be valid for the original session identifier they were generated for. For example: (req) => req.session.id The token will now be signed with the session id included, this means a generated CSRF token will only be valid for the session it was generated for. This also means that if you rotate your sessions (which you should) you will also need to generate a new CSRF token for the session after rotating it.
- Loading branch information
Showing
6 changed files
with
210 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ | ||
import { assert, expect } from "chai"; | ||
import { doubleCsrf } from "../index.js"; | ||
import { | ||
generateMocksWithToken, | ||
next, | ||
RequestWithSessionId, | ||
} from "./utils/mock.js"; | ||
import { | ||
getSingleSecret, | ||
attachResponseValuesToRequest, | ||
} from "./utils/helpers.js"; | ||
|
||
describe("csrf-csrf with getSessionIdentifier", () => { | ||
const cookieName = "xsrf-protection"; | ||
const sessionIdentifier = "asdf68236tr3g34fgds9fgsd9g23grb3"; | ||
|
||
const { | ||
invalidCsrfTokenError, | ||
generateToken, | ||
validateRequest, | ||
doubleCsrfProtection, | ||
} = doubleCsrf({ | ||
cookieName, | ||
getSecret: getSingleSecret, | ||
getSessionIdentifier: (req) => | ||
(req as RequestWithSessionId).session.id ?? "", | ||
}); | ||
|
||
it("should have a valid CSRF token for the session it was generated for", () => { | ||
const { mockRequest, mockResponse } = generateMocksWithToken({ | ||
cookieName, | ||
generateToken, | ||
validateRequest, | ||
signed: false, | ||
sessionIdentifier, | ||
}); | ||
|
||
expect(() => { | ||
doubleCsrfProtection(mockRequest, mockResponse, next); | ||
}, "CSRF protection should be valid").not.to.throw(invalidCsrfTokenError); | ||
}); | ||
|
||
it("should not be a valid CSRF token for a session it was not generated for", () => { | ||
const { mockRequest, mockResponse } = generateMocksWithToken({ | ||
cookieName, | ||
generateToken, | ||
validateRequest, | ||
signed: false, | ||
sessionIdentifier, | ||
}); | ||
|
||
(mockRequest as RequestWithSessionId).session.id = "sdf9342dfa245r13tgvrf"; | ||
|
||
expect(() => { | ||
doubleCsrfProtection(mockRequest, mockResponse, next); | ||
}, "CSRF protection should be invalid").to.throw(invalidCsrfTokenError); | ||
}); | ||
|
||
it("should throw when validateOnReuse is true and session has been rotated", () => { | ||
const { mockRequest, mockResponse } = generateMocksWithToken({ | ||
cookieName, | ||
generateToken, | ||
validateRequest, | ||
signed: false, | ||
sessionIdentifier, | ||
}); | ||
|
||
(mockRequest as RequestWithSessionId).session.id = "sdf9342dfa245r13tgvrf"; | ||
|
||
assert.isFalse(validateRequest(mockRequest)); | ||
expect(() => | ||
generateToken(mockRequest, mockResponse, { | ||
overwrite: false, | ||
validateOnReuse: true, | ||
}), | ||
).to.throw(invalidCsrfTokenError); | ||
}); | ||
|
||
it("should generate a new valid token after session has been rotated", () => { | ||
const { csrfToken, mockRequest, mockResponse } = generateMocksWithToken({ | ||
cookieName, | ||
generateToken, | ||
validateRequest, | ||
signed: false, | ||
sessionIdentifier, | ||
}); | ||
|
||
(mockRequest as RequestWithSessionId).session.id = "sdf9342dfa245r13tgvrf"; | ||
console.log("generating a new token"); | ||
const newCsrfToken = generateToken(mockRequest, mockResponse, { | ||
overwrite: true, | ||
}); | ||
console.log("new token generated"); | ||
assert.notEqual( | ||
newCsrfToken, | ||
csrfToken, | ||
"New token and original token should not match", | ||
); | ||
attachResponseValuesToRequest({ | ||
request: mockRequest, | ||
response: mockResponse, | ||
bodyResponseToken: newCsrfToken, | ||
cookieName, | ||
}); | ||
assert.isTrue(validateRequest(mockRequest)); | ||
expect(() => | ||
doubleCsrfProtection(mockRequest, mockResponse, next), | ||
).not.to.throw(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters