Source: protocol/authenticate.js

'use strict';
/**
 * @module  lib/protocol/authenticate
 * @summary Whiteflag authentication module
 * @description Module for Whiteflag authentication
 * @tutorial modules
 * @tutorial protocol
 */
module.exports = {
    // Authentication functions
    message: verifyMessage,
    verify: verifyAuthentication,
    remove: removeAuthentication,
    sign: createSignature,
    decodeSignature,
    verifySignature,
    generateToken
};

// Node.js core and external modules //
const jwt = require('jsonwebtoken');
const http = require('http');
const https = require('https');

// Whiteflag common functions and classes //
const log = require('../common/logger');
const array = require('../common/arrays');
const { type } = require('../common/protocol');
const { ProcessingError, ProtocolError } = require('../common/errors');

// Whiteflag modules //
const wfApiBlockchains = require('../blockchains');
const wfCrypto = require('./crypto');
const wfState = require('./state');

// Whiteflag configuration data //
const wfConfigData = require('./config').getConfig();

// Module constants //
const MODULELOG = 'authenticate';
const BINENCODING = 'hex';
const AUTHMESSAGECODE = 'A';

// MAIN MODULE FUNCTIONS //
/**
 * Checks if message can be authenticated and updates metaheader accordingly
 * @function verifyMessage
 * @alias module:lib/protocol/authenticate.message
 * @param {wfMessage} wfMessage a Whiteflag message
 * @param {function(Error, wfMessage)} callback function to be called upon completion
 */
function verifyMessage(wfMessage, callback) {
    // Lookup originator in state
    wfState.getOriginatorData(wfMessage.MetaHeader.originatorAddress, function verifyOrigGetDataCb(err, originator) {
        if (err) return callback(err, wfMessage);
        // Originator found in state
        if (originator && wfMessage.MetaHeader.originatorAddress.toLowerCase() === originator.address.toLowerCase()) {
            if (originator.authenticationValid) {
                wfMessage.MetaHeader.originatorValid = true;
                return callback(null, wfMessage);
            }
        }
        // Originator not found in state or no valid authentication data
        if (wfConfigData.authentication.strict) {
            wfMessage.MetaHeader.originatorValid = false;
        }
        return callback(null, wfMessage);
    });
}

/**
 * Checks the authentication information of the message originator and updates metaheader accordingly
 * @function verifyAuthentication
 * @alias module:lib/protocol/authenticate.verify
 * @param {wfMessage} wfAuthMessage a Whiteflag authentication message
 * @param {function(Error, wfMessage)} callback function called upon completion
 */
function verifyAuthentication(wfAuthMessage = {}, callback) {
    if (wfAuthMessage.MessageHeader.MessageCode !== AUTHMESSAGECODE) {
        return callback(new ProcessingError(`Not an authentication message: ${type(wfAuthMessage)} message`), wfAuthMessage);
    }
    // Check indicator for authentication type
    switch (wfAuthMessage.MessageBody.VerificationMethod) {
        case '1': {
            // Digital Signature
            return wfAuthentication1(wfAuthMessage, callback);
        }
        case '2': {
            // Shared Token
            return wfAuthentication2(wfAuthMessage, callback);
        }
        default: {
            // Method does not exist
            return callback(new ProtocolError(`Invalid authentication method: ${type(wfAuthMessage)}`));
        }
    }
}

/**
 * Removes the authentication information of the message originator
 * @function removeAuthentication
 * @alias lib/protocol/authenticate.remove
 * @param {wfMessage} wfAuthMessage a Whiteflag authentication message
 * @param {function(Error, wfMessage)} callback
 */
