all files / blackbird/modules/middleware/ token.js

88.89% Statements 24/27
86.36% Branches 19/22
100% Functions 2/2
88.89% Lines 24/27
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101                                                                                                                                                         
/* */
const mach = require("../index");
const makeToken = require("../utils/makeToken");
const {is, not, isNil} = require("ramda");
mach.extend(
  require("../extensions/server")
);
 
/**
 * The set of HTTP request methods that are considered safe because they
 * do not alter server data.
 */
const SAFE_METHODS = {
    GET: true,
    HEAD: true,
    OPTIONS: true,
    TRACE: true
};
 
/**
 * A middleware that helps to prevent Cross-site Request Forgery attacks by
 * requiring the client to include an authentication token in all form
 * submissions that matches a value stored in the session cookie. See
 * http://www.codinghorror.com/blog/2008/10/preventing-csrf-and-xsrf-attacks.html
 *
 * If the session does not already have an authentication token one is
 * automatically generated and stored in the session. The default session key
 * is "_token". All form submissions need to include this value in the "_token"
 * parameter, like this:
 *
 *   <form method="POST" action="/">
 *     <input type="hidden" name="_token" value="{{session._token}}">
 *   </form>
 *
 * On the backend, you need to put both mach.session and mach.params in front of
 * mach.token in order for it to be able to retrieve values from the request session
 * and parameters, like this:
 *
 *   app.use(mach.session);
 *   app.use(mach.params);
 *   app.use(mach.token);
 *   app.run(function (conn) {
 *     // The connection authenticated successfully
 *   });
 *
 * Options may be any of the following:
 *
 * - paramName        The name of the request parameter that contains the token
 *                    (i.e. the value of the "name" attribute on your <input>).
 *                    Defaults to "_token"
 * - sessionKey       The name of the session variable to use to store the token.
 *                    Defaults to "_token"
 * - byteLength       The length of the token in bytes. Defaults to 32
 *
 * Note: Non-POST requests are always forwarded to the downstream app regardless of
 * whether or not they contain the token since it is assumed they are not modifying
 * anything and are safe.
 */
function verifyToken(app, options) {
    options = options || {};
 
    Iif (is(String, options)) {
        options = {paramName: options};
    }
 
    const paramName = options.paramName || "_token";
    const sessionKey = options.sessionKey || "_token";
    const byteLength = options.byteLength || 32;
 
    return function (conn) {
        const session = conn.session, params = conn.params;
 
        Iif (isNil(session)) {
            conn.onError(new Error("No session! Use mach.session in front of mach.token"));
        } else Iif (isNil(params)) {
            conn.onError(new Error("No params! Use mach.params in front of mach.token"));
        } else {
            let token = session[sessionKey];
 
      // Create a new session token if needed.
            if (!token) {
                token = session[sessionKey] = makeToken(byteLength);
            }
 
            if (params[paramName] && params[paramName] === token) {
                return conn.run(app);
            }
        }
 
    // If the request is not a POST we assume it's not a form submission
    // and therefore not modifying anything. Pass it downstream.
        if (SAFE_METHODS[conn.method] === true) {
            return conn.run(app);
        }
 
        conn.text(403, "Forbidden");
    };
}
 
module.exports = verifyToken;