├── .circleci └── config.yml ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── lib ├── module.js ├── server-middleware.js └── templates │ └── plugin.js ├── package.json ├── test ├── fixture │ ├── nuxt.config.js │ └── pages │ │ └── index.vue └── module.test.js └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:8.4 11 | 12 | steps: 13 | - checkout 14 | 15 | # Download and cache dependencies 16 | - restore_cache: 17 | keys: 18 | - v1-dependencies-{{ checksum "package.json" }} 19 | # fallback to using the latest cache if no exact match is found 20 | - v1-dependencies- 21 | 22 | - run: yarn install 23 | 24 | - save_cache: 25 | paths: 26 | - node_modules 27 | key: v1-dependencies-{{ checksum "package.json" }} 28 | 29 | # run tests! 30 | - run: yarn test 31 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = space 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | sourceType: 'module' 5 | }, 6 | env: { 7 | browser: true, 8 | node: true, 9 | jest: true 10 | }, 11 | extends: 'standard', 12 | plugins: [ 13 | 'jest', 14 | 'vue' 15 | ], 16 | rules: { 17 | // Allow paren-less arrow functions 18 | 'arrow-parens': 0, 19 | // Allow async-await 20 | 'generator-star-spacing': 0, 21 | // Allow debugger during development 22 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 23 | // Do not allow console.logs etc... 24 | 'no-console': 2 25 | }, 26 | globals: { 27 | 'jest/globals': true, 28 | jasmine: true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.iml 3 | .idea 4 | *.log* 5 | .nuxt 6 | .vscode 7 | .DS_STORE 8 | coverage 9 | dist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sam Garson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **nuxt-csrf** _CSRF protection for your Nuxt app_ 2 | 3 | ## Usage 4 | 5 | 1. Install the module 6 | 2. You will find your CSRF token in a Vuex store module 7 | 3. Send the token along with any API requests (except: `GET|HEAD|OPTIONS|TRACE`) 8 | 9 | ## Setup 10 | - Add `nuxt-csrf` dependency using yarn or npm to your project 11 | - Add `nuxt-csrf` to `modules` section of `nuxt.config.js` 12 | 13 | ```js 14 | { 15 | modules: [ 16 | // Simple usage 17 | 'nuxt-csrf', 18 | 19 | // With options 20 | ['nuxt-csrf', { 21 | sessionName: 'myCSRFSession', 22 | secretKey: process.env.SECRET_KEY, 23 | headerName: 'X-MY-CSRF-HEADER' 24 | }], 25 | ] 26 | } 27 | ``` -------------------------------------------------------------------------------- /lib/module.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path') 2 | const serverMiddleware = require('./server-middleware') 3 | 4 | module.exports = async function module (moduleOptions) { 5 | const options = Object.assign({}, moduleOptions, this.options.csrf) 6 | 7 | // Setup middleware 8 | this.addServerMiddleware(serverMiddleware(options)) 9 | 10 | // Setup store 11 | this.addPlugin({ 12 | src: resolve(__dirname, '../templates/plugin.js'), 13 | fileName: 'nuxt-csrf.js', 14 | options 15 | }) 16 | 17 | // Add router middleware to config 18 | this.options.router = this.options.router || {} 19 | this.options.router.middleware = this.options.router.middleware || [] 20 | this.options.router.middleware.push('csrf') 21 | } 22 | -------------------------------------------------------------------------------- /lib/server-middleware.js: -------------------------------------------------------------------------------- 1 | const sessions = require('client-sessions') 2 | const Tokens = require('csrf') 3 | 4 | const createError = (code, message, original) => { 5 | const err = new Error(message) 6 | 7 | err.statusCode = code 8 | err.originalError = original 9 | 10 | return err 11 | } 12 | 13 | const tokens = new Tokens() 14 | const safeMethods = /GET|HEAD|OPTIONS|TRACE/i 15 | 16 | function CSRFSession({ sessionName, secretKey, headerName = 'x-csrf-token' }) { 17 | this.sessionName = sessionName 18 | this.secretKey = secretKey 19 | this.headerName = headerName 20 | } 21 | 22 | CSRFSession.prototype.createSession = (req, res) => { 23 | const session = sessions({ 24 | cookieName: this.sessionName, 25 | secret: this.secretKey, 26 | duration: 60 * 60 * 1000 // 60 mins 27 | }) 28 | return new Promise(resolve => session(req, res, resolve)) 29 | } 30 | 31 | CSRFSession.prototype.initCSRF = async (req, res) => { 32 | await this.createSession(req, res, this.sessionName) 33 | const secret = req[this.sessionName].csrfSecret || await tokens.secret() 34 | req.csrfToken = tokens.create(secret) 35 | req[this.sessionName].csrfSecret = secret 36 | return secret 37 | } 38 | 39 | CSRFSession.prototype.verify = (req) => { 40 | const token = req.headers[this.headerName] 41 | const { csrfSecret: secret } = req[this.sessionName] 42 | return tokens.verify(secret, token) 43 | } 44 | 45 | module.exports = options => async (req, res, next) => { 46 | const session = new CSRFSession(options) 47 | await session.initCSRF(req, res) 48 | if (safeMethods.test(req.method)) return next() 49 | 50 | if (session.verify(req)) return next() 51 | 52 | next(createError(403, 'Invalid CSRF Token', 'InvalidCSRFToken')) 53 | } 54 | -------------------------------------------------------------------------------- /lib/templates/plugin.js: -------------------------------------------------------------------------------- 1 | import middleware from '@@/.nuxt/middleware' 2 | 3 | const moduleName = 'csrf' 4 | 5 | const initStore = context => { 6 | if (context.isClient) return 7 | context.store.registerModule(moduleName, { 8 | namespaced: true, 9 | state: { 10 | token: (context.req && context.req.csrfToken) 11 | } 12 | }) 13 | } 14 | 15 | middleware.csrf = async context => { 16 | initStore(context) 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-csrf", 3 | "version": "0.0.0", 4 | "description": "Add some good old CSRF to your Nuxt app", 5 | "license": "MIT", 6 | "contributors": [ 7 | { 8 | "name": "Sam Garson " 9 | } 10 | ], 11 | "main": "lib/module.js", 12 | "repository": "https://github.com/samtgarson/nuxt-csrf", 13 | "publishConfig": { 14 | "access": "public" 15 | }, 16 | "scripts": { 17 | "lint": "eslint lib test", 18 | "test": "npm run lint && jest", 19 | "release": "standard-version && git push --follow-tags && npm publish" 20 | }, 21 | "eslintIgnore": [ 22 | "lib/templates/*.*" 23 | ], 24 | "files": [ 25 | "lib" 26 | ], 27 | "jest": { 28 | "testEnvironment": "node", 29 | "coverageDirectory": "./coverage/", 30 | "collectCoverage": true, 31 | "collectCoverageFrom": [ 32 | "lib", 33 | "test" 34 | ] 35 | }, 36 | "dependencies": { 37 | "client-sessions": "^0.8.0", 38 | "csrf": "^3.0.6" 39 | }, 40 | "devDependencies": { 41 | "nuxt-module-builder": "latest" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/fixture/nuxt.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | srcDir: __dirname, 3 | dev: false, 4 | render: { 5 | resourceHints: false 6 | }, 7 | modules: [ 8 | '@@' 9 | ], 10 | csrf: { 11 | sessionName: 'nuxtSession', 12 | secretKey: 'sekret' 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/fixture/pages/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /test/module.test.js: -------------------------------------------------------------------------------- 1 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000 2 | process.env.PORT = process.env.PORT || 5060 3 | process.env.NODE_ENV = 'production' 4 | 5 | const { Nuxt, Builder } = require('nuxt') 6 | const request = require('request-promise-native') 7 | 8 | const config = require('./fixture/nuxt.config') 9 | 10 | const url = path => `http://localhost:${process.env.PORT}${path}` 11 | const get = path => request(url(path)) 12 | 13 | describe('Module', () => { 14 | let nuxt 15 | 16 | beforeAll(async () => { 17 | config.modules.unshift(function () { 18 | // Add test specific test only hooks on nuxt life cycle 19 | }) 20 | 21 | // Build a fresh nuxt 22 | nuxt = new Nuxt(config) 23 | await new Builder(nuxt).build() 24 | await nuxt.listen(process.env.PORT) 25 | }) 26 | 27 | afterAll(async () => { 28 | // Close all opened resources 29 | await nuxt.close() 30 | }) 31 | 32 | test('render', async () => { 33 | let html = await get('/') 34 | expect(html).toContain('Works!') 35 | }) 36 | }) 37 | --------------------------------------------------------------------------------