function removeAuthentication(wfAuthMessage = {}, callback) {
    // Check messageheader
    if (wfAuthMessage.MessageHeader.MessageCode !== AUTHMESSAGECODE) {
        return callback(new ProcessingError(`Cannot remove authentication: Not an authentication message: ${wfAuthMessage.MessageHeader.MessageCode}`), wfAuthMessage);
    }
    if (
        wfAuthMessage.MessageHeader.ReferenceIndicator !== '1'
        && wfAuthMessage.MessageHeader.ReferenceIndicator !== '4'
    ) {
        return callback(new ProcessingError(`Cannot remove authentication: ${type(wfAuthMessage)} message does not have reference code 1 or 4`), wfAuthMessage);
    }
    // Check for referenced message in originators state
    wfState.getOriginatorData(wfAuthMessage.MetaHeader.originatorAddress, function authGetOriginatorCb(err, originator) {
        if (err) log.error(MODULELOG, `Error getting originator state: ${err.message}`);

        // Check originator
        if (!originator) {
            // Authentication message from previously unknown originator
            log.debug(MODULELOG, `Cannot remove authentication: ${type(wfAuthMessage)} message is from unknown originator: ${wfAuthMessage.MetaHeader.originatorAddress}`);
            wfAuthMessage.MetaHeader.validationErrors = array.addArray(wfAuthMessage.MetaHeader.validationErrors, 'Unknown originator');
            return callback(null, wfAuthMessage);
        }
        // Check if any known authentication messages
        if (!originator.authenticationMessages) {
            log.debug(MODULELOG, `Cannot process ${type(wfAuthMessage)} message: No authentication messages known for originator: ${originator.address}`);
            return callback(null, wfAuthMessage);
        }
        // Authentication message from known originator
        wfAuthMessage.MetaHeader.originatorValid = true;
        const authIndex = originator.authenticationMessages.findIndex(hash => hash === wfAuthMessage.MessageHeader.ReferencedMessage);
        if (authIndex >= 0) {
            log.debug(MODULELOG, `Removing authentication message from originators state: ${originator.authenticationMessages[authIndex]}`);
            originator.authenticationMessages.splice(authIndex, 1);
            originator.updated = new Date().toISOString();
            wfState.upsertOriginatorData(originator);
        }
        // Check if any authentication message transaction hashes left
        if (originator.authenticationMessages.length === 0) {
            log.debug(MODULELOG, `No valid authentication messages anymore for originator: ${originator.address}`);
            originator.authenticationValid = false;
            originator.updated = new Date().toISOString();
            wfState.upsertOriginatorData(originator);
        }
        return callback(null, wfAuthMessage);
    });
}

/**
 * Requests verification data of an authentication token for the specified blockchain address
 * @function generateToken
 * @alias module:lib/protocol/authenticate.generateToken
 * @param {string} authToken the secret authentication token in hexadecimal
 * @param {string} address the address of the account for which the signature is requested
 * @param {string} blockchain the name of the blockchain
 * @param {function(Error, verificationData)} callback function to be called upon completion
 * @typedef {string} verificationData hexadecimal representation of the authentication token verification data
 */
function generateToken(authToken, address, blockchain, callback) {
    let tokenBuffer = Buffer.from(authToken, BINENCODING);
    authToken = undefined;

    // Get blockchain address is binary
    wfApiBlockchains.getBinaryAddress(address, blockchain, function authGetAddressCb(err, addressBuffer) {
        if (err) return callback(err, null);

        // Generate authentication token and compare with message
        wfCrypto.getTokenVerificationData(tokenBuffer, addressBuffer, function authGenerateTokenCb(err, dataBuffer) {
            if (err) return callback(err, null);
            return callback(null, dataBuffer.toString('hex').toLowerCase());
        });
    });
}

/**
 * A Whiteflag authentication signature object
 * @typedef {Object} wfSignature
 * @property {string} protected Encoded signature header to identify which algorithm is used to generate the signature
 * @property {string} payload Encoded payload with the information as defined in the Whiteflag protocol specification
 * @property {string} signature The digital signature validating the information contained in the payload
 */
/**
 * A Whiteflag decoded authentication signature object
 * @typedef {Object} wfSignatureDecoded
 * @property {Object} header Signature header to identify which algorithm is used to generate the signature
 * @property {wfSignaturePayload} payload Payload object of a Whiteflag authentication signature
 * @property {string} signature The digital signature validating the information contained in the payload
 */
/**
 * A Whiteflag authentication signature payload object
 * @typedef {Object} wfSignaturePayload
 * @property {string} addr The blockchain address used to send the corresponding `A1` message and of which the corresponding private key is used to create the signature
 * @property {string} orgname The name of the originator, which can be chosen freely
 * @property {string} url The same URL as in the `VerificationData` field of the corresponding `A1` message
 * @property {string} extpubkey The serialised extended parent public key from which the child public keys and addresses used by this originator can be derived (currently not supported)
 */

