├── .eslintrc ├── .gitignore ├── AUTHORS ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── SECURITY.md ├── client ├── index.js └── lib.js ├── dist ├── jwt-csrf.js └── jwt-csrf.min.js ├── gulpfile.js ├── index.js ├── lib ├── crypto.js └── index.js ├── package.json └── test └── jwtCsrfSpec.js /.eslintrc: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "env": { 4 | "node": true 5 | }, 6 | "rules": { 7 | "quotes": 0 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .build 3 | .idea 4 | .settings 5 | logs 6 | *.log 7 | .DS_Store 8 | coverage 9 | cdbs 10 | protected 11 | tmp 12 | ext 13 | tools/tmp 14 | public/components 15 | plato-reports 16 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Praveen Gorthy 2 | Daniel Brain 3 | Joel Chen 4 | Mark Stuart 5 | Viswa Nachiappan 6 | Caoyang Shi 7 | Kuswara Pranawahadi 8 | Stephen Westhafer 9 | Arpan Nanavati 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to jwt-csrf 2 | 3 | We are always looking for ways to make our modules better. Adding features and fixing bugs allows everyone who depends 4 | on this code to create better, more stable applications. 5 | Feel free to raise a pull request to us. Our team would review your proposed modifications and, if appropriate, merge 6 | your changes into our code. Ideas and other comments are also welcome. 7 | 8 | ## Getting Started 9 | 1. Create your own [fork](https://help.github.com/articles/fork-a-repo) of this [repository](../../fork). 10 | ```bash 11 | # Clone it 12 | $ git clone git@github.com:me/jwt-csrf.git 13 | 14 | # Change directory 15 | $ cd jwt-csrf 16 | 17 | # Add the upstream repo 18 | $ git remote add upstream git://github.com/krakenjs/jwt-csrf.git 19 | 20 | # Get the latest upstream changes 21 | $ git pull upstream 22 | 23 | # Install dependencies 24 | $ npm install 25 | 26 | # Run scripts to verify installation 27 | $ npm test 28 | $ npm run-script lint 29 | $ npm run-script cover 30 | ``` 31 | 32 | ## Making Changes 33 | 1. Make sure that your changes adhere to the current coding conventions used throughout the project, indentation, accurate comments, etc. 34 | 2. Lint your code regularly and ensure it passes prior to submitting a PR: 35 | `$ npm run lint`. 36 | 3. Ensure existing tests pass (`$ npm test`) and include test cases which fail without your change and succeed with it. 37 | 38 | ## Submitting Changes 39 | 1. Ensure that no errors are generated by ESLint. 40 | 2. Commit your changes in logical chunks, i.e. keep your changes small per single commit. 41 | 3. Locally merge (or rebase) the upstream branch into your topic branch: `$ git pull upstream && git merge`. 42 | 4. Push your topic branch up to your fork: `$ git push origin `. 43 | 5. Open a [Pull Request](https://help.github.com/articles/using-pull-requests) with a clear title and description. 44 | 45 | If you have any questions about contributing, please feel free to contact us by posting your questions on GitHub. 46 | 47 | Copyright 2016, PayPal under [the Apache 2.0 license](LICENSE.txt). 48 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*───────────────────────────────────────────────────────────────────────────*\ 2 | │ Copyright (C) 2016 PayPal │ 3 | │ │ 4 | │ │ 5 | │ Licensed under the Apache License, Version 2.0 (the "License"); you may │ 6 | │ not use this file except in compliance with the License. You may obtain │ 7 | │ a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 │ 8 | │ │ 9 | │ Unless required by applicable law or agreed to in writing, software │ 10 | │ distributed under the License is distributed on an "AS IS" BASIS, │ 11 | │ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │ 12 | │ See the License for the specific language governing permissions and │ 13 | │ limitations under the License. │ 14 | \*───────────────────────────────────────────────────────────────────────────*/ 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jwt-csrf 2 | 3 | CSRF protection using the power of JWTs. Provides a number of stateless methods of csrf protection, if you don't want to keep a session. 4 | 5 | Defaults to the [double submit](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Double_Submit_Cookies) method of csrf protection, but supports a number of different strategies. 6 | 7 | ## Middleware 8 | 9 | #### Example 10 | 11 | ```javascript 12 | var express = require('express'); 13 | var app = express(); 14 | 15 | var jwtCSRF = require('jwt-csrf'); 16 | var jwtMiddleware = jwtCSRF.middleware(options); // This can be used like any other Express middleware 17 | 18 | app.use(jwtMiddleware); // Executed on all requests 19 | ``` 20 | 21 | The middleware must be included before others to be effective. 22 | 23 | #### Handling errors 24 | 25 | On errors, jwt-csrf will call `next(err)` with a `jwtCSRF.CSRFError`. If you want to handle this specifically, you can do so in a middleware: 26 | 27 | ```javascript 28 | function(err, req, res, next) { 29 | if (err instanceof jwtCSRF.CSRFError) { 30 | explode(); 31 | } 32 | } 33 | ``` 34 | 35 | ## Options 36 | 37 | `options` is an Object with the following format: 38 | * **secret** : String (Required) - Your application's secret, must be cryptographically complex. 39 | * **csrfDriver** : String (Optional) - CSRF driver/strategy to use. Defaults to `DOUBLE_SUBMIT`. 40 | * **expiresInMinutes** : Number (Optional) - A token's expiration time. Defaults to `60`. 41 | * **headerName** : String (Optional) - The name of the response header that will contain the csrf token. Defaults to `x-csrf-jwt`. 42 | * **excludeUrls** : Array (Optional) - An array of elements that can be comprised of any of the following 43 | * A **regular expression object**. The request url will be compared using RegExp.test() using the regular expression supplied here 44 | * A **two element array** with the first being a string based regular expression and the second being the regular expression options such as "i" or "g". A regular expression will be created and tested against the request url. This is the ideal way to create a regular expression if the excludUrls are defined in a JSON file. 45 | * **A string**. This string will be tested as a regular expression with no regexp options. If this doesn't match the `request.originalUrl`, then it will be tested against the url as a direct string match. 46 | * **getUserToken** : Function (Optional) - Get a user specific token for the `AUTHED_TOKEN` and `AUTHED_DOUBLE_SUBMIT` strategies. Must accept `req` and return a user-specific token (like a user id) for a known user. 47 | * **getCookieDomain** : Function (Optional) - Must accept `req` and return a domain that the cookie will be scoped for (Ex: ".mysite.com"). Otherwise, defaults to the domain inside of the request. 48 | 49 | ## CSRF Drivers 50 | 51 | ##### DOUBLE_SUBMIT 52 | 53 | Persist two linked tokens on the client side, one via an http header, another via a cookie. On incoming requests, match the tokens. 54 | 55 | ##### AUTHED_TOKEN 56 | 57 | Persist a token via an http header linked to the currently authenticated user. Validate against the user for incoming requests. 58 | 59 | Requires `getUserToken` to be set in options 60 | 61 | ##### AUTHED_DOUBLE_SUBMIT 62 | 63 | A combination of `DOUBLE_SUBMIT` and `AUTHED_TOKEN`, either strategy passing will allow the request to go through. 64 | 65 | 66 | ## Client side 67 | 68 | Note that jwt-csrf **only** works for ajax calls, not full-page posts, since it relies on being able to set and read http headers. 69 | 70 | ### Persisting the csrf token 71 | 72 | Firstly, you will need to pass the token down in your initial page render. You can get the value as follows on the server-side, to insert into your initial html: 73 | 74 | ```javascript 75 | var jwtCsrf = require('jwt-csrf'); 76 | var token = jwtCsrf.getHeaderToken(req, res, { secret: mySecret }); 77 | ``` 78 | 79 | You have two options for persisting the csrf token on the client side: 80 | 81 | #### 1. Manually 82 | 83 | - On every ajax response, persist the `x-csrf-jwt` header 84 | - On every ajax request, send the persisted `x-csrf-jwt` header 85 | 86 | For example: 87 | 88 | ```javascript 89 | var csrfJwt; 90 | 91 | jQuery.ajax({ 92 | type: 'POST', 93 | url: '/api/some/action', 94 | headers: { 95 | 'x-csrf-jwt': csrfJwt 96 | }, 97 | success: function(data, textStatus, request){ 98 | csrfJwt = request.getResponseHeader('x-csrf-jwt'); 99 | } 100 | }); 101 | ``` 102 | 103 | #### 2. Automatically, by patching XMLHttpRequest 104 | 105 | ```javascript 106 | var jwtCsrf = require('jwt-csrf/client'); 107 | jwtCsrf.setToken(initialToken); 108 | jwtCsrf.patchXhr(); 109 | ``` 110 | 111 | This will hook into each request and response and automatically persist the token on the client side for you. 112 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | We take security very seriously and ask that you follow the following process. 4 | 5 | 6 | ## Contact us 7 | If you think you may have found a security bug we ask that you privately send the details to DL-PP-Kraken-Js@ebay.com. Please make sure to use a descriptive title in the email. 8 | 9 | 10 | ## Expectations 11 | We will generally get back to you within **24 hours**, but a more detailed response may take up to **48 hours**. If you feel we're not responding back in time, please send us a message *without detail* on Twitter [@kraken_js](https://twitter.com/kraken_js). 12 | 13 | 14 | ## History 15 | No reported issues 16 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | 2 | import { interceptHeader } from './lib'; 3 | 4 | let token; 5 | let HEADER_NAME = 'x-csrf-jwt'; 6 | 7 | export function setToken(newToken) { 8 | token = newToken; 9 | } 10 | 11 | export function getToken(newToken) { 12 | return token; 13 | } 14 | 15 | export function setHeaderName(name) { 16 | HEADER_NAME = name; 17 | } 18 | 19 | export function getHeaderName() { 20 | return HEADER_NAME; 21 | } 22 | 23 | export function patchXhr() { 24 | 25 | interceptHeader(HEADER_NAME, { 26 | 27 | get(value) { 28 | token = value; 29 | }, 30 | 31 | set() { 32 | return token; 33 | } 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /client/lib.js: -------------------------------------------------------------------------------- 1 | 2 | export function interceptHeader(name, { get, set }) { 3 | 4 | if (set) { 5 | let open = window.XMLHttpRequest.prototype.open; 6 | 7 | window.XMLHttpRequest.prototype.open = function () { 8 | 9 | let result = open.apply(this, arguments); 10 | 11 | let value = set(); 12 | 13 | if (value) { 14 | this.setRequestHeader(name, value); 15 | } else { 16 | return result; 17 | } 18 | 19 | let setRequestHeader = this.setRequestHeader; 20 | 21 | this.setRequestHeader = function(headerName, headerValue) { 22 | 23 | if (headerName === name) { 24 | return; 25 | } 26 | 27 | return setRequestHeader.apply(this, arguments); 28 | } 29 | 30 | return result; 31 | }; 32 | } 33 | 34 | if (get) { 35 | 36 | let send = window.XMLHttpRequest.prototype.send; 37 | 38 | window.XMLHttpRequest.prototype.send = function () { 39 | 40 | let self = this; 41 | let onreadystatechange = self.onreadystatechange; 42 | 43 | function listener() { 44 | try { 45 | let newValue = this.getResponseHeader(name); 46 | 47 | if (newValue) { 48 | get(newValue); 49 | } 50 | } catch (err) { 51 | // pass 52 | } 53 | 54 | if (onreadystatechange) { 55 | return onreadystatechange.apply(this, arguments); 56 | } 57 | } 58 | 59 | delete self.onreadystatechange; 60 | self.onreadystatechange = listener; 61 | 62 | Object.defineProperty(self, 'onreadystatechange', { 63 | get() { 64 | return listener; 65 | }, 66 | set(handler) { 67 | onreadystatechange = handler; 68 | } 69 | }); 70 | 71 | return send.apply(this, arguments); 72 | }; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /dist/jwt-csrf.js: -------------------------------------------------------------------------------- 1 | (function webpackUniversalModuleDefinition(root, factory) { 2 | if(typeof exports === 'object' && typeof module === 'object') 3 | module.exports = factory(); 4 | else if(typeof define === 'function' && define.amd) 5 | define("jwtCsrf", [], factory); 6 | else if(typeof exports === 'object') 7 | exports["jwtCsrf"] = factory(); 8 | else 9 | root["jwtCsrf"] = factory(); 10 | })(this, function() { 11 | return /******/ (function(modules) { // webpackBootstrap 12 | /******/ // The module cache 13 | /******/ var installedModules = {}; 14 | 15 | /******/ // The require function 16 | /******/ function __webpack_require__(moduleId) { 17 | 18 | /******/ // Check if module is in cache 19 | /******/ if(installedModules[moduleId]) 20 | /******/ return installedModules[moduleId].exports; 21 | 22 | /******/ // Create a new module (and put it into the cache) 23 | /******/ var module = installedModules[moduleId] = { 24 | /******/ exports: {}, 25 | /******/ id: moduleId, 26 | /******/ loaded: false 27 | /******/ }; 28 | 29 | /******/ // Execute the module function 30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 31 | 32 | /******/ // Flag the module as loaded 33 | /******/ module.loaded = true; 34 | 35 | /******/ // Return the exports of the module 36 | /******/ return module.exports; 37 | /******/ } 38 | 39 | 40 | /******/ // expose the modules object (__webpack_modules__) 41 | /******/ __webpack_require__.m = modules; 42 | 43 | /******/ // expose the module cache 44 | /******/ __webpack_require__.c = installedModules; 45 | 46 | /******/ // __webpack_public_path__ 47 | /******/ __webpack_require__.p = ""; 48 | 49 | /******/ // Load entry module and return exports 50 | /******/ return __webpack_require__(0); 51 | /******/ }) 52 | /************************************************************************/ 53 | /******/ ([ 54 | /* 0 */ 55 | /***/ function(module, exports, __webpack_require__) { 56 | 57 | 'use strict'; 58 | 59 | Object.defineProperty(exports, "__esModule", { 60 | value: true 61 | }); 62 | exports.setToken = setToken; 63 | exports.getToken = getToken; 64 | exports.setHeaderName = setHeaderName; 65 | exports.getHeaderName = getHeaderName; 66 | exports.patchXhr = patchXhr; 67 | 68 | var _lib = __webpack_require__(1); 69 | 70 | var token = void 0; 71 | var HEADER_NAME = 'x-csrf-jwt'; 72 | 73 | function setToken(newToken) { 74 | token = newToken; 75 | } 76 | 77 | function getToken(newToken) { 78 | return token; 79 | } 80 | 81 | function setHeaderName(name) { 82 | HEADER_NAME = name; 83 | } 84 | 85 | function getHeaderName() { 86 | return HEADER_NAME; 87 | } 88 | 89 | function patchXhr() { 90 | 91 | (0, _lib.interceptHeader)(HEADER_NAME, { 92 | get: function get(value) { 93 | token = value; 94 | }, 95 | set: function set() { 96 | return token; 97 | } 98 | }); 99 | } 100 | 101 | /***/ }, 102 | /* 1 */ 103 | /***/ function(module, exports) { 104 | 105 | 'use strict'; 106 | 107 | Object.defineProperty(exports, "__esModule", { 108 | value: true 109 | }); 110 | exports.interceptHeader = interceptHeader; 111 | function interceptHeader(name, _ref) { 112 | var get = _ref.get, 113 | set = _ref.set; 114 | 115 | 116 | if (set) { 117 | (function () { 118 | var open = window.XMLHttpRequest.prototype.open; 119 | 120 | window.XMLHttpRequest.prototype.open = function () { 121 | 122 | var result = open.apply(this, arguments); 123 | 124 | var value = set(); 125 | 126 | if (value) { 127 | this.setRequestHeader(name, value); 128 | } else { 129 | return result; 130 | } 131 | 132 | var setRequestHeader = this.setRequestHeader; 133 | 134 | this.setRequestHeader = function (headerName, headerValue) { 135 | 136 | if (headerName === name) { 137 | return; 138 | } 139 | 140 | return setRequestHeader.apply(this, arguments); 141 | }; 142 | 143 | return result; 144 | }; 145 | })(); 146 | } 147 | 148 | if (get) { 149 | (function () { 150 | 151 | var send = window.XMLHttpRequest.prototype.send; 152 | 153 | window.XMLHttpRequest.prototype.send = function () { 154 | 155 | var self = this; 156 | var onreadystatechange = self.onreadystatechange; 157 | 158 | function listener() { 159 | try { 160 | var newValue = this.getResponseHeader(name); 161 | 162 | if (newValue) { 163 | get(newValue); 164 | } 165 | } catch (err) { 166 | // pass 167 | } 168 | 169 | if (onreadystatechange) { 170 | return onreadystatechange.apply(this, arguments); 171 | } 172 | } 173 | 174 | delete self.onreadystatechange; 175 | self.onreadystatechange = listener; 176 | 177 | Object.defineProperty(self, 'onreadystatechange', { 178 | get: function get() { 179 | return listener; 180 | }, 181 | set: function set(handler) { 182 | onreadystatechange = handler; 183 | } 184 | }); 185 | 186 | return send.apply(this, arguments); 187 | }; 188 | })(); 189 | } 190 | } 191 | 192 | /***/ } 193 | /******/ ]) 194 | }); 195 | ; -------------------------------------------------------------------------------- /dist/jwt-csrf.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("jwtCsrf",[],t):"object"==typeof exports?exports.jwtCsrf=t():e.jwtCsrf=t()}(this,function(){return function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return e[r].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){"use strict";function r(e){c=e}function o(e){return c}function i(e){p=e}function s(){return p}function u(){(0,a.interceptHeader)(p,{get:function(e){c=e},set:function(){return c}})}Object.defineProperty(t,"__esModule",{value:!0}),t.setToken=r,t.getToken=o,t.setHeaderName=i,t.getHeaderName=s,t.patchXhr=u;var a=n(1),c=void 0,p="x-csrf-jwt"},function(e,t){"use strict";function n(e,t){var n=t.get,r=t.set;r&&!function(){var t=window.XMLHttpRequest.prototype.open;window.XMLHttpRequest.prototype.open=function(){var n=t.apply(this,arguments),o=r();if(!o)return n;this.setRequestHeader(e,o);var i=this.setRequestHeader;return this.setRequestHeader=function(t,n){if(t!==e)return i.apply(this,arguments)},n}}(),n&&!function(){var t=window.XMLHttpRequest.prototype.send;window.XMLHttpRequest.prototype.send=function(){function r(){try{var t=this.getResponseHeader(e);t&&n(t)}catch(e){}if(i)return i.apply(this,arguments)}var o=this,i=o.onreadystatechange;return delete o.onreadystatechange,o.onreadystatechange=r,Object.defineProperty(o,"onreadystatechange",{get:function(){return r},set:function(e){i=e}}),t.apply(this,arguments)}}()}Object.defineProperty(t,"__esModule",{value:!0}),t.interceptHeader=n}])}); -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 2 | var gulp = require('gulp'); 3 | var eslint = require('gulp-eslint'); 4 | var webpack = require('webpack'); 5 | var gulpWebpack = require('gulp-webpack'); 6 | var mocha = require('gulp-mocha'); 7 | 8 | gulp.task('build', ['webpack', 'webpack-min']); 9 | 10 | var FILE_NAME = 'jwt-csrf'; 11 | var MODULE_NAME = 'jwtCsrf'; 12 | 13 | var WEBPACK_CONFIG = { 14 | module: { 15 | loaders: [ 16 | { 17 | test: /\.js$/, 18 | exclude: /(node_modules|bower_components)/, 19 | loader: 'babel', 20 | query: { 21 | presets: ['es2015'], 22 | plugins: [ 23 | 'transform-object-rest-spread', 24 | 'syntax-object-rest-spread', 25 | 'transform-es3-property-literals', 26 | 'transform-es3-member-expression-literals' 27 | ] 28 | } 29 | } 30 | ] 31 | }, 32 | output: { 33 | filename: `${FILE_NAME}.js`, 34 | libraryTarget: 'umd', 35 | umdNamedDefine: true, 36 | library: MODULE_NAME 37 | }, 38 | bail: true 39 | }; 40 | 41 | var WEBPACK_CONFIG_MIN = Object.assign({}, WEBPACK_CONFIG, { 42 | output: { 43 | filename: `${FILE_NAME}.min.js`, 44 | libraryTarget: 'umd', 45 | umdNamedDefine: true, 46 | library: MODULE_NAME 47 | }, 48 | plugins: [ 49 | new webpack.optimize.UglifyJsPlugin({ 50 | test: /\.js$/, 51 | exclude: /(node_modules|bower_components)/, 52 | minimize: true 53 | }) 54 | ] 55 | }); 56 | 57 | 58 | var ESLINT_CONFIG = { 59 | 60 | "env": { 61 | "browser": false, 62 | "node": true, 63 | "amd": true, 64 | "mocha": true, 65 | "es6": true 66 | }, 67 | 68 | "parserOptions": { 69 | "sourceType": "module" 70 | }, 71 | 72 | "globals": { 73 | "window": true, 74 | "document": true, 75 | "Promise": false, 76 | "performance": true 77 | }, 78 | 79 | "fix": true, 80 | 81 | "rules": { 82 | // possible errors 83 | "comma-dangle": 2, 84 | "no-cond-assign": 2, 85 | "no-console": 0, 86 | "no-constant-condition": 0, 87 | "no-control-regex": 2, 88 | "no-debugger": 2, 89 | "no-dupe-args": 0, 90 | "no-dupe-keys": 2, 91 | "no-duplicate-case": 0, 92 | "no-empty-character-class": 2, 93 | "no-empty": 2, 94 | "no-ex-assign": 2, 95 | "no-extra-boolean-cast": 2, 96 | "no-extra-parens": 0, 97 | "no-extra-semi": 2, 98 | "no-func-assign": 2, 99 | "no-inner-declarations": [2, "functions"], 100 | "no-invalid-regexp": 2, 101 | "no-irregular-whitespace": 0, 102 | "no-negated-in-lhs": 2, 103 | "no-obj-calls": 2, 104 | "no-regex-spaces": 2, 105 | "no-sparse-arrays": 2, 106 | "no-unexpected-multiline": 0, 107 | "no-unreachable": 2, 108 | "quote-props": 2, 109 | "use-isnan": 2, 110 | "valid-jsdoc": 0, 111 | "valid-typeof": 2, 112 | 113 | // best practices 114 | "accessor-pairs": 0, 115 | "array-callback-return": 2, 116 | "block-scoped-var": 2, 117 | "complexity": [0, 10], // would love to turn this on someday 118 | "consistent-return": 0, 119 | "curly": [2, "all"], 120 | "default-case": 2, 121 | "dot-location": 0, 122 | "dot-notation": 2, 123 | "eqeqeq": 2, 124 | "guard-for-in": 0, 125 | "no-alert": 2, 126 | "no-caller": 2, 127 | "no-case-declarations": 0, 128 | "no-div-regex": 2, 129 | "no-else-return": 0, 130 | "no-empty-function": 1, 131 | "no-empty-pattern": 0, 132 | "no-eq-null": 2, 133 | "no-eval": 2, 134 | "no-extend-native": 2, 135 | "no-extra-bind": 2, 136 | "no-extra-label": 2, 137 | "no-fallthrough": 2, 138 | "no-floating-decimal": 2, 139 | "no-implicit-coercion": 0, 140 | "no-implicit-globals": 2, 141 | "no-implied-eval": 2, 142 | "no-invalid-this": 0, 143 | "no-iterator": 2, 144 | "no-labels": 2, 145 | "no-lone-blocks": 2, 146 | "no-loop-func": 2, 147 | "no-magic-numbers": 0, 148 | "no-multi-spaces": 2, 149 | "no-multi-str": 2, 150 | "no-native-reassign": 2, 151 | "no-new-func": 2, 152 | "no-new-wrappers": 2, 153 | "no-new": 2, 154 | "no-octal-escape": 2, 155 | "no-octal": 2, 156 | "no-param-reassign": 0, 157 | "no-process-env": 0, 158 | "no-proto": 2, 159 | "no-redeclare": 2, 160 | "no-return-assign": 2, 161 | "no-script-url": 2, 162 | "no-self-assign": 2, 163 | "no-self-compare": 2, 164 | "no-sequences": 2, 165 | "no-throw-literal": 2, 166 | "no-unmodified-loop-condition": 2, 167 | "no-unused-expressions": 2, 168 | "no-unused-labels": 2, 169 | "no-useless-call": 0, 170 | "no-useless-concat": 0, 171 | "no-void": 2, 172 | "no-warning-comments": 0, 173 | "no-with": 2, 174 | "radix": 2, 175 | "vars-on-top": 0, 176 | "wrap-iife": 2, 177 | "yoda": 2, 178 | 179 | // strict 180 | "strict": [2, "global"], 181 | 182 | // variables 183 | "init-declarations": 0, 184 | "no-catch-shadow": 2, 185 | "no-delete-var": 2, 186 | "no-label-var": 2, 187 | "no-restricted-globals": 0, 188 | "no-shadow-restricted-names": 2, 189 | "no-shadow": 2, 190 | "no-undef-init": 2, 191 | "no-undef": 2, 192 | "no-undefined": 0, 193 | "no-unused-vars": [2, {"vars": "all", "args": "none"}], 194 | "no-use-before-define": 1, 195 | 196 | // node.js 197 | "callback-return": 0, 198 | "global-require": 0, 199 | "handle-callback-err": 2, 200 | "no-mixed-requires": 2, 201 | "no-new-require": 2, 202 | "no-path-concat": 2, 203 | "no-process-env": 0, 204 | "no-process-exit": 2, 205 | "no-restricted-modules": 0, 206 | "no-sync": 0, 207 | 208 | // stylistic issues 209 | "array-bracket-spacing": 0, 210 | "block-spacing": 0, 211 | "brace-style": 0, 212 | "camelcase": 0, 213 | "comma-spacing": [2, {"before": false, "after": true}], 214 | "comma-style": [1, "last"], 215 | "computed-property-spacing": 0, 216 | "consistent-this": [2, "self"], 217 | "eol-last": 0, 218 | "func-names": 0, 219 | "func-style": [0, "declaration"], 220 | "id-blacklist": 0, 221 | "id-length": 0, 222 | "id-match": 0, 223 | "indent": [2, 4, {"SwitchCase": 1}], 224 | "jsx-quotes": 0, 225 | "key-spacing": 0, 226 | "keyword-spacing": 2, 227 | "linebreak-style": 0, 228 | "lines-around-comment": 0, 229 | "max-depth": [0, 4], 230 | "max-len": [0, 80, 4], 231 | "max-nested-callbacks": [0, 2], 232 | "max-params": [1, 5], 233 | "max-statements": [1, 30], 234 | "new-cap": 2, 235 | "new-parens": 2, 236 | "newline-after-var": 0, 237 | "newline-before-return": 0, 238 | "newline-per-chained-call": 0, 239 | "no-array-constructor": 2, 240 | "no-bitwise": 2, 241 | "no-continue": 0, 242 | "no-inline-comments": 0, 243 | "no-lonely-if": 2, 244 | "no-mixed-spaces-and-tabs": [2, true], 245 | "no-multiple-empty-lines": 0, 246 | "no-negated-condition": 0, 247 | "no-nested-ternary": 2, 248 | "no-new-object": 2, 249 | "no-plusplus": 0, 250 | "no-restricted-syntax": 0, 251 | "no-spaced-func": 2, 252 | "no-ternary": 0, 253 | "no-trailing-spaces": 0, 254 | "no-underscore-dangle": 0, 255 | "no-unneeded-ternary": 0, 256 | "no-whitespace-before-property": 2, 257 | "object-curly-spacing": 0, 258 | "one-var": 0, 259 | "one-var-declaration-per-line": [1, "always"], 260 | "operator-assignment": 0, 261 | "operator-linebreak": 0, 262 | "padded-blocks": 0, 263 | "quote-props": 0, 264 | "quotes": [2, "single"], 265 | "require-jsdoc": 0, 266 | "semi-spacing": 2, 267 | "semi": 2, 268 | "sort-imports": 0, 269 | "sort-vars": 0, 270 | "space-before-blocks": [2, "always"], 271 | "space-before-function-paren": 0, 272 | "space-in-parens": 2, 273 | "space-infix-ops": 2, 274 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 275 | "spaced-comment": 2, 276 | "wrap-regex": 2 277 | } 278 | 279 | }; 280 | 281 | var ESLINT_CONFIG_ES6 = { 282 | 283 | "parser": "babel-eslint", 284 | 285 | "env": { 286 | "browser": false, 287 | "node": true, 288 | "amd": true, 289 | "mocha": true, 290 | "es6": true 291 | }, 292 | 293 | "ecmaFeatures": { 294 | "modules": true 295 | }, 296 | 297 | "parserOptions": { 298 | "sourceType": "module" 299 | }, 300 | 301 | "globals": { 302 | "window": true, 303 | "document": true, 304 | "Promise": false, 305 | "performance": true 306 | }, 307 | 308 | "fix": true, 309 | 310 | "rules": { 311 | // possible errors 312 | "comma-dangle": 2, 313 | "no-cond-assign": 2, 314 | "no-console": 0, 315 | "no-constant-condition": 0, 316 | "no-control-regex": 2, 317 | "no-debugger": 2, 318 | "no-dupe-args": 0, 319 | "no-dupe-keys": 2, 320 | "no-duplicate-case": 0, 321 | "no-empty-character-class": 2, 322 | "no-empty": 2, 323 | "no-ex-assign": 2, 324 | "no-extra-boolean-cast": 2, 325 | "no-extra-parens": 0, 326 | "no-extra-semi": 2, 327 | "no-func-assign": 2, 328 | "no-inner-declarations": [2, "functions"], 329 | "no-invalid-regexp": 2, 330 | "no-irregular-whitespace": 0, 331 | "no-negated-in-lhs": 2, 332 | "no-obj-calls": 2, 333 | "no-regex-spaces": 2, 334 | "no-sparse-arrays": 2, 335 | "no-unexpected-multiline": 0, 336 | "no-unreachable": 2, 337 | "quote-props": 2, 338 | "use-isnan": 2, 339 | "valid-jsdoc": 0, 340 | "valid-typeof": 2, 341 | 342 | // best practices 343 | "accessor-pairs": 0, 344 | "array-callback-return": 2, 345 | "block-scoped-var": 2, 346 | "complexity": [0, 10], // would love to turn this on someday 347 | "consistent-return": 0, 348 | "curly": [2, "all"], 349 | "default-case": 2, 350 | "dot-location": 0, 351 | "dot-notation": 2, 352 | "eqeqeq": 2, 353 | "guard-for-in": 0, 354 | "no-alert": 2, 355 | "no-caller": 2, 356 | "no-case-declarations": 0, 357 | "no-div-regex": 2, 358 | "no-else-return": 0, 359 | "no-empty-function": 1, 360 | "no-empty-pattern": 0, 361 | "no-eq-null": 2, 362 | "no-eval": 2, 363 | "no-extend-native": 2, 364 | "no-extra-bind": 2, 365 | "no-extra-label": 2, 366 | "no-fallthrough": 2, 367 | "no-floating-decimal": 2, 368 | "no-implicit-coercion": 0, 369 | "no-implicit-globals": 2, 370 | "no-implied-eval": 2, 371 | "no-invalid-this": 0, 372 | "no-iterator": 2, 373 | "no-labels": 2, 374 | "no-lone-blocks": 2, 375 | "no-loop-func": 2, 376 | "no-magic-numbers": 0, 377 | "no-multi-spaces": 2, 378 | "no-multi-str": 2, 379 | "no-native-reassign": 2, 380 | "no-new-func": 2, 381 | "no-new-wrappers": 2, 382 | "no-new": 2, 383 | "no-octal-escape": 2, 384 | "no-octal": 2, 385 | "no-param-reassign": 0, 386 | "no-process-env": 0, 387 | "no-proto": 2, 388 | "no-redeclare": 2, 389 | "no-return-assign": 2, 390 | "no-script-url": 2, 391 | "no-self-assign": 2, 392 | "no-self-compare": 2, 393 | "no-sequences": 2, 394 | "no-throw-literal": 2, 395 | "no-unmodified-loop-condition": 2, 396 | "no-unused-expressions": 2, 397 | "no-unused-labels": 2, 398 | "no-useless-call": 0, 399 | "no-useless-concat": 0, 400 | "no-void": 2, 401 | "no-warning-comments": 0, 402 | "no-with": 2, 403 | "radix": 2, 404 | "vars-on-top": 0, 405 | "wrap-iife": 2, 406 | "yoda": 2, 407 | 408 | // strict 409 | "strict": [2, "global"], 410 | 411 | // variables 412 | "init-declarations": 0, 413 | "no-catch-shadow": 2, 414 | "no-delete-var": 2, 415 | "no-label-var": 2, 416 | "no-restricted-globals": 0, 417 | "no-shadow-restricted-names": 2, 418 | "no-shadow": 2, 419 | "no-undef-init": 2, 420 | "no-undef": 2, 421 | "no-undefined": 0, 422 | "no-unused-vars": [2, {"vars": "all", "args": "none"}], 423 | "no-use-before-define": 1, 424 | 425 | // node.js 426 | "callback-return": 0, 427 | "global-require": 0, 428 | "handle-callback-err": 2, 429 | "no-mixed-requires": 2, 430 | "no-new-require": 2, 431 | "no-path-concat": 2, 432 | "no-process-env": 0, 433 | "no-process-exit": 2, 434 | "no-restricted-modules": 0, 435 | "no-sync": 0, 436 | 437 | // stylistic issues 438 | "array-bracket-spacing": 0, 439 | "block-spacing": 0, 440 | "brace-style": 0, 441 | "camelcase": 0, 442 | "comma-spacing": [2, {"before": false, "after": true}], 443 | "comma-style": [1, "last"], 444 | "computed-property-spacing": 0, 445 | "consistent-this": [2, "self"], 446 | "eol-last": 0, 447 | "func-names": 0, 448 | "func-style": [0, "declaration"], 449 | "id-blacklist": 0, 450 | "id-length": 0, 451 | "id-match": 0, 452 | "indent": [2, 4, {"SwitchCase": 1}], 453 | "jsx-quotes": 0, 454 | "key-spacing": 0, 455 | "keyword-spacing": 2, 456 | "linebreak-style": 0, 457 | "lines-around-comment": 0, 458 | "max-depth": [0, 4], 459 | "max-len": [0, 80, 4], 460 | "max-nested-callbacks": [0, 2], 461 | "max-params": [1, 5], 462 | "max-statements": [1, 30], 463 | "new-cap": 2, 464 | "new-parens": 2, 465 | "newline-after-var": 0, 466 | "newline-before-return": 0, 467 | "newline-per-chained-call": 0, 468 | "no-array-constructor": 2, 469 | "no-bitwise": 2, 470 | "no-continue": 0, 471 | "no-inline-comments": 0, 472 | "no-lonely-if": 2, 473 | "no-mixed-spaces-and-tabs": [2, true], 474 | "no-multiple-empty-lines": 0, 475 | "no-negated-condition": 0, 476 | "no-nested-ternary": 2, 477 | "no-new-object": 2, 478 | "no-plusplus": 0, 479 | "no-restricted-syntax": 0, 480 | "no-spaced-func": 2, 481 | "no-ternary": 0, 482 | "no-trailing-spaces": 0, 483 | "no-underscore-dangle": 0, 484 | "no-unneeded-ternary": 0, 485 | "no-whitespace-before-property": 2, 486 | "object-curly-spacing": 0, 487 | "one-var": 0, 488 | "one-var-declaration-per-line": [1, "always"], 489 | "operator-assignment": 0, 490 | "operator-linebreak": 0, 491 | "padded-blocks": 0, 492 | "quote-props": 0, 493 | "quotes": [2, "single"], 494 | "require-jsdoc": 0, 495 | "semi-spacing": 2, 496 | "semi": 2, 497 | "sort-imports": 0, 498 | "sort-vars": 0, 499 | "space-before-blocks": [2, "always"], 500 | "space-before-function-paren": 0, 501 | "space-in-parens": 2, 502 | "space-infix-ops": 2, 503 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 504 | "spaced-comment": 2, 505 | "wrap-regex": 2, 506 | 507 | // ES6 508 | 509 | // require braces in arrow function body 510 | "arrow-body-style": 0, 511 | // require parens in arrow function arguments 512 | "arrow-parens": 0, 513 | // require space before/after arrow function's arrow 514 | "arrow-spacing": 2, 515 | // verify super() callings in constructors 516 | "constructor-super": 2, 517 | // enforce the spacing around the * in generator functions 518 | "generator-star-spacing": 0, 519 | // disallow modifying variables of class declarations 520 | "no-class-assign": 2, 521 | "no-confusing-arrow": 2, 522 | // disallow modifying variables that are declared using const 523 | "no-const-assign": 2, 524 | // disallow duplicate name in class members 525 | "no-dupe-class-members": 2, 526 | "no-new-symbol": 2, 527 | "no-restricted-imports": 0, 528 | // disallow to use this/super before super() calling in constructors. 529 | "no-this-before-super": 2, 530 | "no-useless-constructor": 2, 531 | // require let or const instead of var 532 | "no-var": 1, 533 | // require method and property shorthand syntax for object literals 534 | "object-shorthand": 1, 535 | // suggest using arrow functions as callbacks 536 | "prefer-arrow-callback": 1, 537 | // suggest using of const declaration for variables that are never modified after declared 538 | "prefer-const": 0, 539 | "prefer-rest-params": 0, 540 | // suggest using Reflect methods where applicable 541 | "prefer-reflect": 0, 542 | // suggest using the spread operator instead of .apply() 543 | "prefer-spread": 0, 544 | // suggest using template literals instead of strings concatenation 545 | "prefer-template": 2, 546 | // disallow generator functions that do not have yield 547 | "require-yield": 2, 548 | "template-curly-spacing": 0, 549 | "yield-star-spacing": 0 550 | } 551 | 552 | }; 553 | 554 | 555 | gulp.task('webpack', ['lint', 'mocha'], function() { 556 | return gulp.src('client/index.js') 557 | .pipe(gulpWebpack(WEBPACK_CONFIG)) 558 | .pipe(gulp.dest('dist')); 559 | }); 560 | 561 | gulp.task('webpack-min', ['lint', 'mocha'], function() { 562 | return gulp.src('client/index.js') 563 | .pipe(gulpWebpack(WEBPACK_CONFIG_MIN)) 564 | .pipe(gulp.dest('dist')); 565 | }); 566 | 567 | gulp.task('lint', ['lint:client', 'lint:server']); 568 | 569 | gulp.task('lint:client', function() { 570 | return gulp.src(['client/**']).pipe(eslint(ESLINT_CONFIG_ES6)) 571 | .pipe(eslint.format()) 572 | .pipe(eslint.failAfterError()); 573 | }); 574 | 575 | gulp.task('lint:server', function() { 576 | return gulp.src(['src/**']).pipe(eslint(ESLINT_CONFIG)) 577 | .pipe(eslint.format()) 578 | .pipe(eslint.failAfterError()); 579 | }); 580 | 581 | gulp.task('mocha', () => { 582 | return gulp.src(['test/jwtCsrfSpec.js']) 583 | // gulp-mocha needs filepaths so you can't have any plugins before it 584 | .pipe(mocha()); 585 | }); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = require('./lib'); -------------------------------------------------------------------------------- /lib/crypto.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var crypto = require('crypto'); 4 | 5 | function encrypt(key, text){ 6 | var cipher = crypto.createCipher('aes-256-ctr', key) 7 | var crypted = cipher.update(text,'utf8','hex') 8 | crypted += cipher.final('hex'); 9 | return crypted; 10 | } 11 | 12 | function decrypt(key, text){ 13 | var decipher = crypto.createDecipher('aes-256-ctr', key) 14 | var dec = decipher.update(text,'hex','utf8') 15 | dec += decipher.final('utf8'); 16 | return dec; 17 | } 18 | 19 | module.exports = { 20 | encrypt: encrypt, 21 | decrypt: decrypt 22 | }; -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var jsonwebtoken = require('jsonwebtoken'); 4 | var onHeaders = require('on-headers'); 5 | var uuid = require('node-uuid'); 6 | var encrypt = require('./crypto').encrypt; 7 | var decrypt = require('./crypto').decrypt; 8 | var util = require('util'); 9 | var _ = require('underscore'); 10 | var crypto = require('crypto'); 11 | 12 | var DEFAULT_EXPIRATION_IN_MINUTES = 60; 13 | var DEFAULT_HEADER_NAME = 'x-csrf-jwt'; 14 | var DEFAULT_CSRF_DRIVER = 'DOUBLE_SUBMIT'; 15 | 16 | // Some quick type testing methods 17 | var toString = Object.prototype.toString; 18 | var isRegExp = function(obj) { return !!/object RegExp/.exec(toString.apply(obj)); } 19 | var isString = function(obj) { return !!/object String/.exec(toString.apply(obj)); } 20 | var isArray = function(obj) { return !!/object Array/.exec(toString.apply(obj)); } 21 | 22 | /* 23 | CSRF Error 24 | ---------- 25 | 26 | A custom CSRF Error specifically for cases when we want to throw a 301 to the user's browser. 27 | Everything else is considered an unhandled error. 28 | */ 29 | 30 | function CSRFError(message) { 31 | this.message = this.code = 'EINVALIDCSRF_' + message; 32 | } 33 | 34 | util.inherits(CSRFError, Error); 35 | 36 | /* 37 | Hash 38 | ---- 39 | 40 | Hash a string using sha256 41 | */ 42 | 43 | function hash(secret, text) { 44 | return crypto.createHmac('sha256', secret).update(text).digest('hex'); 45 | } 46 | 47 | /* 48 | Resolve Domain 49 | -------------- 50 | 51 | Determine the current domain 52 | */ 53 | 54 | function resolveDomain(req) { 55 | var host = req.get('host'); // Ex: "mysite.com:8000" 56 | var truncateAt = host.indexOf(':'); 57 | var domain = host.substr(0, truncateAt > -1 ? truncateAt : host.length); // Ex: "mysite.com" 58 | 59 | return '.' + domain; 60 | } 61 | 62 | 63 | /* 64 | JWT 65 | --- 66 | 67 | An abstraction on top of JWT which also handles serialization/deserialization and encryption/decryption 68 | 69 | The final token looks something like: 70 | 71 | [JWT-SIGNED [ENCRYPTED [JSON SERIALIZED [JS OBJECT]]]] 72 | 73 | These methods just handle creating and unpacking this object. 74 | 75 | * pack: serialize, encrypt and sign a javascript object token 76 | 77 | * unpack: verify, decrypt and deserialize an jwt token 78 | */ 79 | 80 | var JWT = { 81 | 82 | pack: function(token, options) { 83 | 84 | // Attempt to serialize and encrypt the token 85 | var encryptedToken = { 86 | token: encrypt(options.secret, JSON.stringify(token)) 87 | }; 88 | 89 | // Then sign it using jsonwebtoken 90 | return jsonwebtoken.sign(encryptedToken, options.secret, { 91 | expiresInMinutes: options.expiresInMinutes || DEFAULT_EXPIRATION_IN_MINUTES 92 | }); 93 | }, 94 | 95 | unpack: function(token, options) { 96 | 97 | var encryptedPayload; 98 | 99 | try { 100 | 101 | // Verify the json token 102 | encryptedPayload = jsonwebtoken.verify(token, options.secret); 103 | } 104 | catch (err) { 105 | 106 | // If there's no message, it's probably some weird unhandled error 107 | if (!err.message) { 108 | throw err; 109 | } 110 | 111 | // Normalize 'some error message' to 'SOME_ERROR_MESSAGE' 112 | throw new CSRFError(err.message.substring(0, 25).replace(/ /, '_').toUpperCase()); 113 | } 114 | 115 | // Attempt to decrypt and deserialize the token 116 | return JSON.parse(decrypt(options.secret, encryptedPayload.token)); 117 | } 118 | }; 119 | 120 | 121 | 122 | /* 123 | PERSISTENCE DRIVERS 124 | ------------------- 125 | 126 | Drivers for writing and reading to 'persistence' layers, e.g. headers or cookies 127 | 128 | * drop: a user defined method which drops the encrypted jwt token to the persistence layer of choice 129 | 130 | * retrieve: a user defined method which reads the encrypted jwt token from the persistence layer of choice 131 | */ 132 | 133 | var PERSISTENCE_DRIVERS = { 134 | 135 | header: { 136 | drop: function(req, res, options, jwtToken) { 137 | var headerName = options.headerName || DEFAULT_HEADER_NAME; 138 | 139 | res.setHeader(headerName, jwtToken); 140 | res.setHeader(headerName + '-hash', hash(options.secret, jwtToken)); 141 | }, 142 | 143 | retrieve: function(req, res, options) { 144 | var headerName = options.headerName || DEFAULT_HEADER_NAME; 145 | 146 | var jwtToken = req.headers[headerName]; 147 | var jwtTokenBody = req.body && req.body.meta && req.body.meta[headerName]; 148 | 149 | if (!jwtToken && jwtTokenBody) { 150 | 151 | var jwtTokenHash = req.headers[headerName + '-hash']; 152 | 153 | if (!jwtTokenHash) { 154 | throw new CSRFError('BODY_CSRF_HASH_HEADER_MISSING'); 155 | } 156 | 157 | if (jwtTokenHash !== hash(options.secret, jwtTokenBody)) { 158 | throw new CSRFError('BODY_CSRF_HASH_MISMATCH'); 159 | } 160 | 161 | jwtToken = jwtTokenBody; 162 | } 163 | 164 | return jwtToken; 165 | } 166 | }, 167 | 168 | cookie: { 169 | drop: function(req, res, options, jwtToken) { 170 | 171 | var secure = Boolean(process.env.DEPLOY_ENV || req.protocol === 'https'); 172 | var expires = Date.now() + (1000 * 60 * 60 * 24 * 7); // 1 week 173 | 174 | res.cookie(options.headerName || DEFAULT_HEADER_NAME, jwtToken, { 175 | secure: secure, 176 | httpOnly: true, 177 | domain: options.getCookieDomain ? options.getCookieDomain(req) : resolveDomain(req), 178 | expires: new Date(expires), 179 | encryptName: true, 180 | encryptValue: false 181 | }); 182 | }, 183 | 184 | retrieve: function(req, res, options) { 185 | return req.cookies[options.headerName || DEFAULT_HEADER_NAME]; 186 | } 187 | } 188 | }; 189 | 190 | 191 | /* 192 | CSRF DRIVERS 193 | ------------ 194 | 195 | Drivers for generating and verifying jwt tokens. 196 | 197 | The process of retrieving, decrypting and dropping the tokens is abstracted, so 198 | we can just deal with simple javascript objects. 199 | 200 | * persist: a mapping of persistence layers we want to enable for the given csrf mode 201 | 202 | * generate: a user defined method which generates and returns the token (a javascript object) 203 | with everything needed to verify later 204 | 205 | * verify: a user defined method which recieves the token(s) on inbound requests, and throws a CSRFError if there 206 | is a verification problem. This later manifests as a 401 response to the browser. 207 | */ 208 | 209 | var CSRF_DRIVERS = { 210 | 211 | AUTHED_TOKEN: { 212 | 213 | persist: { 214 | cookie: false, 215 | header: true 216 | }, 217 | 218 | generate: function(req, res, options) { 219 | 220 | return { 221 | uid: options.getUserToken(req) 222 | }; 223 | }, 224 | 225 | verify: function(req, res, options, tokens) { 226 | 227 | // tokens.header will always be an object 228 | if (Object.keys(tokens.header).length === 0) { 229 | throw new CSRFError('TOKEN_NOT_IN_HEADER'); 230 | } 231 | 232 | if (options.getUserToken(req)) { 233 | 234 | if (!tokens.header.uid) { 235 | throw new CSRFError('TOKEN_PAYERID_MISSING'); 236 | } 237 | 238 | if (tokens.header.uid !== options.getUserToken(req)) { 239 | throw new CSRFError('TOKEN_PAYERID_MISMATCH'); 240 | } 241 | } 242 | } 243 | }, 244 | 245 | DOUBLE_SUBMIT: { 246 | 247 | persist: { 248 | cookie: true, 249 | header: true 250 | }, 251 | 252 | generate: function(req, res, options) { 253 | 254 | return { 255 | id: uuid.v4() 256 | } 257 | }, 258 | 259 | verify: function(req, res, options, tokens) { 260 | 261 | if (!Object.keys(tokens.header).length) { 262 | throw new CSRFError('TOKEN_NOT_IN_HEADER'); 263 | } 264 | 265 | if (!tokens.header.id) { 266 | throw new CSRFError('ID_NOT_IN_HEADER'); 267 | } 268 | 269 | if (!tokens.cookie.id) { 270 | throw new CSRFError('ID_NOT_IN_COOKIE'); 271 | } 272 | 273 | if (tokens.header.id !== tokens.cookie.id) { 274 | throw new CSRFError('HEADER_COOKIE_ID_MISMATCH'); 275 | } 276 | } 277 | }, 278 | 279 | AUTHED_DOUBLE_SUBMIT: { 280 | 281 | persist: { 282 | cookie: true, 283 | header: true 284 | }, 285 | 286 | generate: function(req, res, options) { 287 | 288 | return { 289 | uid: options.getUserToken(req), 290 | id: uuid.v4() 291 | } 292 | }, 293 | 294 | verify: function(req, res, options, tokens) { 295 | 296 | if (!Object.keys(tokens.header).length) { 297 | throw new CSRFError('TOKEN_NOT_IN_HEADER'); 298 | } 299 | 300 | try { 301 | 302 | // First do the cookie check 303 | 304 | if (!Object.keys(tokens.cookie).length) { 305 | throw new CSRFError('TOKEN_NOT_IN_COOKIE'); 306 | } 307 | 308 | if (!tokens.header.id) { 309 | throw new CSRFError('ID_NOT_IN_HEADER'); 310 | } 311 | 312 | if (!tokens.cookie.id) { 313 | throw new CSRFError('ID_NOT_IN_COOKIE'); 314 | } 315 | 316 | if (tokens.header.id !== tokens.cookie.id) { 317 | throw new CSRFError('HEADER_COOKIE_MISMATCH'); 318 | } 319 | 320 | } catch(err) { 321 | 322 | // Then if this fails, fall back to payerid 323 | 324 | if (err instanceof CSRFError) { 325 | 326 | if (options.getUserToken(req)) { 327 | 328 | if (!tokens.header.uid) { 329 | throw new CSRFError('TOKEN_PAYERID_MISSING'); 330 | } 331 | 332 | if (tokens.header.uid !== options.getUserToken(req)) { 333 | throw new CSRFError('TOKEN_PAYERID_MISMATCH'); 334 | } 335 | } 336 | 337 | } else { 338 | throw err; 339 | } 340 | }; 341 | } 342 | } 343 | }; 344 | 345 | 346 | /* 347 | Generate 348 | -------- 349 | 350 | Generate an object containing packed jwt tokens for each persistence layer: 351 | 352 | { 353 | header: 'xxxxxxxxx', 354 | cookie: 'yyyyyyyyy' 355 | } 356 | */ 357 | 358 | 359 | function generate(req, res, options) { 360 | 361 | // Determine which driver to use to generate the token 362 | var csrfDriver = options.csrfDriver || DEFAULT_CSRF_DRIVER; 363 | var driver = CSRF_DRIVERS[csrfDriver]; 364 | 365 | // Generate the token from our chosen driver 366 | var token = driver.generate(req, res, options); 367 | 368 | // Build a collection of jwt tokens 369 | var jwtTokens = {}; 370 | 371 | // Loop through each persistance type for the current csrfDriver 372 | Object.keys(driver.persist).forEach(function(persistenceDriver) { 373 | 374 | // Check if this persistence type is enabled for the current csrfDriver 375 | if (driver.persist[persistenceDriver]) { 376 | 377 | // Add the csrfDriver and persistenceDriver into the token so we can verify them on inbound requests 378 | var payload = _.extend({ 379 | csrfDriver: csrfDriver, 380 | persistenceDriver: persistenceDriver 381 | }, token); 382 | 383 | // Pack and save our token 384 | jwtTokens[persistenceDriver] = JWT.pack(payload, options); 385 | } 386 | }); 387 | 388 | return jwtTokens; 389 | } 390 | 391 | 392 | /* 393 | Drop 394 | ---- 395 | 396 | Generate new jwt tokens and drop them to the persistence layers (response headers/cookies). 397 | 398 | The persistence layers used will be those valid for the passed csrfType. 399 | */ 400 | 401 | function drop(req, res, options) { 402 | 403 | // Generate the jwt tokens we need to drop 404 | var jwtTokens = generate(req, res, options); 405 | 406 | // Add them to res.locals for other middlewares to consume 407 | res.locals.csrfJwtTokens = jwtTokens; 408 | 409 | // Loop through each persistence type for the current csrf driver 410 | Object.keys(jwtTokens).forEach(function(persistenceDriver) { 411 | 412 | // Get the individual token 413 | var jwtToken = jwtTokens[persistenceDriver]; 414 | 415 | // Drop the token to the persistence layer 416 | PERSISTENCE_DRIVERS[persistenceDriver].drop(req, res, options, jwtToken); 417 | }); 418 | } 419 | 420 | 421 | /* 422 | Read 423 | ---- 424 | 425 | Read and unpack a token, given a persistence driver name. 426 | 427 | e.g. giving 'header' would read the encrypted cookie from req.headers, then decrypt/unpack it. 428 | 429 | Returns an unpacked token, e.g. 430 | 431 | { 432 | uid: XXXX 433 | } 434 | */ 435 | 436 | function read(req, res, options, persistenceDriver) { 437 | 438 | var jwtToken = PERSISTENCE_DRIVERS[persistenceDriver].retrieve(req, res, options); 439 | 440 | if (!jwtToken) { 441 | return {}; 442 | } 443 | 444 | var token = JWT.unpack(jwtToken, options); 445 | 446 | // Default the persistenceDriver to 'header' (for legacy tokens -- can remove this later) 447 | token.persistenceDriver = token.persistenceDriver || 'header'; 448 | 449 | // Validate that it has the correct persistenceDriver 450 | if (token.persistenceDriver !== persistenceDriver) { 451 | throw new CSRFError('GOT_' + token.persistenceDriver.toUpperCase() + '_EXPECTED_' + persistenceDriver.toUpperCase()); 452 | } 453 | 454 | return token; 455 | } 456 | 457 | 458 | /* 459 | Retrieve 460 | -------- 461 | 462 | Retrieve and unpack all tokens from the persistence layer for our driver. 463 | 464 | Returns a mapping of unpacked tokens, e.g. 465 | 466 | { 467 | header: { 468 | uid: XXX 469 | }, 470 | cookie: { 471 | uid: YYY 472 | } 473 | } 474 | */ 475 | 476 | function retrieve(req, res, options, csrfDriver) { 477 | 478 | var driver = CSRF_DRIVERS[csrfDriver]; 479 | 480 | // Build an object of tokens 481 | var tokens = {}; 482 | 483 | // Loop over each persistence mechanism and build an object of decrypted tokens 484 | Object.keys(driver.persist).forEach(function(persistenceDriver) { 485 | 486 | // We only want tokens which are valid for the current csrf driver 487 | if (driver.persist[persistenceDriver]) { 488 | tokens[persistenceDriver] = read(req, res, options, persistenceDriver); 489 | } 490 | }); 491 | 492 | return tokens 493 | } 494 | 495 | 496 | /* 497 | Verify 498 | ------ 499 | 500 | Verify all tokens from the relevant persistence layers. 501 | 502 | Throw a CSRFError on any verification failures. 503 | */ 504 | 505 | function verify(req, res, options) { 506 | 507 | // First we need to get the header first to figure out which csrfDriver we need to verify 508 | var headerToken = read(req, res, options, 'header'); 509 | 510 | var csrfDriver = (headerToken.csrfDriver && CSRF_DRIVERS[headerToken.csrfDriver]) ? headerToken.csrfDriver : DEFAULT_CSRF_DRIVER; 511 | 512 | // Now we know the mode, we can retrieve the tokens from all persistence types for this mode 513 | var tokens = retrieve(req, res, options, csrfDriver); 514 | 515 | // Now we have all of the tokens, pass to the driver to verify them 516 | return CSRF_DRIVERS[csrfDriver].verify(req, res, options, tokens); 517 | } 518 | 519 | 520 | module.exports = { 521 | 522 | CSRFError: CSRFError, 523 | 524 | getHeaderToken: function(req, res, options) { 525 | 526 | var csrfDriver = options.csrfDriver || DEFAULT_CSRF_DRIVER; 527 | var token = CSRF_DRIVERS[csrfDriver].generate(req, res, options); 528 | 529 | var payload = _.extend({ 530 | csrfDriver: csrfDriver, 531 | persistenceDriver: 'header' 532 | }, token); 533 | 534 | return JWT.pack(payload, options); 535 | }, 536 | 537 | middleware: function (options) { 538 | 539 | var csrfDriver = options.csrfDriver || DEFAULT_CSRF_DRIVER; 540 | 541 | if (/AUTHED_TOKEN|AUTHED_DOUBLE_SUBMIT/.test(csrfDriver)) { 542 | if (!options.getUserToken) { 543 | throw new Error('csrf-jwt - getUserToken option required for AUTHED_TOKEN and AUTHED_DOUBLE_SUBMIT drivers'); 544 | } 545 | } 546 | 547 | var excludeUrls = options.excludeUrls || []; 548 | 549 | if (options.baseUrl) { 550 | excludeUrls = excludeUrls.map(function (route) { 551 | return options.baseUrl + route; 552 | }); 553 | } 554 | 555 | return function (req, res, next) { 556 | 557 | // An array to show us the matching excluded urls. If this array 558 | // contains any values, we should skip out and allow. 559 | var urlToTest; 560 | var excludeTheseUrls; 561 | 562 | // Set JWT in header and cookie before response goes out 563 | // This is done in onHeaders since we need to wait for any service 564 | // calls (e.g. auth) which may otherwise change the state of 565 | // our token 566 | onHeaders(res, function () { 567 | drop(req, res, options); 568 | }); 569 | 570 | // Skip out on non mutable REST methods 571 | if (/GET|HEAD|OPTIONS|TRACE/i.test(req.method)) { 572 | return next(); 573 | } 574 | 575 | if (excludeUrls.length) { 576 | 577 | // We only want to verify certain requests 578 | urlToTest = req.originalUrl; 579 | excludeTheseUrls = excludeUrls.filter(function (excludeUrl) { 580 | 581 | if (isArray(excludeUrl)) { 582 | 583 | var expression = excludeUrl[0]; 584 | var options = excludeUrl[1] || ''; 585 | 586 | return new RegExp(expression, options).test(urlToTest); 587 | } 588 | else if (isRegExp(excludeUrl)) { 589 | 590 | return excludeUrl.test(urlToTest); 591 | } 592 | else if (isString(excludeUrl)) { 593 | 594 | // Setup some variables: regExp for regExp testing and 595 | // some bits to use in the indexOf comparison 596 | var regExp = new RegExp(excludeUrl); 597 | var bits = ((urlToTest || '').split(/[?#]/, 1))[0]; 598 | 599 | // Test regular expression strings first 600 | if (regExp.exec(urlToTest)) { 601 | return true; 602 | } 603 | 604 | // If we are still here, test the legacy indexOf case 605 | return excludeUrls.indexOf(bits) !== -1; 606 | } 607 | }); 608 | 609 | // If the filter above actually found anything, that means 610 | // we matched on the possible exclusions. In this case, var's 611 | // just pop out and var the next piece of middleware have a 612 | // shot. 613 | if (excludeTheseUrls.length) { 614 | return next(); 615 | } 616 | } 617 | 618 | try { 619 | verify(req, res, options); 620 | } 621 | catch (err) { 622 | 623 | // If we get a CSRFError, we can send a 401 to trigger a retry, 624 | // otherwise the error will be unhandled 625 | if (err instanceof CSRFError) { 626 | res.status(401); 627 | } 628 | 629 | return next(err); 630 | } 631 | 632 | return next(); 633 | }; 634 | } 635 | }; 636 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jwt-csrf", 3 | "version": "4.1.1", 4 | "description": "A jwt middleware provider for hermes", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "npm run lint && gulp mocha", 8 | "lint": "gulp lint" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/krakenjs/jwt-csrf.git" 13 | }, 14 | "keywords": [ 15 | "jsonwebtoken", 16 | "jwt", 17 | "csrf", 18 | "security", 19 | "kraken", 20 | "krakenjs" 21 | ], 22 | "dependencies": { 23 | "jsonwebtoken": "^4.2.2", 24 | "node-uuid": "^1.4.3", 25 | "on-headers": "~1.0.0", 26 | "underscore": "^1.8.3" 27 | }, 28 | "licenses": [ 29 | { 30 | "type": "Apache 2.0", 31 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 32 | } 33 | ], 34 | "readmeFilename": "README.md", 35 | "devDependencies": { 36 | "babel": "^6.5.2", 37 | "babel-core": "^6.8.0", 38 | "babel-eslint": "^6.0.4", 39 | "babel-loader": "^6.2.4", 40 | "babel-plugin-add-module-exports": "^0.2.0", 41 | "babel-plugin-syntax-object-rest-spread": "^6.8.0", 42 | "babel-plugin-transform-es3-member-expression-literals": "^6.8.0", 43 | "babel-plugin-transform-es3-property-literals": "^6.8.0", 44 | "babel-plugin-transform-object-rest-spread": "^6.8.0", 45 | "babel-preset-es2015": "^6.6.0", 46 | "chai": "^3.5.0", 47 | "eslint": "^2", 48 | "gulp": "^3.9.1", 49 | "gulp-eslint": "^2.0.0", 50 | "gulp-mocha": "^2.2.0", 51 | "gulp-webpack": "^1.5.0", 52 | "mocha": "^2.2.1", 53 | "mocha-jenkins-reporter": "^0.1.6", 54 | "pre-commit": "^1.0.10", 55 | "sinon": "^1.17.4", 56 | "sinon-chai": "^2.6.0", 57 | "webpack": "^1.13.0", 58 | "xunit-file": "0.0.9" 59 | }, 60 | "pre-commit": [ 61 | "lint" 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /test/jwtCsrfSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var assert = require('chai').assert; 5 | var underscore = require('underscore'); 6 | var jwtCsrf = require('../index'); 7 | var should = require('chai').should(); 8 | 9 | var SECRET = 'somerandomsecret'; 10 | var MACKEY = 'somerandommackey'; 11 | 12 | var HEADER_NAME = 'csrf'; 13 | 14 | 15 | function merge(obj, props) { 16 | obj = obj || {}; 17 | props = props || {}; 18 | underscore.extend(props, obj); 19 | underscore.extend(obj, props); 20 | return obj; 21 | } 22 | 23 | 24 | function getOptions(obj) { 25 | return merge(obj, { 26 | headerName: HEADER_NAME, 27 | secret: SECRET, 28 | macKey: MACKEY, 29 | getUserToken: getUserToken 30 | }); 31 | } 32 | 33 | function getReq(obj) { 34 | return merge(obj, { 35 | get: function(key) { 36 | return 'mysite.com:8000'; 37 | }, 38 | protocol: 'https', 39 | headers: {}, 40 | cookies: {} 41 | }); 42 | } 43 | 44 | function getRes(obj) { 45 | return merge(obj, { 46 | locals: {}, 47 | cookies: {}, 48 | headers: {}, 49 | status: function (statusCode) { 50 | 51 | }, 52 | writeHead: function () { 53 | 54 | }, 55 | setHeader: function (key, value) { 56 | assert(value, 'header value exists'); 57 | this.headers[key] = value; 58 | }, 59 | cookie: function (key, value, options) { 60 | assert.equal(key, HEADER_NAME, 'cookie has been set'); 61 | assert(value, 'cookie value exists'); 62 | assert(options, 'cookie options exists'); 63 | assert(options.httpOnly, 'cookie options exists'); 64 | assert.equal(options.domain, '.mysite.com', 'cookie domain has been set'); 65 | this.cookies[key] = value; 66 | } 67 | }); 68 | } 69 | 70 | function assertError(err, message) { 71 | assert(err, 'Expected ' + message); 72 | assert.equal(err.message, message); 73 | } 74 | 75 | function assertNotError(err) { 76 | if (err) { 77 | throw err; 78 | } 79 | } 80 | 81 | function handleCSRFError(err) { 82 | if (err && !(err instanceof jwtCsrf.CSRFError)) { 83 | throw err; 84 | } 85 | } 86 | 87 | function getUserToken(req) { 88 | return req.userId; 89 | } 90 | 91 | function runMiddleware(req, res, options, callback) { 92 | 93 | options = getOptions(options); 94 | console.log(options); 95 | req = getReq(req); 96 | res = getRes(res); 97 | 98 | jwtCsrf.middleware(options)(req, res, function(err) { 99 | 100 | if (err) { 101 | handleCSRFError(err) 102 | } 103 | 104 | try { 105 | res.writeHead(200); 106 | } 107 | catch (err) { 108 | handleCSRFError(err); 109 | return callback(err); 110 | } 111 | 112 | return callback(err); 113 | }); 114 | 115 | } 116 | 117 | describe('middleware', function() { 118 | 119 | it('should support RegExp excludeUrls in options', function() { 120 | 121 | var req = {method: 'POST', originalUrl:'/sOmEuRl/somepath/allowMe'}; 122 | var res = {}; 123 | var options = { 124 | excludeUrls: [ 125 | /someurl/i, 126 | /zomethingElse/ 127 | ] 128 | }; 129 | 130 | runMiddleware(req, res, options, function(err) { 131 | 132 | assertNotError(err); 133 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 134 | }); 135 | 136 | req.originalUrl = '/some/path/to/zomethingelse'; 137 | runMiddleware(req, res, options, function(err) { 138 | should.exist(err); 139 | }); 140 | 141 | req.originalUrl = '/some/other/path/to/zomethingElse'; 142 | runMiddleware(req, res, options, function(err) { 143 | should.not.exist(err); 144 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 145 | }); 146 | }); 147 | 148 | it('should support arrays as RegExp opts excludeUrls in options', function() { 149 | 150 | var req = {method: 'POST', originalUrl:'/sOmEuRl/somepath/allowMe'}; 151 | var res = {}; 152 | var options = { 153 | excludeUrls: [ 154 | ['somePath',''], 155 | ['AnOtHeR/pAtH', 'i'] 156 | ] 157 | }; 158 | 159 | req.originalUrl = '/somepath/to/another/place'; 160 | runMiddleware(req, res, options, function(err) { 161 | should.exist(err); 162 | }); 163 | 164 | req.originalUrl = '/somePath/to/another/Place'; 165 | runMiddleware(req, res, options, function(err) { 166 | should.not.exist(err); 167 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 168 | }); 169 | 170 | req.originalUrl = '/another/path/to/a/different/place'; 171 | runMiddleware(req, res, options, function(err) { 172 | should.not.exist(err); 173 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 174 | }); 175 | 176 | req.originalUrl = '/AnOtHeR/pAtH/to/yet/another/place'; 177 | runMiddleware(req, res, options, function(err) { 178 | should.not.exist(err); 179 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 180 | }); 181 | }); 182 | 183 | it('should support strings as both regexp and indexOf', function() { 184 | 185 | var req = {method: 'POST'}; 186 | var res = {}; 187 | var options = { 188 | excludeUrls: [ 189 | '/the/road/to/n0wher3', 190 | '/a/place/for/numbers/[0-9]+' 191 | ] 192 | }; 193 | 194 | req.originalUrl = '/the/road/to/n0wher3'; 195 | runMiddleware(req, res, options, function(err) { 196 | should.not.exist(err); 197 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 198 | }); 199 | 200 | req.originalUrl = '/a/place/for/numbers/123412'; 201 | runMiddleware(req, res, options, function(err) { 202 | should.not.exist(err); 203 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 204 | }); 205 | }); 206 | 207 | it('should resolve the domain when the port is not included', function() { 208 | 209 | var req = { 210 | method: 'POST', 211 | get: function() { 212 | return 'mysite.com'; 213 | } 214 | }; 215 | var res = {}; 216 | var options = {}; 217 | 218 | // The assert to check the domain is located in a function on the 219 | // object returned by 'getRes()', which is called by 'runMiddleware()' 220 | runMiddleware(req, res, options, function() {}); 221 | }); 222 | 223 | it('should return and validate tokens for GET and POST in AUTHED_TOKEN mode', function() { 224 | 225 | var req = {method: 'GET'}; 226 | var res = {}; 227 | var options = {csrfDriver: 'AUTHED_TOKEN'}; 228 | 229 | runMiddleware(req, res, options, function(err) { 230 | 231 | assertNotError(err); 232 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 233 | assert(!res.cookies[HEADER_NAME], 'Expected JWT cookie to be absent'); 234 | 235 | req.method = 'POST'; 236 | req.headers[HEADER_NAME] = res.headers[HEADER_NAME]; 237 | 238 | runMiddleware(req, res, options, function(err) { 239 | 240 | assertNotError(err); 241 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 242 | assert(!res.cookies[HEADER_NAME], 'Expected JWT cookie to be absent'); 243 | }); 244 | }); 245 | }); 246 | 247 | it('should return and validate tokens for GET and POST in AUTHED_TOKEN mode with an authenticated buyer', function() { 248 | 249 | var req = { 250 | method: 'GET', 251 | userId: 'XYZ' 252 | }; 253 | var res = {}; 254 | var options = {csrfDriver: 'AUTHED_TOKEN'}; 255 | 256 | runMiddleware(req, res, options, function(err) { 257 | 258 | assertNotError(err); 259 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 260 | assert(!res.cookies[HEADER_NAME], 'Expected JWT cookie to be absent'); 261 | 262 | req.method = 'POST'; 263 | req.headers[HEADER_NAME] = res.headers[HEADER_NAME]; 264 | 265 | runMiddleware(req, res, options, function(err) { 266 | 267 | assertNotError(err); 268 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 269 | assert(!res.cookies[HEADER_NAME], 'Expected JWT cookie to be absent'); 270 | }); 271 | }); 272 | }); 273 | 274 | it('should return and validate tokens for GET and POST in DOUBLE_SUBMIT mode', function() { 275 | 276 | var req = {method: 'GET'}; 277 | var res = {}; 278 | var options = {csrfDriver: 'DOUBLE_SUBMIT'}; 279 | 280 | runMiddleware(req, res, options, function(err) { 281 | 282 | assertNotError(err); 283 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 284 | assert(res.cookies[HEADER_NAME], 'Expected JWT cookie to be absent'); 285 | 286 | req.method = 'POST'; 287 | req.headers[HEADER_NAME] = res.headers[HEADER_NAME]; 288 | req.cookies[HEADER_NAME] = res.cookies[HEADER_NAME]; 289 | 290 | runMiddleware(req, res, options, function(err) { 291 | 292 | assertNotError(err); 293 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 294 | assert(res.cookies[HEADER_NAME], 'Expected JWT cookie to be absent'); 295 | }); 296 | }); 297 | }); 298 | 299 | it('should return and validate tokens for GET in AUTHED_TOKEN and POST in DOUBLE_SUBMIT mode', function() { 300 | 301 | var req = {method: 'GET'}; 302 | var res = {}; 303 | var options = {csrfDriver: 'AUTHED_TOKEN'}; 304 | 305 | runMiddleware(req, res, options, function(err) { 306 | 307 | assertNotError(err); 308 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 309 | assert(!res.cookies[HEADER_NAME], 'Expected JWT cookie to be absent'); 310 | 311 | options.csrfDriver = 'DOUBLE_SUBMIT'; 312 | req.method = 'POST'; 313 | req.headers[HEADER_NAME] = res.headers[HEADER_NAME]; 314 | 315 | runMiddleware(req, res, options, function(err) { 316 | 317 | assertNotError(err); 318 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 319 | assert(res.cookies[HEADER_NAME], 'Expected JWT cookie to be present'); 320 | }); 321 | }); 322 | }); 323 | 324 | 325 | it('should return and validate tokens for GET in DOUBLE_SUBMIT and POST in AUTHED_TOKEN mode', function() { 326 | 327 | var req = {method: 'GET'}; 328 | var res = {}; 329 | var options = {csrfDriver: 'DOUBLE_SUBMIT'}; 330 | 331 | runMiddleware(req, res, options, function(err) { 332 | 333 | assertNotError(err); 334 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 335 | assert(res.cookies[HEADER_NAME], 'Expected JWT cookie to be absent'); 336 | 337 | options.csrfDriver = 'AUTHED_TOKEN'; 338 | req.method = 'POST'; 339 | req.headers[HEADER_NAME] = res.headers[HEADER_NAME]; 340 | req.cookies[HEADER_NAME] = res.cookies[HEADER_NAME]; 341 | 342 | runMiddleware(req, res, options, function(err) { 343 | 344 | assertNotError(err); 345 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 346 | assert(res.cookies[HEADER_NAME], 'Expected JWT cookie to be present'); 347 | }); 348 | }); 349 | }); 350 | 351 | it('should work when erratically changing between modes', function() { 352 | 353 | var req = {method: 'GET'}; 354 | var res = {}; 355 | var options = {csrfDriver: 'AUTHED_TOKEN'}; 356 | 357 | runMiddleware(req, res, options, function(err) { 358 | 359 | assertNotError(err); 360 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 361 | assert(!res.cookies[HEADER_NAME], 'Expected JWT cookie to be absent'); 362 | 363 | options.csrfDriver = 'DOUBLE_SUBMIT'; 364 | req.method = 'POST'; 365 | req.headers[HEADER_NAME] = res.headers[HEADER_NAME]; 366 | 367 | runMiddleware(req, res, options, function(err) { 368 | 369 | assertNotError(err); 370 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 371 | assert(res.cookies[HEADER_NAME], 'Expected JWT cookie to be present'); 372 | 373 | options.csrfDriver = 'AUTHED_TOKEN'; 374 | req.method = 'POST'; 375 | req.headers[HEADER_NAME] = res.headers[HEADER_NAME]; 376 | req.cookies[HEADER_NAME] = res.cookies[HEADER_NAME]; 377 | 378 | runMiddleware(req, res, options, function(err) { 379 | 380 | assertNotError(err); 381 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 382 | assert(res.cookies[HEADER_NAME], 'Expected JWT cookie to be present'); 383 | 384 | options.csrfDriver = 'DOUBLE_SUBMIT'; 385 | req.method = 'POST'; 386 | req.headers[HEADER_NAME] = res.headers[HEADER_NAME]; 387 | 388 | runMiddleware(req, res, options, function(err) { 389 | 390 | assertNotError(err); 391 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 392 | assert(res.cookies[HEADER_NAME], 'Expected JWT cookie to be present'); 393 | }); 394 | }); 395 | }); 396 | }); 397 | }); 398 | 399 | 400 | 401 | 402 | it('should fail for POST in AUTHED_TOKEN if no header is passed', function() { 403 | 404 | var req = {method: 'POST'}; 405 | var res = {}; 406 | var options = {csrfDriver: 'AUTHED_TOKEN'}; 407 | 408 | runMiddleware(req, res, options, function(err) { 409 | 410 | assertError(err, 'EINVALIDCSRF_TOKEN_NOT_IN_HEADER'); 411 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 412 | assert(!res.cookies[HEADER_NAME], 'Expected JWT cookie to be absent'); 413 | }); 414 | }); 415 | 416 | 417 | it('should fail for POST in DOUBLE_SUBMIT if no cookie or header is passed', function() { 418 | 419 | var req = {method: 'POST'}; 420 | var res = {}; 421 | var options = {csrfDriver: 'DOUBLE_SUBMIT'}; 422 | 423 | runMiddleware(req, res, options, function(err) { 424 | 425 | assertError(err, 'EINVALIDCSRF_TOKEN_NOT_IN_HEADER'); 426 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 427 | assert(res.cookies[HEADER_NAME], 'Expected JWT cookie to be present'); 428 | }); 429 | }); 430 | 431 | it('should fail POST in DOUBLE_SUBMIT mode if no header is present', function() { 432 | 433 | var req = {method: 'GET'}; 434 | var res = {}; 435 | var options = {csrfDriver: 'DOUBLE_SUBMIT'}; 436 | 437 | runMiddleware(req, res, options, function(err) { 438 | 439 | assertNotError(err); 440 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 441 | assert(res.cookies[HEADER_NAME], 'Expected JWT cookie to be absent'); 442 | 443 | req.method = 'POST'; 444 | req.cookies[HEADER_NAME] = res.cookies[HEADER_NAME]; 445 | 446 | runMiddleware(req, res, options, function(err) { 447 | 448 | assertError(err, 'EINVALIDCSRF_TOKEN_NOT_IN_HEADER'); 449 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 450 | assert(res.cookies[HEADER_NAME], 'Expected JWT cookie to be absent'); 451 | }); 452 | }); 453 | }); 454 | 455 | it('should fail POST in DOUBLE_SUBMIT mode if no cookie is present', function() { 456 | 457 | var req = {method: 'GET'}; 458 | var res = {}; 459 | var options = {csrfDriver: 'DOUBLE_SUBMIT'}; 460 | 461 | runMiddleware(req, res, options, function(err) { 462 | 463 | assertNotError(err); 464 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 465 | assert(res.cookies[HEADER_NAME], 'Expected JWT cookie to be absent'); 466 | 467 | req.method = 'POST'; 468 | req.headers[HEADER_NAME] = res.headers[HEADER_NAME]; 469 | req.userId = 'xyz'; 470 | 471 | runMiddleware(req, res, options, function(err) { 472 | 473 | assertError(err, 'EINVALIDCSRF_ID_NOT_IN_COOKIE'); 474 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 475 | assert(res.cookies[HEADER_NAME], 'Expected JWT cookie to be absent'); 476 | }); 477 | }); 478 | }); 479 | 480 | 481 | it('should fail POST in AUTHED_TOKEN mode with an authenticated buyer but not an authenticated token', function() { 482 | 483 | var req = { 484 | method: 'GET' 485 | }; 486 | var res = {}; 487 | var options = {csrfDriver: 'AUTHED_TOKEN'}; 488 | 489 | runMiddleware(req, res, options, function(err) { 490 | 491 | assertNotError(err); 492 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 493 | assert(!res.cookies[HEADER_NAME], 'Expected JWT cookie to be absent'); 494 | 495 | req.method = 'POST'; 496 | req.userId = 'XYZ'; 497 | req.headers[HEADER_NAME] = res.headers[HEADER_NAME]; 498 | 499 | runMiddleware(req, res, options, function(err) { 500 | 501 | assertError(err, 'EINVALIDCSRF_TOKEN_PAYERID_MISSING'); 502 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 503 | assert(!res.cookies[HEADER_NAME], 'Expected JWT cookie to be absent'); 504 | }); 505 | }); 506 | }); 507 | 508 | 509 | it('should fail POST in AUTHED_TOKEN mode with an authenticated buyer and an authenticated token for a different buyer', function() { 510 | 511 | var req = { 512 | method: 'GET', 513 | userId: 'ABC' 514 | }; 515 | var res = {}; 516 | var options = {csrfDriver: 'AUTHED_TOKEN'}; 517 | 518 | runMiddleware(req, res, options, function(err) { 519 | 520 | assertNotError(err); 521 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 522 | assert(!res.cookies[HEADER_NAME], 'Expected JWT cookie to be absent'); 523 | 524 | req.method = 'POST'; 525 | req.userId = 'XYZ'; 526 | req.headers[HEADER_NAME] = res.headers[HEADER_NAME]; 527 | 528 | runMiddleware(req, res, options, function(err) { 529 | 530 | assertError(err, 'EINVALIDCSRF_TOKEN_PAYERID_MISMATCH'); 531 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 532 | assert(!res.cookies[HEADER_NAME], 'Expected JWT cookie to be absent'); 533 | }); 534 | }); 535 | }); 536 | 537 | it('should fail POST in DOUBLE_SUBMIT mode when passing a header value as a cookie', function() { 538 | 539 | var req = {method: 'GET'}; 540 | var res = {}; 541 | var options = {csrfDriver: 'DOUBLE_SUBMIT'}; 542 | 543 | runMiddleware(req, res, options, function(err) { 544 | 545 | assertNotError(err); 546 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 547 | assert(res.cookies[HEADER_NAME], 'Expected JWT cookie to be absent'); 548 | 549 | req.method = 'POST'; 550 | req.headers[HEADER_NAME] = res.headers[HEADER_NAME]; 551 | req.cookies[HEADER_NAME] = res.headers[HEADER_NAME]; 552 | 553 | runMiddleware(req, res, options, function(err) { 554 | 555 | assertError(err, 'EINVALIDCSRF_GOT_HEADER_EXPECTED_COOKIE'); 556 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 557 | assert(res.cookies[HEADER_NAME], 'Expected JWT cookie to be absent'); 558 | }); 559 | }); 560 | }); 561 | 562 | it('should fail POST in DOUBLE_SUBMIT mode when passing a cookie value as a header', function() { 563 | 564 | var req = {method: 'GET'}; 565 | var res = {}; 566 | var options = {csrfDriver: 'DOUBLE_SUBMIT'}; 567 | 568 | runMiddleware(req, res, options, function(err) { 569 | 570 | assertNotError(err); 571 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 572 | assert(res.cookies[HEADER_NAME], 'Expected JWT cookie to be absent'); 573 | 574 | req.method = 'POST'; 575 | req.headers[HEADER_NAME] = res.cookies[HEADER_NAME]; 576 | req.cookies[HEADER_NAME] = res.cookies[HEADER_NAME]; 577 | 578 | runMiddleware(req, res, options, function(err) { 579 | 580 | assertError(err, 'EINVALIDCSRF_GOT_COOKIE_EXPECTED_HEADER'); 581 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 582 | assert(res.cookies[HEADER_NAME], 'Expected JWT cookie to be absent'); 583 | }); 584 | }); 585 | }); 586 | 587 | it('should fail POST in DOUBLE_SUBMIT mode when passing a cookie value as a header and vice versa', function() { 588 | 589 | var req = {method: 'GET'}; 590 | var res = {}; 591 | var options = {csrfDriver: 'DOUBLE_SUBMIT'}; 592 | 593 | runMiddleware(req, res, options, function(err) { 594 | 595 | assertNotError(err); 596 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 597 | assert(res.cookies[HEADER_NAME], 'Expected JWT cookie to be absent'); 598 | 599 | req.method = 'POST'; 600 | req.headers[HEADER_NAME] = res.cookies[HEADER_NAME]; 601 | req.cookies[HEADER_NAME] = res.headers[HEADER_NAME]; 602 | 603 | runMiddleware(req, res, options, function(err) { 604 | 605 | assertError(err, 'EINVALIDCSRF_GOT_COOKIE_EXPECTED_HEADER'); 606 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 607 | assert(res.cookies[HEADER_NAME], 'Expected JWT cookie to be absent'); 608 | }); 609 | }); 610 | }); 611 | 612 | it('should fail POST in DOUBLE_SUBMIT mode when the token and header do not match', function() { 613 | 614 | var req = {method: 'GET'}; 615 | var res = {}; 616 | var options = {csrfDriver: 'DOUBLE_SUBMIT'}; 617 | 618 | runMiddleware(req, res, options, function(err) { 619 | 620 | assertNotError(err); 621 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 622 | assert(res.cookies[HEADER_NAME], 'Expected JWT cookie to be absent'); 623 | 624 | var oldCookie = res.cookies[HEADER_NAME]; 625 | 626 | runMiddleware(req, res, options, function(err) { 627 | 628 | assertNotError(err); 629 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 630 | assert(res.cookies[HEADER_NAME], 'Expected JWT cookie to be absent'); 631 | 632 | 633 | req.method = 'POST'; 634 | req.headers[HEADER_NAME] = res.headers[HEADER_NAME]; 635 | req.cookies[HEADER_NAME] = oldCookie; 636 | req.userId = 'xyz'; 637 | 638 | runMiddleware(req, res, options, function(err) { 639 | 640 | assertError(err, 'EINVALIDCSRF_HEADER_COOKIE_ID_MISMATCH'); 641 | assert(res.headers[HEADER_NAME], 'Expected JWT header to be present'); 642 | assert(res.cookies[HEADER_NAME], 'Expected JWT cookie to be absent'); 643 | }); 644 | }); 645 | }); 646 | }); 647 | }); 648 | --------------------------------------------------------------------------------