app/controllers/authentication-controller.js

/**
 * @module authentication-controller
 */

const csrfProtection = require( 'csurf' )( {
    cookie: true
} );
const jwt = require( 'jwt-simple' );
const express = require( 'express' );
const router = express.Router();
// var debug = require( 'debug' )( 'authentication-controller' );

module.exports = app => {
    app.use( `${app.get( 'base path' )}/`, router );
};

router
    .get( '/login', csrfProtection, login )
    .get( '/logout', logout )
    .post( '/login', csrfProtection, setToken );

/**
 * @param {module:api-controller~ExpressRequest} req - HTTP request
 * @param {module:api-controller~ExpressResponse} res - HTTP response
 * @param {Function} next - Express callback
 */
function login( req, res, next ) {
    let error;
    const authSettings = req.app.get( 'linked form and data server' ).authentication;
    const returnUrl = req.query.return_url || '';

    if ( authSettings.type.toLowerCase() !== 'basic' ) {
        if ( authSettings.url ) {
            // the url is expected to:
            // - authenticate the user,
            // - set a session cookie (cross-domain if necessary) or add a token as query parameter to the return URL,
            // - and return the user back to Enketo
            // - enketo will then pass the cookie or token along when requesting resources, or submitting data
            // Though returnUrl was encoded with encodeURIComponent, for some reason it appears to have been automatically decoded here.
            res.redirect( authSettings.url.replace( '{RETURNURL}', encodeURIComponent( returnUrl ) ) );
        } else {
            error = new Error( 'Enketo configuration error. External authentication URL is missing.' );
            error.status = 500;
            next( error );
        }
    } else if ( req.app.get( 'env' ) !== 'production' || req.protocol === 'https' || req.headers[ 'x-forwarded-proto' ] === 'https' || req.app.get( 'linked form and data server' ).authentication[ 'allow insecure transport' ] ) {
        res.render( 'surveys/login', {
            csrfToken: req.csrfToken(),
            server: req.app.get( 'linked form and data server' ).name
        } );
    } else {
        error = new Error( 'Forbidden. Enketo needs to use https in production mode to enable authentication.' );
        error.status = 405;
        next( error );
    }
}

/**
 * @param {module:api-controller~ExpressRequest} req - HTTP request
 * @param {module:api-controller~ExpressResponse} res - HTTP response
 */
function logout( req, res ) {
    res
        .clearCookie( req.app.get( 'authentication cookie name' ) )
        .clearCookie( '__enketo_meta_username' )
        .clearCookie( '__enketo_logout' )
        .render( 'surveys/logout' );
}

/**
 * @param {module:api-controller~ExpressRequest} req - HTTP request
 * @param {module:api-controller~ExpressResponse} res - HTTP response
 */
function setToken( req, res ) {
    const username = req.body.username.trim();
    const maxAge = 30 * 24 * 60 * 60 * 1000;
    const returnUrl = req.query.return_url || '';

    const token = jwt.encode( {
        user: username,
        pass: req.body.password
    }, req.app.get( 'encryption key' ) );

    // Do not allow authentication cookies to be saved if enketo runs on http, unless 'allow insecure transport' is set to true
    // This is double because the check in login() already ensures the login screen isn't even shown.
    const secure = ( req.protocol === 'production' && !req.app.get( 'linked form and data server' ).authentication[ 'allow insecure transport' ] );

    const authOptions = {
        secure,
        signed: true,
        httpOnly: true,
        path: '/'
    };

    const uidOptions = {
        signed: true,
        maxAge: 30 * 24 * 60 * 60 * 1000,
        path: '/'
    };

    if ( req.body.remember ) {
        authOptions.maxAge = maxAge;
        uidOptions.maxAge = maxAge;
    }

    // store the token in a cookie on the client
    res
        .cookie( req.app.get( 'authentication cookie name' ), token, authOptions )
        .cookie( '__enketo_logout', true )
        .cookie( '__enketo_meta_username', username, uidOptions );

    if ( returnUrl ) {
        res.redirect( returnUrl );
    } else {
        res.send( 'Username and password are stored. You can close this page now.' );
    }
}