/**
 * Requests a authentication signature for the appropriate blockchain
 * @function createSignature
 * @alias module:lib/protocol/authenticate.sign
 * @param {wfSignaturePayload} signPayload the signature payload object to be signed
 * @param {string} address the address of the account for which the signature is requested
 * @param {string} blockchain the blockchain for which the signature is requested
 * @param {function(Error, wfSignature, wfSignatureDecoded)} callback function to be called upon completion
 */
function createSignature(signPayload = {}, address, blockchain, callback) {
    // Check blockchain and address
    if (!blockchain || !address) {
        return callback(new ProcessingError('Missing blockchain or address', null, 'WF_API_BAD_REQUEST'));
    }
    // Check request for complete signature payload
    let signErrors = [];
    if (!signPayload.addr) signPayload.addr = address;
    if (signPayload.addr !== address) signErrors.push('Signature address does not match blockchain account');
    if (!signPayload.orgname) signErrors.push('Missing authentication signature property: orgname');
    if (!signPayload.url) signErrors.push('Missing authentication signature property: url');
    if (signErrors.length > 0) {
        return callback(new ProtocolError('Invalid Whiteflag authentication signature request', signErrors, 'WF_SIGN_ERROR'), null);
    }
    // Get signature for the appropriate blockchain
    wfApiBlockchains.requestSignature(signPayload, blockchain, function authRequestSignatureCb(err, wfSignature) {
        if (err) return callback(err);

        // Check signature before returning response
        decodeSignature(wfSignature, function authVerifySignatureCb(err, wfSignatureDecoded) {
            if (err) return callback(err);

            // Return verified signature with decoded signature
            return callback(null, wfSignature, wfSignatureDecoded);
        });
    });
}

/**
 * Decodes authentication signature
 * @function decodeSignature
 * @alias module:lib/protocol/authenticate.decodeSignature
 * @param {wfSignature} wfSignature a Whiteflag authentication signature
 * @param {function(Error, wfSignatureDecoded)} callback function to be called upon completion
 * @todo Detailed error processing after JWT decoding
 */
function decodeSignature(wfSignature, callback) {
    // Check properties of the signature object
    let signErrors = [];
    if (!wfSignature.protected) signErrors.push('Missing object property: protected');
    if (!wfSignature.payload) signErrors.push('Missing object property: payload');
    if (!wfSignature.signature) signErrors.push('Missing object property: signature');
    if (signErrors.length > 0) {
        return callback(new ProtocolError('Invalid Whiteflag authentication signature', signErrors, 'WF_SIGN_ERROR'), null);
    }
    // Try to decode the signature and return result
    let wfSignatureDecoded = {};
    try {
        const signatureToken = wfSignature.protected
                        + '.' + wfSignature.payload
                        + '.' + wfSignature.signature;
        wfSignatureDecoded = jwt.decode(signatureToken, {complete: true});
    } catch(err) {
        return callback(err, null);
    }
    return callback(null, wfSignatureDecoded);
}

/**
 * A Whiteflag authentication signature object
 * @typedef {Object} wfExtendedSignature
 * @property {string} blockchain the name of the blockchain
 * @property {string} originatorPubKey the blockchain account public key of the originator
 * @property {wfSignature} wfSignature A Whiteflag authentication signature object
 */

/**
 * Verifies authentication signature
 * @function verifySignature
 * @alias module:lib/protocol/authenticate.verifySignature
 * @param {wfExtendedSignature} wfExtSignature an extended Whiteflag authentication signature
 * @param {function(Error, wfSignatureDecoded, Object)} callback function to be called upon completion
 */
