├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── eslint.config.js ├── examples └── example.mjs ├── index.js ├── package.json ├── test ├── basic.test.js └── user-info.test.js └── types ├── index.d.ts └── index.test-d.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behavior to automatically convert line endings 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: "npm" 10 | directory: "/" 11 | schedule: 12 | interval: "monthly" 13 | open-pull-requests-limit: 10 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | - 'v*' 9 | paths-ignore: 10 | - 'docs/**' 11 | - '*.md' 12 | pull_request: 13 | paths-ignore: 14 | - 'docs/**' 15 | - '*.md' 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | test: 22 | permissions: 23 | contents: write 24 | pull-requests: write 25 | uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5 26 | with: 27 | license-check: true 28 | lint: true 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # Vim swap files 133 | *.swp 134 | 135 | # macOS files 136 | .DS_Store 137 | 138 | # lock files 139 | bun.lockb 140 | package-lock.json 141 | pnpm-lock.yaml 142 | yarn.lock 143 | 144 | # editor files 145 | .vscode 146 | .idea 147 | 148 | #tap files 149 | .tap/ 150 | 151 | # Optional compressed files (npm generated package, zip, etc) 152 | /*.zip 153 | 154 | # 0x 155 | profile-* 156 | 157 | # clinic 158 | profile* 159 | *clinic* 160 | *flamegraph* 161 | 162 | # generated code 163 | examples/typescript-server.js 164 | test/types/index.js 165 | 166 | # test tap report 167 | out.tap 168 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Tarang11 4 | Copyright (c) 2020 The Fastify Team 5 | 6 | The Fastify team members are listed at https://github.com/fastify/fastify#team 7 | and in the README file. 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @fastify/csrf-protection 2 | 3 | [](https://github.com/fastify/csrf-protection/actions/workflows/ci.yml) 4 | [](https://www.npmjs.com/package/@fastify/csrf-protection) 5 | [](https://github.com/neostandard/neostandard) 6 | 7 | This plugin helps developers protect their Fastify server against [CSRF](https://en.wikipedia.org/wiki/Cross-site_request_forgery) attacks. 8 | In order to fully protect against CSRF, developers should study [Cross-Site Request Forgery Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html) 9 | in depth. See also [pillarjs/understanding-csrf](https://github.com/pillarjs/understanding-csrf) as a good guide. 10 | 11 | ## Security Disclaimer 12 | 13 | Securing applications against CSRF is a _developer's responsibility_ and it should not be fully trusted to any third-party modules. 14 | We do not claim that this module is able to protect an application without a clear study of CSRF, its impact, and the needed mitigations. 15 | @fastify/csrf-protection provides a series of utilities that developers can use to secure their application. 16 | We recommend using [@fastify/helmet](https://github.com/fastify/fastify-helmet) to implement some of those mitigations. 17 | 18 | Security is always a tradeoff between risk mitigation, functionality, performance, and developer experience. 19 | As a result, we will not consider a report of a plugin default configuration option as a security 20 | vulnerability that might be unsafe in certain scenarios as long as this module provides a 21 | way to provide full mitigation through configuration. 22 | 23 | ## Install 24 | ```js 25 | npm i @fastify/csrf-protection 26 | ``` 27 | 28 | ### Compatibility 29 | | Plugin version | Fastify version | 30 | | ---------------|-----------------| 31 | | `>=7.x` | `^5.x` | 32 | | `>=4.x <7.x` | `^4.x` | 33 | | `^3.x` | `^3.x` | 34 | 35 | 36 | Please note that if a Fastify version is out of support, then so are the corresponding versions of this plugin 37 | in the table above. 38 | See [Fastify's LTS policy](https://github.com/fastify/fastify/blob/main/docs/Reference/LTS.md) for more details. 39 | 40 | ## Usage 41 | 42 | 43 | ### Use with [`@fastify/cookie`](https://github.com/fastify/fastify-cookie) 44 | 45 | If you use `@fastify/csrf-protection` with `@fastify/cookie`, the CSRF secret will be added to the response cookies. 46 | By default, the cookie used will be named `_csrf`, but you can rename it via the `cookieKey` option. 47 | When `cookieOpts` are provided, they **override** the default cookie options. Make sure you restore any of the default options which provide sensible and secure defaults. 48 | 49 | ```js 50 | fastify.register(require('@fastify/cookie')) 51 | fastify.register(require('@fastify/csrf-protection')) 52 | 53 | // if you want to sign cookies: 54 | fastify.register(require('@fastify/cookie'), { secret }) // See following section to ensure security 55 | fastify.register(require('@fastify/csrf-protection'), { cookieOpts: { signed: true } }) 56 | 57 | // generate a token 58 | fastify.route({ 59 | method: 'GET', 60 | path: '/', 61 | handler: async (req, reply) => { 62 | const token = reply.generateCsrf() 63 | return { token } 64 | } 65 | }) 66 | 67 | // protect a route 68 | fastify.route({ 69 | method: 'POST', 70 | path: '/', 71 | onRequest: fastify.csrfProtection, 72 | handler: async (req, reply) => { 73 | return req.body 74 | } 75 | }) 76 | ``` 77 | 78 | ### Use with [`@fastify/session`](https://github.com/fastify/session) 79 | 80 | If you use `@fastify/csrf-protection` with `@fastify/session`, the CSRF secret will be added to the session. 81 | By default, the key used will be named `_csrf`, but you can rename it via the `sessionKey` option. 82 | 83 | ```js 84 | fastify.register(require('@fastify-session'), { secret: "a string which is longer than 32 characters" }) 85 | fastify.register(require('@fastify/csrf-protection'), { sessionPlugin: '@fastify/session' }) 86 | 87 | // generate a token 88 | fastify.route({ 89 | method: 'GET', 90 | path: '/', 91 | handler: async (req, reply) => { 92 | const token = reply.generateCsrf() 93 | return { token } 94 | } 95 | }) 96 | 97 | // protect a route 98 | fastify.route({ 99 | method: 'POST', 100 | path: '/', 101 | onRequest: fastify.csrfProtection, 102 | handler: async (req, reply) => { 103 | return req.body 104 | } 105 | }) 106 | ``` 107 | 108 | ### Use with [`@fastify/secure-session`](https://github.com/fastify/fastify-secure-session) 109 | 110 | If you use `@fastify/csrf-protection` with `@fastify/secure-session`, the CSRF secret will be added to the session. 111 | By default, the key used will be named `_csrf`, but you can rename it via the `sessionKey` option. 112 | 113 | ```js 114 | fastify.register(require('@fastify/secure-session'), { secret: "a string which is longer than 32 characters" }) 115 | fastify.register(require('@fastify/csrf-protection'), { sessionPlugin: '@fastify/secure-session' }) 116 | 117 | // generate a token 118 | fastify.route({ 119 | method: 'GET', 120 | path: '/', 121 | handler: async (req, reply) => { 122 | const token = reply.generateCsrf() 123 | return { token } 124 | } 125 | }) 126 | 127 | // protect a route 128 | fastify.route({ 129 | method: 'POST', 130 | path: '/', 131 | onRequest: fastify.csrfProtection, 132 | handler: async (req, reply) => { 133 | return req.body 134 | } 135 | }) 136 | ``` 137 | 138 | ### Securing the secret 139 | 140 | The `secret` shown in the code above is strictly just an example. In all cases, you would need to make sure that the `secret` is: 141 | - **Never** hard-coded in the code or `.env` files or anywhere in the repository 142 | - Stored in some external services like KMS, Vault, or something similar 143 | - Read at run-time and supplied to this option 144 | - Of significant character length to provide adequate entropy 145 | - A truly random sequence of characters (You could use [crypto-random-string](https://npm.im/crypto-random-string)) 146 | 147 | Apart from these safeguards, it is extremely important to [use HTTPS for your website/app](https://letsencrypt.org/) to avoid a bunch of other potential security issues like [MITM attacks](https://en.wikipedia.org/wiki/Man-in-the-middle_attack) etc. 148 | 149 | ## API 150 | 151 | ### Module Options 152 | 153 | | Options | Description | 154 | | ----------- | ----------- | 155 | | `cookieKey` | The name of the cookie where the CSRF secret will be stored, default `_csrf`. | 156 | | `cookieOpts` | The cookie serialization options. See [@fastify/cookie](https://github.com/fastify/fastify-cookie). | 157 | | `sessionKey` | The key where to store the CSRF secret in the session. | 158 | | `getToken` | A sync function to get the CSRF secret from the request. | 159 | | `getUserInfo` | A sync function to get a string of user-specific information to prevent cookie tossing. | 160 | | `sessionPlugin` | The session plugin that you are using (if applicable). | 161 | | `csrfOpts` | The csrf options. See [@fastify/csrf](https://github.com/fastify/csrf). | 162 | | `logLevel` | The log level for `fastify.csrfProtection` errors. | 163 | 164 | ### `reply.generateCsrf([opts])` 165 | 166 | Generates a secret (if it is not already present) and returns a promise that resolves to the associated secret. 167 | 168 | ```js 169 | const token = reply.generateCsrf() 170 | ``` 171 | 172 | You can also pass the [cookie serialization](https://github.com/fastify/fastify-cookie) options to the function. 173 | 174 | The option `userInfo` is required if `getUserInfo` has been specified in the module option. 175 | The provided `userInfo` is hashed inside the csrf token and it is not directly exposed. 176 | This option is needed to protect against cookie tossing. 177 | The option `csrfOpts.hmacKey` is required if `getUserInfo` has been specified in the module option in combination with using [@fastify/cookie](https://github.com/fastify/fastify-cookie) as sessionPlugin. 178 | 179 | ### `fastify.csrfProtection(request, reply, next)` 180 | 181 | A hook that you can use for protecting routes or entire plugins from CSRF attacks. 182 | Generally, we recommend using an `onRequest` hook, but if you are sending the token 183 | via the request body, then you must use a `preValidation` or `preHandler` hook. 184 | 185 | ```js 186 | // protect the fastify instance 187 | fastify.addHook('onRequest', fastify.csrfProtection) 188 | 189 | // protect a single route 190 | fastify.route({ 191 | method: 'POST', 192 | path: '/', 193 | onRequest: fastify.csrfProtection, 194 | handler: async (req, reply) => { 195 | return req.body 196 | } 197 | }) 198 | ``` 199 | 200 | You can configure the function to read the CSRF token via the `getToken` option, by default the following is used: 201 | 202 | ```js 203 | function getToken (req) { 204 | return (req.body && req.body._csrf) || 205 | req.headers['csrf-token'] || 206 | req.headers['xsrf-token'] || 207 | req.headers['x-csrf-token'] || 208 | req.headers['x-xsrf-token'] 209 | } 210 | ``` 211 | 212 | It is recommended to provide a custom `getToken` function for performance and [security](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#use-of-custom-request-headers) reasons. 213 | 214 | ```js 215 | fastify.register(require('@fastify/csrf-protection'), 216 | { getToken: function (req) { return req.headers['csrf-token'] } } 217 | ) 218 | ``` 219 | or 220 | 221 | ```js 222 | fastify.register(require('@fastify/csrf-protection'), 223 | { getToken: (req) => req.headers['csrf-token'] } 224 | ) 225 | ``` 226 | 227 | ## License 228 | 229 | Licensed under [MIT](./LICENSE). 230 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('neostandard')({ 4 | ignores: require('neostandard').resolveIgnoresFromGitignore(), 5 | ts: true 6 | }) 7 | -------------------------------------------------------------------------------- /examples/example.mjs: -------------------------------------------------------------------------------- 1 | import Fastify from 'fastify' 2 | import fastifyCookie from '@fastify/cookie' 3 | import fastifyCsrfProtection from '../index.js' 4 | 5 | const fastify = Fastify({ 6 | logger: true 7 | }) 8 | 9 | fastify.register(fastifyCookie) 10 | await fastify.register(fastifyCsrfProtection) 11 | 12 | fastify.post( 13 | '/', 14 | { 15 | preHandler: fastify.csrfProtection 16 | }, 17 | async (req) => { 18 | return req.body 19 | } 20 | ) 21 | 22 | // generate a token 23 | fastify.route({ 24 | method: 'GET', 25 | url: '/', 26 | handler: async (_req, reply) => { 27 | const token = reply.generateCsrf() 28 | reply.type('text/html') 29 | 30 | return ` 31 | 32 | 48 |
49 | 52 | 53 | 54 | 55 | ` 56 | } 57 | }) 58 | 59 | // Run the server! 60 | fastify.listen({ port: 3001 }, function (err) { 61 | if (err) { 62 | fastify.log.error(err) 63 | process.exit(1) 64 | } 65 | }) 66 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const assert = require('node:assert') 4 | const fp = require('fastify-plugin') 5 | const CSRF = require('@fastify/csrf') 6 | const createError = require('@fastify/error') 7 | 8 | const MissingCSRFSecretError = createError('FST_CSRF_MISSING_SECRET', 'Missing csrf secret', 403) 9 | const InvalidCSRFTokenError = createError('FST_CSRF_INVALID_TOKEN', 'Invalid csrf token', 403) 10 | 11 | const defaultOptions = { 12 | cookieKey: '_csrf', 13 | cookieOpts: { path: '/', sameSite: true, httpOnly: true }, 14 | sessionKey: '_csrf', 15 | getToken: getTokenDefault, 16 | getUserInfo: getUserInfoDefault, 17 | sessionPlugin: '@fastify/cookie', 18 | logLevel: 'warn' 19 | } 20 | 21 | async function fastifyCsrfProtection (fastify, opts) { 22 | const { 23 | cookieKey, 24 | cookieOpts, 25 | sessionKey, 26 | getToken, 27 | getUserInfo, 28 | sessionPlugin, 29 | logLevel 30 | } = Object.assign({}, defaultOptions, opts) 31 | 32 | const csrfOpts = opts?.csrfOpts ? opts.csrfOpts : {} 33 | 34 | assert(typeof cookieKey === 'string', 'cookieKey should be a string') 35 | assert(typeof sessionKey === 'string', 'sessionKey should be a string') 36 | assert(typeof getToken === 'function', 'getToken should be a function') 37 | assert(typeof getUserInfo === 'function', 'getUserInfo should be a function') 38 | assert(typeof cookieOpts === 'object', 'cookieOpts should be a object') 39 | assert(typeof logLevel === 'string', 'logLevel should be a string') 40 | assert( 41 | ['@fastify/cookie', '@fastify/session', '@fastify/secure-session'].includes(sessionPlugin), 42 | "sessionPlugin should be one of the following: '@fastify/cookie', '@fastify/session', '@fastify/secure-session'" 43 | ) 44 | 45 | if (opts.getUserInfo) { 46 | csrfOpts.userInfo = true 47 | } 48 | 49 | if (sessionPlugin === '@fastify/cookie' && csrfOpts.userInfo) { 50 | assert(csrfOpts.hmacKey, 'csrfOpts.hmacKey is required') 51 | } 52 | 53 | const tokens = new CSRF(csrfOpts) 54 | 55 | const isCookieSigned = cookieOpts?.signed 56 | 57 | if (sessionPlugin === '@fastify/secure-session') { 58 | fastify.decorateReply('generateCsrf', generateCsrfSecureSession) 59 | } else if (sessionPlugin === '@fastify/session') { 60 | fastify.decorateReply('generateCsrf', generateCsrfSession) 61 | } else { 62 | fastify.decorateReply('generateCsrf', generateCsrfCookie) 63 | } 64 | 65 | fastify.decorate('csrfProtection', csrfProtection) 66 | 67 | let getSecret 68 | 69 | if (sessionPlugin === '@fastify/secure-session') { 70 | getSecret = function getSecret (req, _reply) { return req.session.get(sessionKey) } 71 | } else if (sessionPlugin === '@fastify/session') { 72 | getSecret = function getSecret (req, _reply) { return req.session[sessionKey] } 73 | } else { 74 | getSecret = function getSecret (req, reply) { 75 | return isCookieSigned 76 | ? reply.unsignCookie(req.cookies[cookieKey] || '').value 77 | : req.cookies[cookieKey] 78 | } 79 | } 80 | 81 | function generateCsrfCookie (opts) { 82 | let secret = isCookieSigned 83 | ? this.unsignCookie(this.request.cookies[cookieKey] || '').value 84 | : this.request.cookies[cookieKey] 85 | const userInfo = opts ? opts.userInfo : undefined 86 | if (!secret) { 87 | secret = tokens.secretSync() 88 | this.setCookie(cookieKey, secret, Object.assign({}, cookieOpts, opts)) 89 | } 90 | return tokens.create(secret, userInfo) 91 | } 92 | 93 | function generateCsrfSecureSession (opts) { 94 | let secret = this.request.session.get(sessionKey) 95 | if (!secret) { 96 | secret = tokens.secretSync() 97 | this.request.session.set(sessionKey, secret) 98 | } 99 | const userInfo = opts ? opts.userInfo : undefined 100 | if (opts) { 101 | this.request.session.options(opts) 102 | } 103 | return tokens.create(secret, userInfo) 104 | } 105 | 106 | function generateCsrfSession (opts) { 107 | let secret = this.request.session[sessionKey] 108 | const userInfo = opts ? opts.userInfo : undefined 109 | if (!secret) { 110 | secret = tokens.secretSync() 111 | this.request.session[sessionKey] = secret 112 | } 113 | return tokens.create(secret, userInfo) 114 | } 115 | 116 | function csrfProtection (req, reply, next) { 117 | const secret = getSecret(req, reply) 118 | if (!secret) { 119 | req.log[logLevel]('Missing csrf secret') 120 | return reply.send(new MissingCSRFSecretError()) 121 | } 122 | if (!tokens.verify(secret, getToken(req), getUserInfo(req))) { 123 | req.log[logLevel]('Invalid csrf token') 124 | return reply.send(new InvalidCSRFTokenError()) 125 | } 126 | next() 127 | } 128 | } 129 | 130 | function getTokenDefault (req) { 131 | return req.body?._csrf || 132 | req.headers['csrf-token'] || 133 | req.headers['xsrf-token'] || 134 | req.headers['x-csrf-token'] || 135 | req.headers['x-xsrf-token'] 136 | } 137 | 138 | function getUserInfoDefault (_req) { 139 | return undefined 140 | } 141 | 142 | module.exports = fp(fastifyCsrfProtection, { 143 | fastify: '5.x', 144 | name: '@fastify/csrf-protection' 145 | }) 146 | module.exports.default = fastifyCsrfProtection 147 | module.exports.fastifyCsrfProtection = fastifyCsrfProtection 148 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fastify/csrf-protection", 3 | "version": "7.1.0", 4 | "description": "A plugin for adding CSRF protection to Fastify.", 5 | "main": "index.js", 6 | "type": "commonjs", 7 | "types": "types/index.d.ts", 8 | "scripts": { 9 | "lint": "eslint", 10 | "lint:fix": "eslint --fix", 11 | "test": "npm run test:unit", 12 | "test:unit": "c8 --100 node --test", 13 | "test:typescript": "tsd" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/fastify/fastify-csrf.git" 18 | }, 19 | "author": "Tarang11", 20 | "contributors": [ 21 | { 22 | "name": "Matteo Collina", 23 | "email": "hello@matteocollina.com" 24 | }, 25 | { 26 | "name": "Tomas Della Vedova", 27 | "url": "http://delved.org" 28 | }, 29 | { 30 | "name": "Aras Abbasi", 31 | "email": "aras.abbasi@gmail.com" 32 | }, 33 | { 34 | "name": "Frazer Smith", 35 | "email": "frazer.dev@icloud.com", 36 | "url": "https://github.com/fdawgs" 37 | } 38 | ], 39 | "license": "MIT", 40 | "bugs": { 41 | "url": "https://github.com/fastify/fastify-csrf/issues" 42 | }, 43 | "homepage": "https://github.com/fastify/fastify-csrf#readme", 44 | "funding": [ 45 | { 46 | "type": "github", 47 | "url": "https://github.com/sponsors/fastify" 48 | }, 49 | { 50 | "type": "opencollective", 51 | "url": "https://opencollective.com/fastify" 52 | } 53 | ], 54 | "dependencies": { 55 | "@fastify/csrf": "^8.0.0", 56 | "@fastify/error": "^4.0.0", 57 | "fastify-plugin": "^5.0.0" 58 | }, 59 | "devDependencies": { 60 | "@fastify/cookie": "^11.0.1", 61 | "@fastify/pre-commit": "^2.1.0", 62 | "@fastify/secure-session": "^8.0.0", 63 | "@fastify/session": "^11.0.0", 64 | "@types/node": "^22.0.0", 65 | "c8": "^10.1.3", 66 | "eslint": "^9.17.0", 67 | "fastify": "^5.0.0", 68 | "neostandard": "^0.12.0", 69 | "proxyquire": "^2.1.3", 70 | "sinon": "^20.0.0", 71 | "tsd": "^0.32.0" 72 | }, 73 | "pre-commit": [ 74 | "lint", 75 | "test" 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /test/basic.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const fastifyCookie = require('@fastify/cookie') 6 | const fastifySession = require('@fastify/session') 7 | const fastifySecureSession = require('@fastify/secure-session') 8 | const proxyquire = require('proxyquire') 9 | const sinon = require('sinon') 10 | const fastifyCsrf = require('../') 11 | 12 | const sodium = require('sodium-native') 13 | const key = Buffer.alloc(sodium.crypto_secretbox_KEYBYTES) 14 | sodium.randombytes_buf(key) 15 | 16 | test('Cookies', async t => { 17 | async function load () { 18 | const fastify = Fastify() 19 | await fastify.register(fastifyCookie) 20 | await fastify.register(fastifyCsrf) 21 | fastify.decorate('testType', 'fastify-cookie') 22 | return fastify 23 | } 24 | await runtTest(t, load, { property: '_csrf', place: 'body' }, 'preValidation') 25 | await runtTest(t, load, { property: 'csrf-token', place: 'headers' }) 26 | await runtTest(t, load, { property: 'xsrf-token', place: 'headers' }) 27 | await runtTest(t, load, { property: 'x-csrf-token', place: 'headers' }) 28 | await runtTest(t, load, { property: 'x-xsrf-token', place: 'headers' }) 29 | await runCookieOpts(t, load) 30 | 31 | await t.test('Default cookie options', async t => { 32 | const fastify = await load() 33 | 34 | fastify.get('/', async (_req, reply) => { 35 | const token = reply.generateCsrf() 36 | return { token } 37 | }) 38 | 39 | const response = await fastify.inject({ 40 | method: 'GET', 41 | path: '/' 42 | }) 43 | 44 | const cookie = response.cookies[0] 45 | t.assert.deepStrictEqual({ path: cookie.path, sameSite: cookie.sameSite, httpOnly: cookie.httpOnly }, { path: '/', sameSite: 'Strict', httpOnly: true }) 46 | }) 47 | }) 48 | 49 | test('Cookies signed', async t => { 50 | async function load () { 51 | const fastify = Fastify() 52 | await fastify.register(fastifyCookie, { secret: 'supersecret' }) 53 | await fastify.register(fastifyCsrf, { cookieOpts: { signed: true } }) 54 | fastify.decorate('testType', 'fastify-cookie') 55 | return fastify 56 | } 57 | await runtTest(t, load, { property: '_csrf', place: 'body' }, 'preValidation') 58 | await runtTest(t, load, { property: 'csrf-token', place: 'headers' }) 59 | await runtTest(t, load, { property: 'xsrf-token', place: 'headers' }) 60 | await runtTest(t, load, { property: 'x-csrf-token', place: 'headers' }) 61 | await runtTest(t, load, { property: 'x-xsrf-token', place: 'headers' }) 62 | await runCookieOpts(t, load) 63 | }) 64 | 65 | test('Fastify Session', async t => { 66 | async function load () { 67 | const fastify = Fastify() 68 | await fastify.register(fastifyCookie) 69 | await fastify.register(fastifySession, { 70 | secret: 'a'.repeat(32), 71 | cookie: { path: '/', secure: false } 72 | }) 73 | await fastify.register(fastifyCsrf, { sessionPlugin: '@fastify/session' }) 74 | fastify.decorate('testType', 'fastify-session') 75 | return fastify 76 | } 77 | await runtTest(t, load, { property: '_csrf', place: 'body' }, 'preValidation') 78 | await runtTest(t, load, { property: 'csrf-token', place: 'headers' }, 'preValidation') 79 | await runtTest(t, load, { property: 'xsrf-token', place: 'headers' }, 'preValidation') 80 | await runtTest(t, load, { property: 'x-csrf-token', place: 'headers' }, 'preValidation') 81 | await runtTest(t, load, { property: 'x-xsrf-token', place: 'headers' }, 'preValidation') 82 | }) 83 | 84 | test('Fastify Secure Session', async t => { 85 | async function load () { 86 | const fastify = Fastify() 87 | await fastify.register(fastifySecureSession, { key, cookie: { path: '/', secure: false } }) 88 | await fastify.register(fastifyCsrf, { sessionPlugin: '@fastify/secure-session' }) 89 | fastify.decorate('testType', 'fastify-secure-session') 90 | return fastify 91 | } 92 | await runtTest(t, load, { property: '_csrf', place: 'body' }, 'preValidation') 93 | await runtTest(t, load, { property: 'csrf-token', place: 'headers' }) 94 | await runtTest(t, load, { property: 'xsrf-token', place: 'headers' }) 95 | await runtTest(t, load, { property: 'x-csrf-token', place: 'headers' }) 96 | await runtTest(t, load, { property: 'x-xsrf-token', place: 'headers' }) 97 | await runCookieOpts(t, load) 98 | }) 99 | 100 | test('Validation', async t => { 101 | await t.test('cookieKey', async t => { 102 | t.plan(1) 103 | try { 104 | const fastify = Fastify() 105 | await fastify.register(fastifyCookie) 106 | await fastify.register(fastifyCsrf, { cookieKey: 42 }) 107 | await fastify.ready() 108 | } catch (err) { 109 | t.assert.strictEqual(err.message, 'cookieKey should be a string') 110 | } 111 | }) 112 | 113 | await t.test('sessionKey', async t => { 114 | t.plan(1) 115 | const fastify = Fastify() 116 | try { 117 | await fastify.register(fastifyCookie) 118 | await fastify.register(fastifyCsrf, { sessionKey: 42 }) 119 | await fastify.ready() 120 | } catch (err) { 121 | t.assert.strictEqual(err.message, 'sessionKey should be a string') 122 | } 123 | }) 124 | 125 | await t.test('getToken', async t => { 126 | t.plan(1) 127 | try { 128 | const fastify = Fastify() 129 | await fastify.register(fastifyCookie) 130 | await fastify.register(fastifyCsrf, { getToken: 42 }) 131 | await fastify.ready() 132 | } catch (err) { 133 | t.assert.strictEqual(err.message, 'getToken should be a function') 134 | } 135 | }) 136 | 137 | await t.test('cookieOpts', async t => { 138 | t.plan(1) 139 | try { 140 | const fastify = Fastify() 141 | await fastify.register(fastifyCookie) 142 | await fastify.register(fastifyCsrf, { cookieOpts: 42 }) 143 | await fastify.ready() 144 | } catch (err) { 145 | t.assert.strictEqual(err.message, 'cookieOpts should be a object') 146 | } 147 | }) 148 | 149 | await t.test('sessionPlugin', async t => { 150 | t.plan(1) 151 | try { 152 | const fastify = Fastify() 153 | await fastify.register(fastifyCookie) 154 | await fastify.register(fastifyCsrf, { sessionPlugin: 42 }) 155 | await fastify.ready() 156 | } catch (err) { 157 | t.assert.strictEqual(err.message, "sessionPlugin should be one of the following: '@fastify/cookie', '@fastify/session', '@fastify/secure-session'") 158 | } 159 | }) 160 | 161 | await t.test('logLevel', async t => { 162 | t.plan(1) 163 | try { 164 | const fastify = Fastify() 165 | await fastify.register(fastifyCookie) 166 | await fastify.register(fastifyCsrf, { logLevel: undefined }) 167 | await fastify.ready() 168 | } catch (err) { 169 | t.assert.strictEqual(err.message, 'logLevel should be a string') 170 | } 171 | }) 172 | }) 173 | 174 | test('csrf options', async () => { 175 | const csrf = sinon.stub() 176 | 177 | const fastifyCsrf = proxyquire('../', { 178 | '@fastify/csrf': function (...args) { 179 | return csrf(...args) 180 | } 181 | }) 182 | 183 | const csrfOpts = { some: 'options' } 184 | 185 | await Fastify() 186 | .register(fastifyCookie) 187 | .register(fastifyCsrf, { csrfOpts }) 188 | 189 | sinon.assert.calledWith(csrf, csrfOpts) 190 | }) 191 | 192 | const spyLogger = { 193 | warn: sinon.spy(), 194 | error: sinon.spy(), 195 | info: sinon.spy(), 196 | debug: sinon.spy(), 197 | fatal: sinon.spy(), 198 | trace: sinon.spy(), 199 | child: () => spyLogger 200 | } 201 | 202 | test('logLevel options', async t => { 203 | async function load (logLevel) { 204 | const opts = logLevel ? { logLevel } : {} 205 | const fastify = Fastify({ loggerInstance: spyLogger }) 206 | await fastify.register(fastifyCookie) 207 | await fastify.register(fastifyCsrf, opts) 208 | fastify.get('/', async (_req, reply) => { 209 | reply.generateCsrf() 210 | return {} 211 | }) 212 | 213 | fastify.post('/', { 214 | onRequest: fastify.csrfProtection 215 | }, async () => { 216 | return {} 217 | }) 218 | await fastify.ready() 219 | return fastify 220 | } 221 | 222 | async function makeRequests (fastify) { 223 | const response = await fastify.inject({ 224 | method: 'GET', 225 | path: '/' 226 | }) 227 | 228 | const cookie = response.cookies[0] 229 | 230 | // missing csrf secret 231 | await fastify.inject({ 232 | method: 'POST', 233 | payload: { hello: 'world' }, 234 | path: '/', 235 | }) 236 | 237 | // invalid csrf token 238 | await fastify.inject({ 239 | method: 'POST', 240 | payload: { hello: 'world' }, 241 | path: '/', 242 | cookies: { 243 | [cookie.name]: cookie.value 244 | } 245 | }) 246 | } 247 | 248 | t.afterEach(() => { 249 | spyLogger.warn.resetHistory() 250 | spyLogger.error.resetHistory() 251 | }) 252 | 253 | await t.test('default log level', async t => { 254 | t.plan(1) 255 | const fastify = await load() 256 | await makeRequests(fastify) 257 | 258 | t.assert.strictEqual(spyLogger.warn.callCount, 2) 259 | }) 260 | 261 | await t.test('custom log level', async t => { 262 | t.plan(2) 263 | const fastify = await load('error') 264 | await makeRequests(fastify) 265 | 266 | t.assert.strictEqual(spyLogger.error.callCount, 2) 267 | t.assert.ok(spyLogger.warn.notCalled) 268 | }) 269 | 270 | await t.test('silent log level', async t => { 271 | t.plan(1) 272 | const fastify = await load('silent') 273 | await makeRequests(fastify) 274 | 275 | t.assert.ok(spyLogger.warn.notCalled) 276 | }) 277 | }) 278 | 279 | async function runtTest (t, load, tkn, hook = 'onRequest') { 280 | await t.test(`Token in ${tkn.place}`, async t => { 281 | const fastify = await load() 282 | 283 | fastify.get('/', async (_req, reply) => { 284 | const token = reply.generateCsrf() 285 | return { token } 286 | }) 287 | 288 | fastify.post('/', { [hook]: fastify.csrfProtection }, async (req) => { 289 | return req.body 290 | }) 291 | 292 | let response = await fastify.inject({ 293 | method: 'GET', 294 | path: '/' 295 | }) 296 | 297 | t.assert.strictEqual(response.statusCode, 200) 298 | const cookie = response.cookies[0] 299 | const tokenFirst = response.json().token 300 | 301 | response = await fastify.inject({ 302 | method: 'GET', 303 | path: '/', 304 | cookies: { 305 | [cookie.name]: cookie.value 306 | } 307 | }) 308 | 309 | t.assert.strictEqual(response.statusCode, 200) 310 | const cookieSecond = response.cookies[0] 311 | const token = response.json().token 312 | 313 | if (fastify.testType === 'fastify-session') { 314 | t.assert.deepStrictEqual(cookie, cookieSecond) 315 | } else if (fastify.testType === 'fastify-secure-session') { 316 | t.assert.notStrictEqual(cookie, cookieSecond) 317 | } else { 318 | t.assert.strictEqual(cookieSecond, undefined) 319 | } 320 | t.assert.notStrictEqual(tokenFirst, token) 321 | 322 | if (tkn.place === 'body') { 323 | response = await fastify.inject({ 324 | method: 'POST', 325 | path: '/', 326 | payload: { 327 | hello: 'world', 328 | [tkn.property]: token 329 | }, 330 | cookies: { 331 | [cookie.name]: cookie.value 332 | } 333 | }) 334 | } else { 335 | response = await fastify.inject({ 336 | method: 'POST', 337 | path: '/', 338 | payload: { hello: 'world' }, 339 | headers: { 340 | [tkn.property]: token 341 | }, 342 | cookies: { 343 | [cookie.name]: cookie.value 344 | } 345 | }) 346 | } 347 | 348 | t.assert.strictEqual(response.statusCode, 200) 349 | t.assert.strictEqual(response.json().hello, 'world') 350 | 351 | response = await fastify.inject({ 352 | method: 'POST', 353 | path: '/', 354 | payload: { hello: 'world' } 355 | }) 356 | 357 | t.assert.strictEqual(response.statusCode, 403) 358 | t.assert.strictEqual(response.json().message, 'Missing csrf secret') 359 | 360 | response = await fastify.inject({ 361 | method: 'POST', 362 | path: '/', 363 | payload: { hello: 'world' }, 364 | cookies: { 365 | [cookie.name]: cookie.value 366 | } 367 | }) 368 | 369 | t.assert.strictEqual(response.statusCode, 403) 370 | t.assert.strictEqual(response.json().message, 'Invalid csrf token') 371 | }) 372 | } 373 | 374 | async function runCookieOpts (t, load) { 375 | await t.test('Custom cookie options', async t => { 376 | const fastify = await load() 377 | 378 | fastify.get('/', async (_req, reply) => { 379 | const token = reply.generateCsrf({ path: '/hello' }) 380 | return { token } 381 | }) 382 | 383 | const response = await fastify.inject({ 384 | method: 'GET', 385 | path: '/' 386 | }) 387 | 388 | const cookie = response.cookies[0] 389 | t.assert.strictEqual(cookie.path, '/hello') 390 | }) 391 | } 392 | -------------------------------------------------------------------------------- /test/user-info.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const fastifyCookie = require('@fastify/cookie') 6 | const fastifySession = require('@fastify/session') 7 | const fastifySecureSession = require('@fastify/secure-session') 8 | const fastifyCsrf = require('../') 9 | 10 | const sodium = require('sodium-native') 11 | const key = Buffer.alloc(sodium.crypto_secretbox_KEYBYTES) 12 | sodium.randombytes_buf(key) 13 | 14 | test('Cookies with User-Info', async t => { 15 | const fastify = Fastify() 16 | await fastify.register(fastifyCookie) 17 | await fastify.register(fastifyCsrf, { 18 | getUserInfo (req) { 19 | return userInfoDB[req.body.username] 20 | }, 21 | csrfOpts: { 22 | hmacKey: 'foo' 23 | } 24 | }) 25 | 26 | const userInfoDB = { 27 | foo: 'a42' 28 | } 29 | 30 | fastify.post('/login', async (req, reply) => { 31 | const token = reply.generateCsrf({ userInfo: userInfoDB[req.body.username] }) 32 | return { token } 33 | }) 34 | 35 | // must be preHandler as we are parsing the body 36 | fastify.post('/', { preHandler: fastify.csrfProtection }, async (req) => { 37 | return req.body 38 | }) 39 | 40 | const response1 = await fastify.inject({ 41 | method: 'POST', 42 | path: '/login', 43 | body: { 44 | username: 'foo' 45 | } 46 | }) 47 | 48 | t.assert.strictEqual(response1.statusCode, 200) 49 | 50 | const cookie1 = response1.cookies[0] 51 | const { token } = response1.json() 52 | 53 | const response2 = await fastify.inject({ 54 | method: 'POST', 55 | path: '/', 56 | cookies: { 57 | _csrf: cookie1.value 58 | }, 59 | body: { 60 | _csrf: token, 61 | username: 'foo' 62 | } 63 | }) 64 | 65 | t.assert.strictEqual(response2.statusCode, 200) 66 | }) 67 | 68 | test('Session with User-Info', async t => { 69 | const fastify = Fastify() 70 | await fastify.register(fastifyCookie) 71 | await fastify.register(fastifySession, { 72 | secret: 'a'.repeat(32), 73 | cookie: { path: '/', secure: false } 74 | }) 75 | await fastify.register(fastifyCsrf, { 76 | sessionPlugin: '@fastify/session', 77 | getUserInfo (req) { 78 | return req.session.username 79 | }, 80 | csrfOpts: { 81 | hmacKey: 'foo' 82 | } 83 | }) 84 | 85 | fastify.post('/login', async (req, reply) => { 86 | req.session.username = req.body.username 87 | const token = reply.generateCsrf({ userInfo: req.body.username }) 88 | return { token } 89 | }) 90 | 91 | // must be preHandler as we are parsing the body 92 | fastify.post('/', { preHandler: fastify.csrfProtection }, async (req) => { 93 | return req.body 94 | }) 95 | 96 | const response1 = await fastify.inject({ 97 | method: 'POST', 98 | path: '/login', 99 | body: { 100 | username: 'foo' 101 | } 102 | }) 103 | 104 | t.assert.strictEqual(response1.statusCode, 200) 105 | 106 | const cookie1 = response1.cookies[0] 107 | const { token } = response1.json() 108 | 109 | const response2 = await fastify.inject({ 110 | method: 'POST', 111 | path: '/', 112 | cookies: { 113 | sessionId: cookie1.value 114 | }, 115 | body: { 116 | _csrf: token, 117 | username: 'foo' 118 | } 119 | }) 120 | 121 | t.assert.strictEqual(response2.statusCode, 200) 122 | }) 123 | 124 | test('SecureSession with User-Info', async t => { 125 | const fastify = Fastify() 126 | await fastify.register(fastifySecureSession, { key, cookie: { path: '/', secure: false } }) 127 | await fastify.register(fastifyCsrf, { 128 | sessionPlugin: '@fastify/secure-session', 129 | getUserInfo (req) { 130 | return req.session.get('username') 131 | }, 132 | csrfOpts: { 133 | hmacKey: 'foo' 134 | } 135 | }) 136 | 137 | fastify.post('/login', async (req, reply) => { 138 | req.session.set('username', req.body.username) 139 | const token = reply.generateCsrf({ userInfo: req.body.username }) 140 | return { token } 141 | }) 142 | 143 | // must be preHandler as we are parsing the body 144 | fastify.post('/', { preHandler: fastify.csrfProtection }, async (req) => { 145 | return req.body 146 | }) 147 | 148 | const response1 = await fastify.inject({ 149 | method: 'POST', 150 | path: '/login', 151 | body: { 152 | username: 'foo' 153 | } 154 | }) 155 | 156 | t.assert.strictEqual(response1.statusCode, 200) 157 | 158 | const cookie1 = response1.cookies[0] 159 | const { token } = response1.json() 160 | 161 | const response2 = await fastify.inject({ 162 | method: 'POST', 163 | path: '/', 164 | cookies: { 165 | session: cookie1.value 166 | }, 167 | body: { 168 | _csrf: token, 169 | username: 'foo' 170 | } 171 | }) 172 | 173 | t.assert.strictEqual(response2.statusCode, 200) 174 | }) 175 | 176 | test('Validate presence of hmac key with User-Info /1', async (t) => { 177 | const fastify = Fastify() 178 | await fastify.register(fastifyCookie) 179 | 180 | await t.assert.rejects(new Promise((resolve, reject) => { 181 | fastify.register(fastifyCsrf, { 182 | getUserInfo (req) { 183 | return req.session.get('username') 184 | } 185 | }).then(() => { 186 | resolve() 187 | }).catch(err => { 188 | reject(err) 189 | }) 190 | }), 191 | (err) => { 192 | t.assert.strictEqual(err.name, 'AssertionError') 193 | t.assert.strictEqual(err.message, 'csrfOpts.hmacKey is required') 194 | return true 195 | } 196 | ) 197 | }) 198 | 199 | test('Validate presence of hmac key with User-Info /2', async (t) => { 200 | const fastify = Fastify() 201 | await fastify.register(fastifyCookie) 202 | 203 | await t.assert.rejects(new Promise((resolve, reject) => { 204 | fastify.register(fastifyCsrf, { 205 | getUserInfo (req) { 206 | return req.session.get('username') 207 | }, 208 | sessionPlugin: '@fastify/cookie' 209 | }).then(() => { 210 | resolve() 211 | }).catch(err => { 212 | reject(err) 213 | }) 214 | }), 215 | (err) => { 216 | t.assert.strictEqual(err.name, 'AssertionError') 217 | t.assert.strictEqual(err.message, 'csrfOpts.hmacKey is required') 218 | return true 219 | } 220 | 221 | ) 222 | }) 223 | 224 | test('Validate presence of hmac key with User-Info /3', async (t) => { 225 | const fastify = Fastify() 226 | await fastify.register(fastifyCookie) 227 | 228 | await t.assert.rejects(new Promise((resolve, reject) => { 229 | fastify.register(fastifyCsrf, { 230 | getUserInfo (req) { 231 | return req.session.get('username') 232 | }, 233 | csrfOpts: { 234 | hmacKey: undefined 235 | } 236 | }).then(() => { 237 | resolve() 238 | }).catch(err => { 239 | reject(err) 240 | }) 241 | }), 242 | (err) => { 243 | t.assert.strictEqual(err.name, 'AssertionError') 244 | t.assert.strictEqual(err.message, 'csrfOpts.hmacKey is required') 245 | return true 246 | } 247 | ) 248 | }) 249 | 250 | test('Validate presence of hmac key with User-Info /4', async (t) => { 251 | const fastify = Fastify() 252 | await fastify.register(fastifyCookie) 253 | 254 | await t.assert.rejects(new Promise((resolve, reject) => { 255 | fastify.register(fastifyCsrf, { 256 | getUserInfo (req) { 257 | return req.session.get('username') 258 | }, 259 | sessionPlugin: '@fastify/cookie', 260 | csrfOpts: { 261 | hmacKey: undefined 262 | } 263 | }).then(() => { 264 | resolve() 265 | }).catch(err => { 266 | reject(err) 267 | }) 268 | }), 269 | (err) => { 270 | t.assert.strictEqual(err.name, 'AssertionError') 271 | t.assert.strictEqual(err.message, 'csrfOpts.hmacKey is required') 272 | return true 273 | } 274 | ) 275 | }) 276 | 277 | test('Validate presence of hmac key with User-Info /5', async (t) => { 278 | const fastify = Fastify() 279 | await fastify.register(fastifySecureSession, { key, cookie: { path: '/', secure: false } }) 280 | 281 | await t.assert.doesNotReject(new Promise((resolve, reject) => { 282 | fastify.register(fastifyCsrf, { 283 | getUserInfo (req) { 284 | return req.session.get('username') 285 | }, 286 | sessionPlugin: '@fastify/secure-session' 287 | }).then(() => { 288 | resolve() 289 | }).catch(err => { 290 | reject(err) 291 | }) 292 | })) 293 | }) 294 | 295 | test('Validate presence of hmac key with User-Info /6', async (t) => { 296 | const fastify = Fastify() 297 | await fastify.register(fastifySecureSession, { key, cookie: { path: '/', secure: false } }) 298 | 299 | await t.assert.doesNotReject(new Promise((resolve, reject) => { 300 | fastify.register(fastifyCsrf, { 301 | getUserInfo (req) { 302 | return req.session.get('username') 303 | }, 304 | sessionPlugin: '@fastify/secure-session', 305 | csrfOpts: { 306 | hmacKey: 'foo' 307 | } 308 | }).then(() => { 309 | resolve() 310 | }).catch(err => { 311 | reject(err) 312 | }) 313 | })) 314 | }) 315 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | ///