/*!
* csurf
* Copyright(c) 2011 Sencha Inc.
* Copyright(c) 2014 Jonathan Ong
* Copyright(c) 2014-2016 Douglas Christopher Wilson
* MIT Licensed
*/
'use strict'
/**
* Module dependencies.
* @private
*/
var Cookie = require('cookie')
var createError = require('http-errors')
var sign = require('cookie-signature').sign
var Tokens = require('csrf')
/**
* Module exports.
* @public
*/
module.exports = csurf
/**
* CSRF protection middleware.
*
* This middleware adds a `req.csrfToken()` function to make a token
* which should be added to requests which mutate
* state, within a hidden form field, query-string etc. This
* token is validated against the visitor's session.
*
* @param {Object} options
* @return {Function} middleware
* @public
*/
function csurf (options) {
var opts = options || {}
// get cookie options
var cookie = getCookieOptions(opts.cookie)
// get session options
var sessionKey = opts.sessionKey || 'session'
// get value getter
var value = opts.value || defaultValue
// token repo
var tokens = new Tokens(opts)
// ignored methods
var ignoreMethods = opts.ignoreMethods === undefined
? ['GET', 'HEAD', 'OPTIONS']
: opts.ignoreMethods
if (!Array.isArray(ignoreMethods)) {
throw new TypeError('option ignoreMethods must be an array')
}
// generate lookup
var ignoreMethod = getIgnoredMethods(ignoreMethods)
return function csrf (req, res, next) {
// validate the configuration against request
if (!verifyConfiguration(req, sessionKey, cookie)) {
return next(new Error('misconfigured csrf'))
}
// get the secret from the request
var secret = getSecret(req, sessionKey, cookie)
var token
// lazy-load token getter
req.csrfToken = function csrfToken () {
var sec = !cookie
? getSecret(req, sessionKey, cookie)
: secret
// use cached token if secret has not changed
if (token && sec === secret) {
return token
}
// generate & set new secret
if (sec === undefined) {
sec = tokens.secretSync()
setSecret(req, res, sessionKey, sec, cookie)
}
// update changed secret
secret = sec
// create new token
token = tokens.create(secret)
return token
}
// generate & set secret
if (!secret) {
secret = tokens.secretSync()
setSecret(req, res, sessionKey, secret, cookie)
}
// verify the incoming token
if (!ignoreMethod[req.method] && !tokens.verify(secret, value(req))) {
return next(createError(403, 'invalid csrf token', {
code: 'EBADCSRFTOKEN'
}))
}
next()
}
}
/**
* Default value function, checking the `req.body`
* and `req.query` for the CSRF token.
*
* @param {IncomingMessage} req
* @return {String}
* @api private
*/
function defaultValue (req) {
return (req.body && req.body._csrf) ||
(req.query && req.query._csrf) ||
(req.headers['csrf-token']) ||
(req.headers['xsrf-token']) ||
(req.headers['x-csrf-token']) ||
(req.headers['x-xsrf-token'])
}
/**
* Get options for cookie.
*
* @param {boolean|object} [options]
* @returns {object}
* @api private
*/
function getCookieOptions (options) {
if (options !== true && typeof options !== 'object') {
return undefined
}
var opts = Object.create(null)
// defaults
opts.key = '_csrf'
opts.path = '/'
if (options && typeof options === 'object') {
for (var prop in options) {
var val = options[prop]
if (val !== undefined) {
opts[prop] = val
}
}
}
return opts
}
/**
* Get a lookup of ignored methods.
*
* @param {array} methods
* @returns {object}
* @api private
*/
function getIgnoredMethods (methods) {
var obj = Object.create(null)
for (var i = 0; i < methods.length; i++) {
var method = methods[i].toUpperCase()
obj[method] = true
}
return obj
}
/**
* Get the token secret from the request.
*
* @param {IncomingMessage} req
* @param {String} sessionKey
* @param {Object} [cookie]
* @api private
*/
function getSecret (req, sessionKey, cookie) {
// get the bag & key
var bag = getSecretBag(req, sessionKey, cookie)
var key = cookie ? cookie.key : 'csrfSecret'
if (!bag) {
throw new Error('misconfigured csrf')
}
// return secret from bag
return bag[key]
}
/**
* Get the token secret bag from the request.
*
* @param {IncomingMessage} req
* @param {String} sessionKey
* @param {Object} [cookie]
* @api private
*/
function getSecretBag (req, sessionKey, cookie) {
if (cookie) {
// get secret from cookie
var cookieKey = cookie.signed
? 'signedCookies'
: 'cookies'
return req[cookieKey]
} else {
// get secret from session
return req[sessionKey]
}
}
/**
* Set a cookie on the HTTP response.
*
* @param {OutgoingMessage} res
* @param {string} name
* @param {string} val
* @param {Object} [options]
* @api private
*/
function setCookie (res, name, val, options) {
var data = Cookie.serialize(name, val, options)
var prev = res.getHeader('set-cookie') || []
var header = Array.isArray(prev) ? prev.concat(data)
: [prev, data]
res.setHeader('set-cookie', header)
}
/**
* Set the token secret on the request.
*
* @param {IncomingMessage} req
* @param {OutgoingMessage} res
* @param {string} sessionKey
* @param {string} val
* @param {Object} [cookie]
* @api private
*/
function setSecret (req, res, sessionKey, val, cookie) {
if (cookie) {
// set secret on cookie
var value = val
if (cookie.signed) {
value = 's:' + sign(val, req.secret)
}
setCookie(res, cookie.key, value, cookie)
} else {
// set secret on session
req[sessionKey].csrfSecret = val
}
}
/**
* Verify the configuration against the request.
* @private
*/
function verifyConfiguration (req, sessionKey, cookie) {
if (!getSecretBag(req, sessionKey, cookie)) {
return false
}
if (cookie && cookie.signed && !req.secret) {
return false
}
return true
}