function verifySignature(wfExtSignature, callback) {
    // Check properties of the extended signature object
    let signErrors = [];
    if (!wfExtSignature.blockchain) signErrors.push('Missing object property: blockchain');
    if (!wfExtSignature.originatorPubKey) signErrors.push('Missing object property: originatorPubKey');
    if (!wfExtSignature.wfSignature) signErrors.push('Missing object property: wfSignature');
    if (signErrors.length > 0) {
        return callback(new ProcessingError('Invalid extended Whiteflag authentication signature', signErrors), null);
    }
    // Check properties of the signature object
    if (!wfExtSignature.wfSignature.protected) signErrors.push('Missing object property: protected');
    if (!wfExtSignature.wfSignature.payload) signErrors.push('Missing object property: payload');
    if (!wfExtSignature.wfSignature.signature) signErrors.push('Missing object property: signature');
    if (signErrors.length > 0) {
        return callback(new ProtocolError('Invalid Whiteflag authentication signature', signErrors, 'WF_SIGN_ERROR'), null);
    }
    // Construct JSON Web Token for verification
    const signatureToken = wfExtSignature.wfSignature.protected
        + '.' + wfExtSignature.wfSignature.payload
        + '.' + wfExtSignature.wfSignature.signature;

    // Get keys and verify signature
    wfApiBlockchains.requestKeys(wfExtSignature.originatorPubKey, wfExtSignature.blockchain, function authRequestKeysCb(err, originatorKeys) {
            if (err) return callback(err);

            // Call verification function and check for errors
            jwt.verify(signatureToken,
                       originatorKeys.publicKey.pem,
                       { allowInvalidAsymmetricKeyTypes: true },
                       function authVerifySignatureCb(err, wfSignatureDecoded) {
                if (err) {
                    if (err.name === 'JsonWebTokenError'
                        || err.name === 'TokenExpiredError'
                        || err.name === 'NotBeforeError'
                    ) {
                        return callback(new ProtocolError('Invalid Whiteflag authentication signature', [ `${err.name}: ${err.message}` ], 'WF_SIGN_ERROR'));
                    }
                    return callback(new Error(`Could not verify signature: ${err.message}`));
                }
                return callback(null, wfSignatureDecoded, originatorKeys);
            }
        );
    });
}

// PRIVATE MODULE FUNCTIONS //
/**
 * Verifies the information for authentication method 1
 * @private
 * @param {Object} wfAuthMessage a Whiteflag authentication message
 * @param {function(Error, wfMessage)} callback function to be called upon completion
 */
function wfAuthentication1(wfAuthMessage, callback) {
    let authURL;
    let validDomains = [];

    // Get URL in authentication message and from valid domain list in config file
    try {
        // eslint-disable-next-line no-undef
        authURL = new URL(wfAuthMessage.MessageBody.VerificationData);
        validDomains = wfConfigData.authentication['1'].validDomains || [];
    } catch(err) {
        log.error(MODULELOG, `Could not get signature URLs: ${err.message}`);
        wfAuthMessage.MetaHeader.validationErrors = array.addItem(
            wfAuthMessage.MetaHeader.validationErrors,
            `Could not get signature URLs: ${err.message}`
        );
        return callback(err, wfAuthMessage);
    }
    // Check for valid domain names
    if (validDomains.length > 0 && !validDomains.includes(authURL.hostname)) {
        wfAuthMessage.MetaHeader.validationErrors = array.addItem(
            wfAuthMessage.MetaHeader.validationErrors,
            `The domain that holds the authentication signature is not considered valid: ${authURL.hostname}`
        );
        wfAuthMessage.MetaHeader.originatorValid = false;
        return callback(null, wfAuthMessage);
    }
    // Get signature from an internet resource
    getSignature(authURL, function authGetSignatureCb(err, wfSignature) {
        if (err) {
            if (!(err instanceof ProcessingError || err instanceof ProtocolError)) {
                log.error(MODULELOG, `Could not get signature: ${err.message}`);
            }
            wfAuthMessage.MetaHeader.validationErrors = array.addItem(
                wfAuthMessage.MetaHeader.validationErrors,
                `Could not get signature: ${err.message}`);
            return callback(err, wfAuthMessage);
        }
        // Verify the signature
        const wfExtSignature = {
            blockchain: wfAuthMessage.MetaHeader.blockchain,
            originatorPubKey: wfAuthMessage.MetaHeader.originatorPubKey,
            wfSignature: wfSignature
        };
        verifySignature(wfExtSignature, function authVerifySignatureCb(err, wfSignatureDecoded, originatorKeys) {
            if (err) {
                log.warn(MODULELOG, `Could not verify signature from ${wfAuthMessage.MessageBody.VerificationData}: ${err.message}`);
                wfAuthMessage.MetaHeader.validationErrors = array.addItem(
                    wfAuthMessage.MetaHeader.validationErrors,
                    `Could not verify signature: ${err.message}`
                );
                return callback(null, wfAuthMessage);
            }
            if (!wfSignatureDecoded) {
                log.debug(MODULELOG, `Could not decode and verify signature from ${wfAuthMessage.MessageBody.VerificationData}`);
                wfAuthMessage.MetaHeader.validationErrors = array.addItem(
                    wfAuthMessage.MetaHeader.validationErrors,
                    'Could not decode and verify signature'
                );
                return callback(null, wfAuthMessage);
            }
            log.trace(MODULELOG, `Verified signature from ${wfAuthMessage.MessageBody.VerificationData}: ` + JSON.stringify(wfSignatureDecoded));

            // Perform authentication checks
            let authenticationErrors = [];
            if (wfSignatureDecoded.addr.toLowerCase() !== originatorKeys.address.toLowerCase()) {
                authenticationErrors.push(`Signature address does not correspond with derived blockchain address: ${wfSignatureDecoded.addr} != ${originatorKeys.address}`);
                wfAuthMessage.MetaHeader.originatorValid = false;
            }
            if (wfSignatureDecoded.addr.toLowerCase() !== wfAuthMessage.MetaHeader.originatorAddress.toLowerCase()) {
                authenticationErrors.push(`Signature address does not correspond with message address: ${wfSignatureDecoded.addr} != ${wfAuthMessage.MetaHeader.originatorAddress}`);
                wfAuthMessage.MetaHeader.originatorValid = false;
            }
            if (wfSignatureDecoded.url !== wfAuthMessage.MessageBody.VerificationData) {
                authenticationErrors.push(`Signature URL does not correspond with authentication message URL: ${wfSignatureDecoded.url} != ${wfAuthMessage.MessageBody.VerificationData}`);
                wfAuthMessage.MetaHeader.originatorValid = false;
            }
            // Check result and update metaheader and state
            if (authenticationErrors.length > 0) {
                // Return with authentication errors
                wfAuthMessage.MetaHeader.originatorValid = false;
                wfAuthMessage.MetaHeader.validationErrors = array.addArray(wfAuthMessage.MetaHeader.validationErrors, authenticationErrors);
                let err = new ProtocolError(`Could not authenticate originator of ${type(wfAuthMessage)} message: ${wfAuthMessage.MetaHeader.transactionHash}`, authenticationErrors, 'WF_AUTH_ERROR');
                return callback(err, wfAuthMessage);
            }
            // Authentication is valid
            wfAuthMessage.MetaHeader.originatorValid = true;

            // Update originator state
            let originatorData = {
                name: wfSignatureDecoded.orgname,
                blockchain: wfAuthMessage.MetaHeader.blockchain,
                address: wfAuthMessage.MetaHeader.originatorAddress,
                originatorPubKey: wfAuthMessage.MetaHeader.originatorPubKey,
                url: wfSignatureDecoded.url,
                updated: new Date().toISOString(),
                authenticationValid: true,
                authenticationMessages: [ wfAuthMessage.MetaHeader.transactionHash ]
            };
            if (wfAuthMessage.MessageHeader.ReferenceIndicator === '0') {
                originatorData.authenticationMessages = [ wfAuthMessage.MetaHeader.transactionHash ];
            }
            log.debug(MODULELOG, `Updating state with authenticated originator of ${type(wfAuthMessage)} message: ${wfAuthMessage.MetaHeader.transactionHash}: ` + JSON.stringify(originatorData));
            originatorData.updated = new Date().toISOString();
            wfState.upsertOriginatorData(originatorData);
            return callback(null, wfAuthMessage);
        });
    });
}

/**
 * Verifies the information for authentication method 2
 * @private
 * @param {Object} wfAuthMessage a Whiteflag authentication message
 * @param {function(Error, wfMessage)} callback
 */
function wfAuthentication2(wfAuthMessage, callback) {
    // Get known authentication tokens and iterate over them
    wfState.getKeyIds('authTokens', function authGetKeyIdsCb(err, authTokenIds) {
        if (err) return callback(err, wfAuthMessage);
        iterateAuthTokens(authTokenIds, 0);
    });

    /**
     * Tries to match a known authentication token with the received authentication message
     * @private
     * @param {Array} authTokenIds Identifiers of all known authentication tokens
     * @param {number} t Token counter
     */
    function iterateAuthTokens(authTokenIds = [], t = 0) {
        // Return message if no more tokens
        if (t >= authTokenIds.length) {
            log.debug(MODULELOG, `Unknown originator authentication token in ${type(wfAuthMessage)} message: ${wfAuthMessage.MetaHeader.transactionHash}`);
            wfAuthMessage.MetaHeader.validationErrors = array.addItem(
                wfAuthMessage.MetaHeader.validationErrors,
                'Unknown originator authentication token'
            );
            return callback(null, wfAuthMessage);
        }
        log.trace(MODULELOG, `Trying authentication token ${(t + 1)}/${authTokenIds.length} for ${type(wfAuthMessage)} message: ${wfAuthMessage.MetaHeader.transactionHash} from ${wfAuthMessage.MetaHeader.blockchain} account ${wfAuthMessage.MetaHeader.originatorAddress}`);

        // Get secret authentication token from state and put in buffer
        wfState.getKey('authTokens', authTokenIds[t], function authGetKey(err, authToken) {
            if (err) return callback(err, wfAuthMessage);

            // Generate authentication token and compare with message
            generateToken(authToken, wfAuthMessage.MetaHeader.originatorAddress, wfAuthMessage.MetaHeader.blockchain, function authGenerateTokenCb(err, token) {
                if (err) return callback(err, wfAuthMessage);

                // Comnpare message authentication data with known token
                if (token !== wfAuthMessage.MessageBody.VerificationData.toLowerCase()) {
                    // No match; try next token
                    return iterateAuthTokens(authTokenIds, (t + 1));
                }
                log.trace(MODULELOG, `Found a matching authentication token for ${type(wfAuthMessage)} message: ${wfAuthMessage.MetaHeader.transactionHash}`);
                wfAuthMessage.MetaHeader.originatorValid = true;

                // Get originator data and update originator state
                wfState.getOriginatorAuthToken(authTokenIds[t], function authGetOriginatorTokenCb(err, originator) {
                    if (err) return callback(err, wfAuthMessage);
                    let name = '(unknown)';
                    if (originator && originator.name) name = originator.name;
                    let originatorData = {
                        name: name,
                        blockchain: wfAuthMessage.MetaHeader.blockchain,
                        address: wfAuthMessage.MetaHeader.originatorAddress,
                        originatorPubKey: wfAuthMessage.MetaHeader.originatorPubKey,
                        authTokenId: authTokenIds[t],
                        updated: new Date().toISOString(),
                        authenticationValid: true
                    };
                    if (wfAuthMessage.MessageHeader.ReferenceIndicator === '0') {
                        originatorData.authenticationMessages = [ wfAuthMessage.MetaHeader.transactionHash ];
                    }
                    log.debug(MODULELOG, `Updating state with authenticated originator of ${type(wfAuthMessage)} message: ${wfAuthMessage.MetaHeader.transactionHash}: ` + JSON.stringify(originatorData));
                    wfState.upsertOriginatorData(originatorData);
                    return callback(null, wfAuthMessage);
                });
            });
        });
    }
}

/**
 * Gets an authentication signature from an url
 * @private
 * @param {URL} authURL a URL to get the authentication signature from
 * @param {function(Error, wfSignature)} callback function to be called upon completion
 */
function getSignature(authURL, callback) {
    // Get data based on protocol specified in url
    switch (authURL.protocol) {
        case 'http:': {
                // Get signature over http
                http.get(
                    authURL,
                    httpResponseHandler
                ).on('error', errorHandler);
            break;
        }
        case 'https:': {
                // Get signature over https
                https.get(
                    authURL,
                    httpResponseHandler
                ).on('error', errorHandler);
            break;
        }
        default: {
            // Method does not exist
            errorHandler(new ProcessingError(`Unsupported protocol for retrieving signature: ${authURL.protocol}`, null, 'WF_API_NOT_IMPLEMENTED'));
        }
    }
    /**
     * Handles http request errors
     * @private
     * @param {Error} err error object if any error
     */
    function errorHandler(err) {
        if (err && err instanceof ProcessingError) return callback(err);
        return callback(new Error(`Error when retrieving signature from ${authURL.href}: ${err.message}`));
    }
    /**
     * Handles http responses
     * @private
     * @param {Object} res http response
     */
    function httpResponseHandler(res) {
        let resBody = '';
        // Get data chuncks
        res.on('data', function authHttpDataCb(data) {
            resBody += data;
        });
        // Get signature from response body
        let wfSignature;
        res.on('end', function authHttpEndCb() {
            try {
                wfSignature = JSON.parse(resBody);
            } catch(err) {
                errorHandler(err);
            }
            return callback(null, wfSignature);
        });
    }
}